发布基线

This commit is contained in:
TermiNexus committed 2026-11-07 19:35:29 +08:00
1 parent 7385f9ed3b
commit c3b322c7d1
44 files changed
+11797 -10553

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
def a(l: int):
pass
def main2() -> int*:
a(1)
# 1024000000000000000000
return a
File diff suppressed because it is too large. Load diff
+16 -16
View File
@@ -1,10 +1,10 @@
from stdint import *
import string
import stdlib
import sys
import memhub
import viperlib
import io
import win32.win32base
import ast
import mlirlite
import lib.core.VLogger as VLogger
@@ -233,7 +233,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
# On-demand dependency scan (App-first): seed the queue from App imports, then BFS-resolve
# reachable includes only (read + SHA1 + import parse); unreferenced ones are never read.
t_scan0: uint64 = win32.win32base.GetTickCount64()
t_scan0: uint64 = sys.monotonic_ms()
reachable_set: str = None
reachable_count: int = 0
use_reachable: int = 0
@@ -257,7 +257,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
else:
VLogger.warning("SourceDir unavailable; cannot build the dependency graph; skipping Phase1", "Phase1")
return 0
t_scan1: uint64 = win32.win32base.GetTickCount64()
t_scan1: uint64 = sys.monotonic_ms()
VLogger.info(f"dependency-graph scan total time: {t_scan1 - t_scan0} ms (reachable {reachable_count} includes)", "Phase1")
# Build the reachable FileEntry list from the Sha1Store; 1a/1b-pre/1b iterate result
@@ -348,7 +348,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
# Iterate files: topo order preferred, falling back to alphabetical
iter_count: int = int(topo_count if topo_count > 0 else result.Count)
# 1a total-time start (struct/enum/union registration)
t_a0: uint64 = win32.win32base.GetTickCount64()
t_a0: uint64 = sys.monotonic_ms()
for i in range(int(iter_count)):
# Per-file mark: free this file's AST after translating (registered data is in perm_pool)
p1a_mark: sizet = mb.mark()
@@ -382,7 +382,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
continue
# Per-file timer start (read + parse, to locate 1a stalls)
t_fa0: uint64 = win32.win32base.GetTickCount64()
t_fa0: uint64 = sys.monotonic_ms()
# Read file contents
file_path_r: str = entry_r.Path
@@ -427,7 +427,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
continue
# Timer 1: read + parse done
t_fa1: uint64 = win32.win32base.GetTickCount64()
t_fa1: uint64 = sys.monotonic_ms()
# Create Translator with _declare_only=1 (register struct/enum/union)
tr_r: HandlesTranslator.Translator* = HandlesTranslator.Translator()
@@ -446,7 +446,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
GenApi.reset_visible_structs(mb, 0)
ret_r: int = tr_r.translate(tree_r)
# Timer 2: translate(declare=1) returned
t_fa2: uint64 = win32.win32base.GetTickCount64()
t_fa2: uint64 = sys.monotonic_ms()
if ret_r != 0: p1a_fl += 1
else: p1a_reg += 1
@@ -471,7 +471,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
mb.reset_to(p1a_mark)
# 1a total time (struct/enum/union registration, incl. per-file read+parse+declaration)
t_a1: uint64 = win32.win32base.GetTickCount64()
t_a1: uint64 = sys.monotonic_ms()
VLogger.info(f"1a total time: {t_a1-t_a0} ms (registering struct/enum, reg={p1a_reg} skp={p1a_skp})", "Phase1")
if p1a_fl > 0:
@@ -498,7 +498,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
p1bpre_failed: int = 0
dir_len_pv: sizet = string.strlen(temp_dir)
# 1b-pre total-time start (vpi pre-generation)
t_pv0: uint64 = win32.win32base.GetTickCount64()
t_pv0: uint64 = sys.monotonic_ms()
for i_pv in range(int(result.Count)):
entry_addr_pv: uint64 = uint64(result.Entries) + i_pv * entry_size
entry_pv: IncludesScanner.FileEntry* = IncludesScanner.FileEntry*(entry_addr_pv)
@@ -596,7 +596,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
mb.reset_to(mark1)
# 1b-pre total time (vpi pre-generation)
t_pv1: uint64 = win32.win32base.GetTickCount64()
t_pv1: uint64 = sys.monotonic_ms()
VLogger.info(f"1b-pre total time: {t_pv1-t_pv0} ms (vpi pre-generation)", "Phase1")
if p1bpre_failed > 0:
@@ -609,7 +609,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
skipped: int = 0
failed: int = 0
# 1b total-time start (full translation)
t_b0: uint64 = win32.win32base.GetTickCount64()
t_b0: uint64 = sys.monotonic_ms()
loop_n_b: int = int(result.Count)
if topo_count > 0: loop_n_b = topo_count
@@ -732,7 +732,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
stdlib.free(stub_path)
# Per-file timer start: read (parse/translate/dump segments, matching Phase2 instrumentation)
t_f0: uint64 = win32.win32base.GetTickCount64()
t_f0: uint64 = sys.monotonic_ms()
# Read file contents
file_path: str = entry.Path
@@ -786,7 +786,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
continue
# Timer 1: read + lexer/tokenize/parse done
t_f1: uint64 = win32.win32base.GetTickCount64()
t_f1: uint64 = sys.monotonic_ms()
# Register TypedefT by scanning source text: AST string fields (attr/id) point into src_buf
# and dangle across phases (reading attr faults), so match "NAME: TypedefT = TYPE" lines.
@@ -842,7 +842,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
ret: int = tr.translate(tree)
# Timer 2: translate returned
t_f2: uint64 = win32.win32base.GetTickCount64()
t_f2: uint64 = sys.monotonic_ms()
# Diagnostic: translate returned normally (if it hangs inside, this line is not printed,
# distinguishing an internal hang from a later dump hang).
if ret == 0: VLogger.info(f"translation complete (translate returned): {file_path}", "Phase1")
@@ -937,7 +937,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
stdlib.free(vpcall_text_p1)
# Timer 3: dump (stub+text) done; print the per-file three-segment timing
t_f3: uint64 = win32.win32base.GetTickCount64()
t_f3: uint64 = sys.monotonic_ms()
VLogger.info(f"[{i+1}/{loop_n_b}] translating: {file_path} (parse={t_f1-t_f0} ms, translate={t_f2-t_f1} ms, dump={t_f3-t_f2} ms, total={t_f3-t_f0} ms)", "Phase1")
# save dependencies(_imported_modules) for Phase B
@@ -971,7 +971,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
if sc_rc != 0: VLogger.error(f"memory corruption (free list, rc={sc_rc}): {file_path}", "Phase1")
# 1b total time (full translation + dump)
t_b1: uint64 = win32.win32base.GetTickCount64()
t_b1: uint64 = sys.monotonic_ms()
VLogger.info(f"1b total time: {t_b1-t_b0} ms (full translation, trans={translated} skip={skipped} fail={failed})", "Phase1")
# Release the topo-sort resources used by Phase 1b
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -626,7 +626,14 @@ def get_includes_binary_dir() -> str:
# (sha1 subdirectories) while TPC's includes cache at {IncludesDir}.binary/ holds flat
# "<sha1>.<triple>.pic.obj" files, so sharing one directory would mix the two layouts.
def mlir_dir_suffix() -> str:
"""Directory isolation suffix for the includes cache of the MLIR backend (always "_mlir")."""
"""Directory isolation suffix for the includes cache of the MLIR backend.
The object format is target-specific, so a Linux-target build uses a separate
cache from the Windows one (otherwise cross-builds would link the wrong objects):
Windows target -> "_mlir" Linux target -> "_mlir_linux"
"""
if TargetTriple is not None and string.strstr(TargetTriple, "linux") is not None:
return "_mlir_linux"
return "_mlir"
+17 -46
View File
@@ -4,9 +4,8 @@ import string
import stdlib
import viperlib
import hashlib
import win32.win32file
import win32.win32base
import io
import os
# Utils - project utility functions (SHA1 hashing, directory cleanup)
@@ -140,50 +139,22 @@ def PathToModuleName(path: str, strip_includes_prefix: int) -> str:
def CleanDir(dir_path: str) -> int:
"""Recursively delete files/subdirs under dir_path, returning the count."""
if dir_path is None: return -1
dir_len: sizet = string.strlen(dir_path)
pattern: bytes = stdlib.malloc(dir_len + 8)
if pattern is None: return -1
viperlib.snprintf(pattern, dir_len + 8, "%s/*", dir_path)
find_data: win32.win32file.WIN32_FIND_DATAA* = stdlib.malloc(win32.win32file.WIN32_FIND_DATAA.__sizeof__())
if find_data is None:
stdlib.free(pattern)
return -1
string.memset(find_data, 0, win32.win32file.WIN32_FIND_DATAA.__sizeof__())
handle: win32.win32base.HANDLE = win32.win32file.FindFirstFileA(pattern, find_data)
if handle == win32.win32base.INVALID_HANDLE_VALUE:
stdlib.free(pattern)
stdlib.free(find_data)
return 0
reader: os.DirReader* = os.scandir(dir_path)
if reader is None: return 0
entry: os.DirEntry = os.DirEntry()
deleted: int = 0
while True:
fname: str = find_data.cFileName
if fname is not None:
# Skip . and ..
if fname[0] == '.':
if fname[1] == '\0': fname = None
elif fname[1] == '.' and fname[2] == '\0': fname = None
if fname is not None:
full_path: bytes = JoinPath(dir_path, fname)
if full_path is not None:
# Check whether this is a directory
is_dir: uint32 = find_data.dwFileAttributes & win32.win32file.FILE_ATTRIBUTE_DIRECTORY
if is_dir != 0:
# Recurse into the subdirectory
sub_deleted: int = CleanDir(full_path)
if sub_deleted > 0: deleted += sub_deleted
# Remove the now-empty subdirectory
win32.win32file.RemoveDirectoryA(full_path)
else:
# Delete the file
if win32.win32file.DeleteFileA(full_path) != 0: deleted += 1
stdlib.free(full_path)
if win32.win32file.FindNextFileA(handle, find_data) == 0: break
win32.win32file.FindClose(handle)
stdlib.free(pattern)
stdlib.free(find_data)
rr: int = os.readdir(reader, &entry)
if rr == 0: break
if rr < 0: break
full_path: bytes = JoinPath(dir_path, entry.name)
if full_path is None: continue
if entry.is_dir:
sub_deleted: int = CleanDir(full_path)
if sub_deleted > 0: deleted += sub_deleted
os.rmdir(full_path)
else:
if os.remove(full_path) == 0: deleted += 1
stdlib.free(full_path)
os.closedir(reader)
return deleted
+45
View File
@@ -810,6 +810,25 @@ def _PyiDefaultStr(buf: bytes, buf_size: sizet, pos: sizet, node: ast.AST*, dept
pos = _PyiEmit(buf, buf_size, pos, num_buf_dv)
stdlib.free(num_buf_dv)
return pos
if cn.const_kind == ast.CONST_FLOAT:
# %.17g round-trips an IEEE double exactly (used elsewhere in this file too). Without
# this branch a float default emitted nothing, producing a malformed stub line
# `def f(x: float = )` that crashes the vpi default scan.
num_buf_df: str = stdlib.malloc(64)
if num_buf_df is not None:
viperlib.snprintf(num_buf_df, 64, "%.17g", cn.float_val)
# %.17g prints an integral float as "-1"/"1" (no dot or exponent). The vpi default
# scanner would classify that as an INTEGER and hand an i32 to a float parameter
# (cross-module defaults then read 0.0). Keep the token unambiguously float.
has_frac_df: int = 0
j_df: sizet = 0
while num_buf_df[j_df] != 0:
if num_buf_df[j_df] == '.' or num_buf_df[j_df] == 'e' or num_buf_df[j_df] == 'E': has_frac_df = 1
j_df += 1
pos = _PyiEmit(buf, buf_size, pos, num_buf_df)
if has_frac_df == 0: pos = _PyiEmit(buf, buf_size, pos, ".0")
stdlib.free(num_buf_df)
return pos
if cn.const_kind == ast.CONST_STR:
pos = _PyiEmit(buf, buf_size, pos, "\"")
if cn.str_val is not None: pos = _PyiEmit(buf, buf_size, pos, cn.str_val)
@@ -836,6 +855,32 @@ def _PyiDefaultStr(buf: bytes, buf_size: sizet, pos: sizet, node: ast.AST*, dept
if uop.operand is not None: return _PyiDefaultStr(buf, buf_size, pos, uop.operand, depth + 1)
return pos
# Call: struct-constructor / call default (e.g. Vec2(1.0, 2.0)). Emitting the expression
# keeps the .vpi parameter list syntactically valid (an empty default used to drop the
# whole parameter and produce an arity mismatch) and lets the default scanner encode the
# literal field values for cross-module reconstruction.
if k == ast.ASTKind.Call:
cc: ast.Call* = ast.Call*(node)
if cc.func is None: return pos
pos = _PyiDefaultStr(buf, buf_size, pos, cc.func, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, "(")
if cc.args is not None:
cargs: list[ast.AST*]* = cc.args
can: sizet = len(cargs)
for cai in range(int(can)):
if cai > 0: pos = _PyiEmit(buf, buf_size, pos, ", ")
pos = _PyiDefaultStr(buf, buf_size, pos, cargs.get(cai), depth + 1)
return _PyiEmit(buf, buf_size, pos, ")")
# Attribute: module-qualified constant / constructor (e.g. mod.Vec2, mod.SHARE)
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.value is not None:
pos = _PyiDefaultStr(buf, buf_size, pos, at.value, depth + 1)
pos = _PyiEmit(buf, buf_size, pos, ".")
if at.attr is not None: pos = _PyiEmit(buf, buf_size, pos, at.attr)
return pos
# Other complex expressions (BinOp etc.): emit nothing; caller enforces fail-fast
return pos
+281 -36
View File
@@ -456,14 +456,14 @@ def _lookup_vpi_typedef(src_sha1: str, name: str) -> str:
def _resolve_vpi_annot_with_typedefs(pool: memhub.MemBuddy*, src_sha1: str, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""Resolve a vpi annotation string, following same-module typedef aliases (max 4 levels)."""
if ret_annot is None or pool is None: return None
ty: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty)
ty: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty, src_sha1)
if ty is not None: return ty
cur: str = ret_annot
depth: int = 0
while depth < 4:
nxt: str = _lookup_vpi_typedef(src_sha1, cur)
if nxt is None: return None
ty = _resolve_vpi_ret_annot_str(pool, nxt, i32_ty, i8_ptr_ty)
ty = _resolve_vpi_ret_annot_str(pool, nxt, i32_ty, i8_ptr_ty, src_sha1)
if ty is not None: return ty
cur = nxt
depth += 1
@@ -974,7 +974,7 @@ def _resolve_vpi_param_annot_seg_template(pool: memhub.MemBuddy*, src_sha1: str,
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)
ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty, src_sha1)
stdlib.free(seg_buf_t)
break
stdlib.free(base_t)
@@ -1011,7 +1011,7 @@ def _repair_declare_params_from_text(pool: memhub.MemBuddy*, func: mlirlite.Func
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)
rt: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_r, i32_ty, i8_ptr_ty, src_sha1)
if rt is not None: param_ty_r = rt
stdlib.free(seg_buf_r)
mlirlite.add_param(pool, func, param_ty_r, "arg")
@@ -1041,7 +1041,7 @@ def _resolve_vpi_param_annot_seg(pool: memhub.MemBuddy*, src_sha1: str, cls_name
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)
seg_ty_p: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_p, i32_ty, i8_ptr_ty, src_sha1)
stdlib.free(seg_buf_p)
return seg_ty_p
@@ -1913,9 +1913,29 @@ def _vpi_struct_or_enum(seg: str) -> mlirlite.LLVMType*:
return struct_ty
# _vpi_struct_or_enum_in_module - module-aware variant of _vpi_struct_or_enum.
# A .vpi return/param annotation carries the DEFINING module's bare name (`-> Point`), so a
# bare-name registry lookup returns the first same-named struct from another module and the
# caller gets the wrong layout (cross-module same-name struct returned by value). Resolve by
# (name, src_sha1) first; only fall back to the context-free lookup when the module has no entry.
def _vpi_struct_or_enum_in_module(seg: str, src_sha1: str) -> mlirlite.LLVMType*:
"""Module-aware struct/enum lookup for a .vpi annotation segment."""
if seg is None: return None
if src_sha1 is not None:
se_m: GenTypes.StructEntry* = GenApi.find_struct_by_module(seg, src_sha1)
if se_m is not None:
en_m: mlirlite.LLVMType* = GenApi.get_enum_base_type_in_module(seg, src_sha1)
# struct forward placeholder must not shadow a real enum in the same module
if se_m.IsForwardDecl != 0: return en_m
if se_m.Ty is not None: return se_m.Ty
en_m2: mlirlite.LLVMType* = GenApi.get_enum_base_type_in_module(seg, src_sha1)
if en_m2 is not None: return en_m2
return _vpi_struct_or_enum(seg)
# _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*:
def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str, src_sha1: str = None) -> mlirlite.LLVMType*:
"""Resolve a single type segment to LLVMType; None if unrecognized."""
if seg is None or pool is None: return None
# Source-syntax descriptors / value references: `const T` / `volatile T` / `&T` /
@@ -1930,19 +1950,19 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
if trim_buf is None: return None
string.strncpy(trim_buf, seg + seg_sp0, seg_ep0 - seg_sp0)
trim_buf[seg_ep0 - seg_sp0] = '\0'
trim_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, trim_buf)
trim_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, trim_buf, src_sha1)
stdlib.free(trim_buf)
return trim_ty
if seg_l0 - seg_sp0 >= 6 and string.strncmp(seg + seg_sp0, "const ", 6) == 0:
return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 6)
return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 6, src_sha1)
if seg_l0 - seg_sp0 >= 9 and string.strncmp(seg + seg_sp0, "volatile ", 9) == 0:
return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 9)
return _resolve_single_vpi_type_str(pool, seg + seg_sp0 + 9, src_sha1)
if seg_sp0 < seg_l0 and seg[seg_sp0] == '&':
# Value-reference parameter ABI: &T / &temp T -> T* (both modes use a pointer).
ref_sp1: sizet = seg_sp0 + 1
while ref_sp1 < seg_l0 and (seg[ref_sp1] == ' ' or seg[ref_sp1] == '\t'): ref_sp1 += 1
if seg_l0 - ref_sp1 >= 5 and string.strncmp(seg + ref_sp1, "temp ", 5) == 0: ref_sp1 += 5
inner_ref: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + ref_sp1)
inner_ref: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + ref_sp1, src_sha1)
if inner_ref is not None: return mlirlite.Ptr(pool, inner_ref)
return None
if seg_sp0 < seg_l0 and seg[seg_sp0] == '[':
@@ -1950,7 +1970,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
arr_rb: sizet = seg_sp0 + 1
while arr_rb < seg_l0 and seg[arr_rb] != ']': arr_rb += 1
if arr_rb < seg_l0:
inner_arr: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + arr_rb + 1)
inner_arr: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg + arr_rb + 1, src_sha1)
if inner_arr is not None: return mlirlite.Ptr(pool, inner_arr)
return None
# Canonical pointer form is `T*` (also rawptr[T]); rewrite trailing `*`
@@ -1978,7 +1998,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
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)
star_ty_ps: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, star_buf_ps, src_sha1)
stdlib.free(star_buf_ps)
return star_ty_ps
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
@@ -2030,7 +2050,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
if inner_mk is not None:
string.strncpy(inner_mk, seg + lb_off + 1, inner_len_mk)
inner_mk[inner_len_mk] = '\0'
inner_ty_mk: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_mk)
inner_ty_mk: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_mk, src_sha1)
stdlib.free(inner_mk)
stdlib.free(mk_buf)
return inner_ty_mk
@@ -2045,7 +2065,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
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)
inner_ty_rp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_rp, src_sha1)
stdlib.free(inner_buf_rp)
if inner_ty_rp is not None: return mlirlite.Ptr(pool, inner_ty_rp)
return None
@@ -2059,7 +2079,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
# Bare class / enum name (e.g. `-> Lexer*` written in the defining module's own vpi):
# mirror _resolve_vpi_annot_direct and consult the struct/enum registries, otherwise
# `X*` -> rawptr[X] fails on the inner name and the whole return type is lost.
bare_cls_ty: mlirlite.LLVMType* = _vpi_struct_or_enum(seg)
bare_cls_ty: mlirlite.LLVMType* = _vpi_struct_or_enum_in_module(seg, src_sha1)
if bare_cls_ty is not None: return bare_cls_ty
return None
# module-qualified class name "memhub.MemBuddy" -> use last segment to look up struct
@@ -2071,7 +2091,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
if mod_struct_ty is not None: return mod_struct_ty
return None
# bare class name -> global struct registry
bare_struct_ty: mlirlite.LLVMType* = _vpi_struct_or_enum(seg)
bare_struct_ty: mlirlite.LLVMType* = _vpi_struct_or_enum_in_module(seg, src_sha1)
if bare_struct_ty is not None: return bare_struct_ty
# bare typedef name (e.g. VOIDPTR): TypedefT dynamic table first, on miss fall back to map_builtin_type
mapped_bare: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, seg, None, None)
@@ -2081,7 +2101,7 @@ def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.L
# _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*:
def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, src_sha1: str = None) -> 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
# `T1 | T2` unions were retired; a stub containing one must be regenerated.
@@ -2112,7 +2132,7 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m
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)
seg_ty_t: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf_t, src_sha1)
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)
@@ -2136,7 +2156,7 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m
# A stub return annotation is a single type. `T*` / `T**` and `const` / `volatile`
# prefixes are handled by the shared single-segment resolver.
return _resolve_single_vpi_type_str(pool, ret_annot)
return _resolve_single_vpi_type_str(pool, ret_annot, src_sha1)
# VpiReader - .vpi AST parser. Reads function signatures and TypedefT definitions from
@@ -2563,7 +2583,195 @@ def _VpiParseIntRange(vbuf: bytes, s: sizet, e: sizet, out_v: int64*) -> int:
out_v[0] = v
return 1
# _VpiIsFloatLit - whether vbuf[a:b] is a float literal (optional sign, digits, '.', exponent).
def _VpiIsFloatLit(vbuf: bytes, a: sizet, b: sizet) -> int:
"""1 when the span is a plain float literal (1.5 / 1e3 / -2.5e-4), 0 otherwise."""
if vbuf is None or b <= a: return 0
i: sizet = a
if vbuf[i] == 0x2D or vbuf[i] == 0x2B: i += 1
digits: int = 0
dot: int = 0
has_frac: int = 0
while i < b:
chf: uint8 = vbuf[i]
if chf >= 0x30 and chf <= 0x39: digits += 1
elif chf == 0x2E and dot == 0:
dot = 1
has_frac = 1
else: break
i += 1
if i < b and (vbuf[i] == 0x65 or vbuf[i] == 0x45):
has_frac = 1
i += 1
if i < b and (vbuf[i] == 0x2D or vbuf[i] == 0x2B): i += 1
ed: int = 0
while i < b:
chfe: uint8 = vbuf[i]
if chfe >= 0x30 and chfe <= 0x39: ed += 1
else: return 0
i += 1
if ed == 0: return 0
if i != b: return 0
if digits == 0: return 0
# A plain digit run is an INTEGER default (handled by _VpiParseIntRange); only a dot or
# exponent makes it a float. Without this guard "0"/"12" would be stored as 0.0 f64 and an
# int parameter would receive an f64 operand (llvm.call type mismatch).
if has_frac == 0: return 0
return 1
# _VpiFillDefaultLit - 把第 di 个缺省值字面量写入快照数组;1=成功(可重建)
# ---- aggregate default literals (struct constructor defaults) ----
# Encoded token format (one token per struct field, '|' separated):
# i:<decimal> d:<float text> s:<len>:<text> b:<0|1> n:0
# The call site rebuilds the aggregate from the parameter's struct type, so the .vpi only
# needs the literal values (the constructor name is not used).
def _VpiEncPut(dst: bytes, cap: sizet, pos: sizet*, src: char*, n: sizet) -> int:
"""Append n bytes of src at dst[pos]; keep dst NUL-terminated. 0 = overflow/argument error."""
if dst is None or pos is None or src is None: return 0
if n == 0: return 1
if pos[0] + n + 1 > cap: return 0
string.memcpy(dst + pos[0], src, n)
pos[0] += n
dst[pos[0]] = '\0'
return 1
def _VpiEncInt(dst: bytes, cap: sizet, pos: sizet*, v: int64) -> int:
"""Append the decimal form of v."""
tmp: str = stdlib.malloc(32)
if tmp is None: return 0
viperlib.snprintf(tmp, 32, "%lld", v)
r: int = _VpiEncPut(dst, cap, pos, tmp, string.strlen(tmp))
stdlib.free(tmp)
return r
def _VpiEncodeAggArg(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet, dst: bytes, cap: sizet, pos: sizet*) -> int:
"""Encode one scalar constructor argument (already trimmed) into one token."""
a: sizet = s
b: sizet = e
while a < b and (vbuf[a] == 0x20 or vbuf[a] == 0x09): a += 1
while b > a and (vbuf[b - 1] == 0x20 or vbuf[b - 1] == 0x09): b -= 1
if b <= a: return 0
ch0: uint8 = vbuf[a]
if ch0 == 0x22 or ch0 == 0x27:
if b - a < 2 or vbuf[b - 1] != ch0: return 0
ci: sizet = a + 1
while ci < b - 1:
if vbuf[ci] == 0x5C: return 0 # escapes are not supported here
ci += 1
clen: sizet = b - a - 2
if _VpiEncPut(dst, cap, pos, "s:", 2) == 0: return 0
if _VpiEncInt(dst, cap, pos, int64(clen)) == 0: return 0
if _VpiEncPut(dst, cap, pos, ":", 1) == 0: return 0
return _VpiEncPut(dst, cap, pos, vbuf + a + 1, clen)
if b - a == 4 and string.strncmp(vbuf + a, "True", 4) == 0: return _VpiEncPut(dst, cap, pos, "b:1", 3)
if b - a == 5 and string.strncmp(vbuf + a, "False", 5) == 0: return _VpiEncPut(dst, cap, pos, "b:0", 3)
if b - a == 4 and string.strncmp(vbuf + a, "None", 4) == 0: return _VpiEncPut(dst, cap, pos, "n:0", 3)
if _VpiIsFloatLit(vbuf, a, b) != 0:
if _VpiEncPut(dst, cap, pos, "d:", 2) == 0: return 0
return _VpiEncPut(dst, cap, pos, vbuf + a, b - a)
iv: int64* = pool.alloc(8)
if iv is None: return 0
if _VpiParseIntRange(vbuf, a, b, iv) == 0: return 0
if _VpiEncPut(dst, cap, pos, "i:", 2) == 0: return 0
return _VpiEncInt(dst, cap, pos, iv[0])
def _VpiEncodeAggDefault(pool: memhub.MemBuddy*, vbuf: bytes, a0: sizet, b0: sizet, kinds: int*, ints: int64*, strs: sizet*, di: int) -> int:
"""Encode `Name(arg, ...)` as an aggregate literal snapshot; 0 = not a flat constructor."""
if pool is None or vbuf is None or kinds is None or ints is None or strs is None or di < 0: return 0
a: sizet = a0
b: sizet = b0
while a < b and (vbuf[a] == 0x20 or vbuf[a] == 0x09): a += 1
while b > a and (vbuf[b - 1] == 0x20 or vbuf[b - 1] == 0x09): b -= 1
if b - a < 3 or vbuf[b - 1] != 0x29: return 0 # ')'
# Constructor name: identifier characters plus '.' qualifier; then '('.
lp: sizet = a
has_name: int = 0
while lp < b:
ch: uint8 = vbuf[lp]
if ch == 0x28: break
ok_ch: int = 0
if (ch >= 0x41 and ch <= 0x5A) or (ch >= 0x61 and ch <= 0x7A) or ch == 0x5F: ok_ch = 1
elif ch >= 0x30 and ch <= 0x39: ok_ch = 1
elif ch == 0x2E: ok_ch = 1
elif ch == 0x20 or ch == 0x09: pass
else: return 0
if ok_ch != 0: has_name = 1
lp += 1
if lp >= b - 1 or has_name == 0: return 0
cap: sizet = 4096
dst: bytes = CompilerFactory.factory.PermPool.alloc(cap)
if dst is None: return 0
dst[0] = '\0'
pos: sizet = 0
ai: sizet = lp + 1
first: int = 1
while True:
while ai < b - 1 and (vbuf[ai] == 0x20 or vbuf[ai] == 0x09): ai += 1
if ai >= b - 1: break
j: sizet = ai
quote: uint8 = 0
while j < b - 1:
c: uint8 = vbuf[j]
if quote != 0:
if c == 0x5C: j += 1
elif c == quote: quote = 0
j += 1
continue
if c == 0x22 or c == 0x27: quote = c
elif c == 0x28 or c == 0x29: return 0 # nested calls are not supported
elif c == 0x2C: break
j += 1
if first == 0:
if _VpiEncPut(dst, cap, &pos, "|", 1) == 0: return 0
if _VpiEncodeAggArg(pool, vbuf, ai, j, dst, cap, &pos) == 0: return 0
first = 0
ai = j + 1
kinds[di] = GenTypes.CONST_AGG_DEFAULT
ints[di] = 0
strs[di] = sizet(dst)
return 1
def _VpiEncodeNameDefault(pool: memhub.MemBuddy*, vbuf: bytes, a0: sizet, b0: sizet, kinds: int*, ints: int64*, strs: sizet*, di: int) -> int:
"""Encode a bare/qualified identifier default (enum member or CDefine) as text.
The .vpi default pre-scan runs before enums are registered, so the value cannot be resolved
here. Keep the text in the snapshot; the call site resolves it after enum registration.
"""
if pool is None or vbuf is None or kinds is None or ints is None or strs is None or di < 0: return 0
a: sizet = a0
b: sizet = b0
while a < b and (vbuf[a] == 0x20 or vbuf[a] == 0x09): a += 1
while b > a and (vbuf[b - 1] == 0x20 or vbuf[b - 1] == 0x09): b -= 1
if b <= a or b - a > 200: return 0
i: sizet = a
has_id: int = 0
while i < b:
ch: uint8 = vbuf[i]
ok: int = 0
if (ch >= 0x41 and ch <= 0x5A) or (ch >= 0x61 and ch <= 0x7A) or ch == 0x5F: ok = 1
elif ch >= 0x30 and ch <= 0x39: ok = 1
elif ch == 0x2E: ok = 1
else: return 0
if ch != 0x2E: has_id = 1
i += 1
if has_id == 0: return 0
copy: str = CompilerFactory.factory.PermPool.alloc(b - a + 1)
if copy is None: return 0
string.strncpy(copy, vbuf + a, b - a)
copy[b - a] = '\0'
kinds[di] = GenTypes.CONST_EXPR_TEXT
ints[di] = 0
strs[di] = sizet(copy)
return 1
def _VpiFillDefaultLit(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet, kinds: int*, ints: int64*, strs: sizet*, di: int) -> int:
"""把第 di 个缺省值字面量写入快照;0=不可重建(字符串转义/复杂表达式)"""
if kinds is None or ints is None or strs is None or di < 0: return 0
@@ -2605,13 +2813,36 @@ def _VpiFillDefaultLit(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet,
ints[di] = 0
strs[di] = 0
return 1
# Float literal: store the IEEE-754 bit pattern (int64 slot); the snapshot rebuild
# reinterprets it back to double, then the call-site coercion narrows to the param type.
if _VpiIsFloatLit(vbuf, a, b) != 0:
flen: sizet = b - a
fbuf: bytes = stdlib.malloc(flen + 1)
if fbuf is None: return 0
string.strncpy(fbuf, vbuf + a, flen)
fbuf[flen] = '\0'
dv: double = string.atof(fbuf)
stdlib.free(fbuf)
uv: uint64 = 0
*&uv = *&dv
kinds[di] = ast.CONST_FLOAT
ints[di] = int64(uv)
strs[di] = 0
return 1
# Struct-constructor literal (e.g. Vec2(1.0, 2.0)): encode the scalar field values so a
# cross-module call site can rebuild the aggregate from its parameter's struct type.
if _VpiEncodeAggDefault(pool, vbuf, a, b, kinds, ints, strs, di) != 0:
return 1
iv: int64* = pool.alloc(8)
if iv is None: return 0
if _VpiParseIntRange(vbuf, a, b, iv) == 0: return 0
kinds[di] = ast.CONST_INT
ints[di] = iv[0]
strs[di] = 0
return 1
if _VpiParseIntRange(vbuf, a, b, iv) != 0:
kinds[di] = ast.CONST_INT
ints[di] = iv[0]
strs[di] = 0
return 1
# Identifier default (enum member `Flags.Name_` / CDefine): keep the text; the call site
# resolves it after enum registration (the pre-scan runs before enums exist).
return _VpiEncodeNameDefault(pool, vbuf, a, b, kinds, ints, strs, di)
# _VpiParseParamDefaults - 解析参数区间:返回 (参数个数 << 8) | 缺省值个数;-1=有不可重建缺省值
def _VpiParseParamDefaults(pool: memhub.MemBuddy*, vbuf: bytes, ps: sizet, pe: sizet, kinds: int*, ints: int64*, strs: sizet*) -> int:
@@ -2718,8 +2949,7 @@ def _ScanVpiDefaults(pool: memhub.MemBuddy*, vbuf: bytes, buf_len: sizet, sha1:
stdlib.free(strs)
return reg
# 单个 .vpi 的枚举成员上限(与 ENUM_MEMBER_MAX 同量级)
define VPI_ENUM_MEMBER_MAX = 128
# 单个 .vpi 的枚举成员上限:直接复用 GenTypes.ENUM_MEMBER_MAX(唯一定义点),不再维护本地副本。
# _vpi_slice_copy - 把 buf[a:b] 拷成 NUL 结尾的字符串(分配自 pool)
@@ -2746,9 +2976,9 @@ def _ScanVpiEnumsText(pool: memhub.MemBuddy*, vbuf: bytes, vlen: sizet, sha1: st
# 类型对象与枚举表都要活到进程结束:统一挂在 PermPool 上(EnumTable 本身就在 PermPool)
ty_pool: memhub.MemBuddy* = pool
if CompilerFactory.factory.PermPool is not None: ty_pool = CompilerFactory.factory.PermPool
nm_buf: sizet* = pool.alloc(8 * VPI_ENUM_MEMBER_MAX)
ty_buf: sizet* = pool.alloc(8 * VPI_ENUM_MEMBER_MAX)
val_buf: int64* = pool.alloc(8 * VPI_ENUM_MEMBER_MAX)
nm_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX)
ty_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX)
val_buf: int64* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX)
if nm_buf is None or ty_buf is None or val_buf is None: return 0
reg: int = 0
i: sizet = 0
@@ -2800,7 +3030,7 @@ def _ScanVpiEnumsText(pool: memhub.MemBuddy*, vbuf: bytes, vlen: sizet, sha1: st
ty_txt: str = _vpi_slice_copy(pool, vbuf, ts, ty_end)
# `State` 是自动编号成员的占位类型(源码端 t.State -> i32)
if string.strcmp(ty_txt, "State") != 0:
rty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(ty_pool, ty_txt)
rty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(ty_pool, ty_txt, sha1)
rb: int = _vpi_int_ty_bits(rty)
# 枚举存储必须可按字节寻址:bool 在定义端按 8 位算
if rb == 1: mty = mlirlite.Int8(ty_pool)
@@ -2821,7 +3051,7 @@ def _ScanVpiEnumsText(pool: memhub.MemBuddy*, vbuf: bytes, vlen: sizet, sha1: st
if neg != 0: pv = -pv
cur = pv
next_val = cur + 1
if cnt < VPI_ENUM_MEMBER_MAX:
if cnt < GenTypes.ENUM_MEMBER_MAX:
nm_buf[cnt] = sizet(mname)
ty_buf[cnt] = sizet(mty)
val_buf[cnt] = cur
@@ -3674,7 +3904,7 @@ def _inject_reexport_class_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMMod
# _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).
# The stub keeps @Attribute(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.<name> / t.attr.<name>(args) and compare."""
if node is None: return 0
@@ -4029,7 +4259,7 @@ def _inject_class_struct_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModul
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
# B5: vpi keeps @Attribute(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
@@ -4111,7 +4341,9 @@ def _AggregateParamTy(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mli
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
# Win64: iN for exactly 1/2/4/8 bytes, Ptr(struct) (byref copy) for every other size.
# Non-Windows keeps the struct value and lets the backend apply the platform ABI.
return GenApi.extern_c_param_agg_ty(pool, 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)
@@ -4244,7 +4476,7 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
# 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_annot_fd is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot_fd, i32_ty, i8_ptr_ty, src_sha1)
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
@@ -4272,6 +4504,12 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
# missing; an i32 default here would split "declare i32" from "define void".
if ret_ty is None: ret_ty = mlirlite.Void(pool)
# C extern returning an aggregate: remember the symbol so the call site emits the C ABI
# (Win64 integer register <=8B / hidden sret pointer otherwise) instead of Viper's own
# aggregate-return convention. ExportT alone stays on the Viper convention.
if (has_extern != 0 or has_state != 0) and ret_ty.kind() == mlirlite.LLVMType.Struct:
GenApi.register_extern_agg(mangled_name)
# 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)
@@ -4297,6 +4535,7 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
# convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed)
VLogger.repl_abort()
pi: sizet
decl_pi: int = 0
for pi in range(int(param_count)):
ag: ast.Arg* = ast.Arg*(args_list.get(pi))
if ag is None: continue
@@ -4313,6 +4552,9 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
# 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)
# Win64 byref: mark the call site so it passes a copy, not the variable address.
if agg_is_c_abi != 0 and param_ty is not None and param_ty.kind() == mlirlite.LLVMType.Ptr:
GenApi.register_extern_byref_param(mangled_name, decl_pi)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
@@ -4325,6 +4567,8 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
match resolved_ty:
case mlirlite.LLVMType.Struct(_, _, _):
param_ty = _AggregateParamTy(mod, pool, resolved_ty, agg_is_c_abi)
if agg_is_c_abi != 0 and param_ty is not None and param_ty.kind() == mlirlite.LLVMType.Ptr:
GenApi.register_extern_byref_param(mangled_name, decl_pi)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
@@ -4334,6 +4578,7 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
# 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)
decl_pi += 1
# 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)
@@ -4395,7 +4640,7 @@ def _inject_method_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, m_
# 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_annot is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty, src_sha1)
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)
+7 -22
View File
@@ -5,8 +5,7 @@ import memhub
import string
import stdlib
import mlirlite
import win32.win32file as win32file
import win32.win32base as win32base
import os
import io
import viperlib
import sys
@@ -629,29 +628,15 @@ define OBJ_CACHE_STAMP_TAG_CAP = 128
def _CompilerBuildTag(out: bytes, cap: sizet) -> int:
"""Serialize the running compiler's build id (exe size + last write time) into out."""
"""Serialize the running compiler's build id (exe size + mtime) into out."""
if out is None or cap < 32: return 0
out[0] = '\0'
EXE_CAP: sizet = sizet(1024)
exe_path: bytes = stdlib.malloc(EXE_CAP)
exe_path: str = sys.executable()
if exe_path is None: return 0
string.memset(exe_path, 0, EXE_CAP)
n: uint32 = win32file.GetModuleFileNameA(None, exe_path, uint32(1024))
ok: int = 0
if int(n) > 0 and int(n) < 1024:
fd_size: sizet = sizet(win32file.WIN32_FIND_DATAA.__sizeof__())
fd: win32file.WIN32_FIND_DATAA* = (win32file.WIN32_FIND_DATAA*)(stdlib.malloc(fd_size))
if fd is not None:
string.memset(fd, 0, fd_size)
fh: win32base.HANDLE = win32file.FindFirstFileA(exe_path, fd)
if fh != win32base.INVALID_HANDLE_VALUE:
win32file.FindClose(fh)
viperlib.snprintf(out, cap, "%u.%u-%u.%u", fd.nFileSizeLow, fd.nFileSizeHigh,
fd.ftLastWriteTime.dwLowDateTime, fd.ftLastWriteTime.dwHighDateTime)
ok = 1
stdlib.free(fd)
stdlib.free(exe_path)
return ok
st: os.stat_result* = os.stat(exe_path)
if st is None: return 0
viperlib.snprintf(out, cap, "%lld.%lld", st.st_size, st.st_mtime)
return 1
def _BuildObjCacheStamp(out: bytes, cap: sizet, shas: bytes, incdirs: bytes, count: int) -> sizet:
+128 -130
View File
@@ -1,130 +1,128 @@
from stdint import *
import memhub
import string
import stdlib
import mlirlite
import win32.win32file as win32file
import win32.win32base as win32base
import io
import viperlib
import sys
import ast
import lib.core.VLogger as VLogger
import lib.core.IncludesScanner as IncludesScanner
import lib.core.Gen.Translator as HandlesTranslator
import lib.Phase.BuildPipeline as BuildPipeline
import lib.Stub.StubGen.VpiReader as VpiReader
import lib.Projectrans.Utils as Utils
import lib.Projectrans.Config as Config
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo
import lib.Stub.StubMerger.StubMergerIR as StubMergerIR
import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild
import lib.Stub.StubMerger as StubMerger
# StubMergerCollect - slice-path construction and stub-file SHA1 collection.
# Functions live in this module; cross-module globals are accessed via StubMerger._xxx.
# _sliced_path - build a slice path (stdlib.malloc; caller frees; creates subdir when level>0).
# Shards by Config.Sha1SliceLevel into SHA1-prefix subdirs; returns the full path or None on failure.
def _sliced_path(temp_dir: str, td_len: sizet, sha1: str, ext: str) -> str:
"""Build a SHA1-sliced path (stdlib.malloc; caller frees; creates the subdir)."""
if temp_dir is None or sha1 is None or ext is None: return None
sha1_len: sizet = string.strlen(sha1)
ext_len: sizet = string.strlen(ext)
# Subdir prefix: reuse Config.slice_subdir (the authoritative SHA1 slice impl; mbuddy-allocated, no free)
subdir_buf: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
sub_len: sizet = 0
if subdir_buf is not None:
sub_len = string.strlen(subdir_buf)
# Ensure subdir {temp_dir}/{subdir} exists.
# Otherwise io.File(MODE.W) fails silently (file.closed==True) and the reader returns 0.
dir_path_len: sizet = td_len + sub_len + 2
dir_path: bytes = stdlib.malloc(dir_path_len)
if dir_path is not None:
viperlib.snprintf(dir_path, dir_path_len, "%s/%s", temp_dir, subdir_buf)
BuildPipeline.ensure_dir(dir_path)
stdlib.free(dir_path)
# Build the full path
path: str = None
path_len: sizet = 0
if sub_len > 0:
path_len = td_len + sub_len + sha1_len + ext_len + 4
path = stdlib.malloc(path_len)
if path is not None: viperlib.snprintf(path, path_len, "%s/%s/%s.%s", temp_dir, subdir_buf, sha1, ext)
else:
path_len = td_len + sha1_len + ext_len + 3
path = stdlib.malloc(path_len)
if path is not None: viperlib.snprintf(path, path_len, "%s/%s.%s", temp_dir, sha1, ext)
return path
# _is_in_sha1_set - check whether a SHA1 is in the set.
def _is_in_sha1_set(sha1: str, sha1_set: str, set_count: int) -> int:
"""Whether sha1 is present in the set."""
if sha1 is None or sha1_set is None: return 0
for i in range(int(set_count)):
if string.strcmp(sha1_set + i * 17, sha1) == 0: return 1
return 0
# Sha1MapPath - _sha1_map.txt 的落盘路径:放在**构建目录**(temp 的上一级),
# 便于直接按 SHA1 反查源文件;temp 内的 sliced 产物(.ll/.vpi/.meta)位置不变。
# 返回 stdlib 缓冲区(调用方 free);temp_dir 无上一级(相对路径)时退回 temp 目录。
def Sha1MapPath(pool: memhub.MemBuddy*, temp_dir: str) -> str:
"""返回 {temp_dir 的上一级}/_sha1_map.txt"""
if temp_dir is None: return None
td: sizet = string.strlen(temp_dir)
end: sizet = td
while end > 0 and (temp_dir[end - 1] == '/' or temp_dir[end - 1] == '\\'): end -= 1
cut: sizet = end
while cut > 0 and temp_dir[cut - 1] != '/' and temp_dir[cut - 1] != '\\': cut -= 1
parent_len: sizet = 0
if cut > 0: parent_len = cut - 1
else: parent_len = end # 没有上一级(相对路径):退回 temp 目录本身
buf: bytes = stdlib.malloc(parent_len + 32)
if buf is None: return None
if parent_len > 0: string.strncpy(buf, temp_dir, parent_len)
buf[parent_len] = '\0'
viperlib.snprintf(buf + parent_len, 32, "/_sha1_map.txt")
return buf
# WriteIncludesSha1Map - write includes scan results to _sha1_map.txt (human-readable debug output only).
# Internally the in-memory store filled by PopulateSha1MapStore is used; returns 0 on success, non-zero on failure.
def WriteIncludesSha1Map(mb: memhub.MemBuddy*, temp_dir: str, scan_result: IncludesScanner.ScanResult*, filter_set: str, filter_count: int) -> int:
"""Write the includes scan result to _sha1_map.txt (human-readable debug output)."""
if temp_dir is None or scan_result is None: return 1
# Build the path {temp_dir 的上一级}/_sha1_map.txt
map_path: bytes = Sha1MapPath(mb, temp_dir)
if map_path is None: return 1
# Open the file for writing (CREATE_ALWAYS)
f: io.File* = io.File(map_path, io.MODE.W)
stdlib.free(map_path)
if f.closed: return 1
# Write each include entry: {sha1}:includes/{rel_path}\n
entry_size_w: sizet = IncludesScanner.FileEntry.__sizeof__()
line_buf: bytes = stdlib.malloc(512)
if line_buf is None:
f.close()
return 1
written_count: int = 0
for i in range(int(scan_result.Count)):
ea_w: uint64 = uint64(scan_result.Entries) + i * entry_size_w
ent_w: IncludesScanner.FileEntry* = (IncludesScanner.FileEntry*)(ea_w)
if ent_w is None or ent_w.Sha1 is None or ent_w.RelPath is None: continue
# When a filter set is provided, write only its entries
if filter_set is not None and filter_count > 0:
if _is_in_sha1_set(ent_w.Sha1, filter_set, filter_count) == 0: continue
viperlib.snprintf(line_buf, 512, "%s:includes/%s\n", ent_w.Sha1, ent_w.RelPath)
line_len_w: sizet = string.strlen(line_buf)
f.write(line_buf, line_len_w)
written_count += 1
f.close()
return 0
from stdint import *
import memhub
import string
import stdlib
import mlirlite
import io
import viperlib
import sys
import ast
import lib.core.VLogger as VLogger
import lib.core.IncludesScanner as IncludesScanner
import lib.core.Gen.Translator as HandlesTranslator
import lib.Phase.BuildPipeline as BuildPipeline
import lib.Stub.StubGen.VpiReader as VpiReader
import lib.Projectrans.Utils as Utils
import lib.Projectrans.Config as Config
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo
import lib.Stub.StubMerger.StubMergerIR as StubMergerIR
import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild
import lib.Stub.StubMerger as StubMerger
# StubMergerCollect - slice-path construction and stub-file SHA1 collection.
# Functions live in this module; cross-module globals are accessed via StubMerger._xxx.
# _sliced_path - build a slice path (stdlib.malloc; caller frees; creates subdir when level>0).
# Shards by Config.Sha1SliceLevel into SHA1-prefix subdirs; returns the full path or None on failure.
def _sliced_path(temp_dir: str, td_len: sizet, sha1: str, ext: str) -> str:
"""Build a SHA1-sliced path (stdlib.malloc; caller frees; creates the subdir)."""
if temp_dir is None or sha1 is None or ext is None: return None
sha1_len: sizet = string.strlen(sha1)
ext_len: sizet = string.strlen(ext)
# Subdir prefix: reuse Config.slice_subdir (the authoritative SHA1 slice impl; mbuddy-allocated, no free)
subdir_buf: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
sub_len: sizet = 0
if subdir_buf is not None:
sub_len = string.strlen(subdir_buf)
# Ensure subdir {temp_dir}/{subdir} exists.
# Otherwise io.File(MODE.W) fails silently (file.closed==True) and the reader returns 0.
dir_path_len: sizet = td_len + sub_len + 2
dir_path: bytes = stdlib.malloc(dir_path_len)
if dir_path is not None:
viperlib.snprintf(dir_path, dir_path_len, "%s/%s", temp_dir, subdir_buf)
BuildPipeline.ensure_dir(dir_path)
stdlib.free(dir_path)
# Build the full path
path: str = None
path_len: sizet = 0
if sub_len > 0:
path_len = td_len + sub_len + sha1_len + ext_len + 4
path = stdlib.malloc(path_len)
if path is not None: viperlib.snprintf(path, path_len, "%s/%s/%s.%s", temp_dir, subdir_buf, sha1, ext)
else:
path_len = td_len + sha1_len + ext_len + 3
path = stdlib.malloc(path_len)
if path is not None: viperlib.snprintf(path, path_len, "%s/%s.%s", temp_dir, sha1, ext)
return path
# _is_in_sha1_set - check whether a SHA1 is in the set.
def _is_in_sha1_set(sha1: str, sha1_set: str, set_count: int) -> int:
"""Whether sha1 is present in the set."""
if sha1 is None or sha1_set is None: return 0
for i in range(int(set_count)):
if string.strcmp(sha1_set + i * 17, sha1) == 0: return 1
return 0
# Sha1MapPath - _sha1_map.txt 的落盘路径:放在**构建目录**(temp 的上一级),
# 便于直接按 SHA1 反查源文件;temp 内的 sliced 产物(.ll/.vpi/.meta)位置不变。
# 返回 stdlib 缓冲区(调用方 free);temp_dir 无上一级(相对路径)时退回 temp 目录。
def Sha1MapPath(pool: memhub.MemBuddy*, temp_dir: str) -> str:
"""返回 {temp_dir 的上一级}/_sha1_map.txt"""
if temp_dir is None: return None
td: sizet = string.strlen(temp_dir)
end: sizet = td
while end > 0 and (temp_dir[end - 1] == '/' or temp_dir[end - 1] == '\\'): end -= 1
cut: sizet = end
while cut > 0 and temp_dir[cut - 1] != '/' and temp_dir[cut - 1] != '\\': cut -= 1
parent_len: sizet = 0
if cut > 0: parent_len = cut - 1
else: parent_len = end # 没有上一级(相对路径):退回 temp 目录本身
buf: bytes = stdlib.malloc(parent_len + 32)
if buf is None: return None
if parent_len > 0: string.strncpy(buf, temp_dir, parent_len)
buf[parent_len] = '\0'
viperlib.snprintf(buf + parent_len, 32, "/_sha1_map.txt")
return buf
# WriteIncludesSha1Map - write includes scan results to _sha1_map.txt (human-readable debug output only).
# Internally the in-memory store filled by PopulateSha1MapStore is used; returns 0 on success, non-zero on failure.
def WriteIncludesSha1Map(mb: memhub.MemBuddy*, temp_dir: str, scan_result: IncludesScanner.ScanResult*, filter_set: str, filter_count: int) -> int:
"""Write the includes scan result to _sha1_map.txt (human-readable debug output)."""
if temp_dir is None or scan_result is None: return 1
# Build the path {temp_dir 的上一级}/_sha1_map.txt
map_path: bytes = Sha1MapPath(mb, temp_dir)
if map_path is None: return 1
# Open the file for writing (CREATE_ALWAYS)
f: io.File* = io.File(map_path, io.MODE.W)
stdlib.free(map_path)
if f.closed: return 1
# Write each include entry: {sha1}:includes/{rel_path}\n
entry_size_w: sizet = IncludesScanner.FileEntry.__sizeof__()
line_buf: bytes = stdlib.malloc(512)
if line_buf is None:
f.close()
return 1
written_count: int = 0
for i in range(int(scan_result.Count)):
ea_w: uint64 = uint64(scan_result.Entries) + i * entry_size_w
ent_w: IncludesScanner.FileEntry* = (IncludesScanner.FileEntry*)(ea_w)
if ent_w is None or ent_w.Sha1 is None or ent_w.RelPath is None: continue
# When a filter set is provided, write only its entries
if filter_set is not None and filter_count > 0:
if _is_in_sha1_set(ent_w.Sha1, filter_set, filter_count) == 0: continue
viperlib.snprintf(line_buf, 512, "%s:includes/%s\n", ent_w.Sha1, ent_w.RelPath)
line_len_w: sizet = string.strlen(line_buf)
f.write(line_buf, line_len_w)
written_count += 1
f.close()
return 0
-2
View File
@@ -4,8 +4,6 @@ import memhub
import string
import stdlib
import mlirlite
import win32.win32file as win32file
import win32.win32base as win32base
import io
import viperlib
import sys
@@ -3,8 +3,6 @@ import memhub
import string
import stdlib
import mlirlite
import win32.win32file as win32file
import win32.win32base as win32base
import io
import viperlib
import sys
+1 -1
View File
@@ -1356,7 +1356,7 @@ novirtual class AssignGenMixin:
cl: ast.Call* = ast.Call*(rhs)
return self._pure_str_call_owned(trans, cl)
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = BinOp*(rhs)
bo: ast.BinOp* = ast.BinOp*(rhs)
if bo is not None and bo.op == ast.OpKind.Add and trans._in_pure != 0:
return self._pure_str_expr(trans, bo.left) | self._pure_str_expr(trans, bo.right)
return 0
+246 -10
View File
@@ -12,13 +12,14 @@ import stdio
import stdlib
import viperlib
import io
import win32.win32file as win32file
import os
import lib.core.VLogger as VLogger
import lib.Stub.StubMerger as StubMerger
import lib.core.CompilerFactory as CompilerFactory
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubGen.VpiReader as VpiReader
import lib.Projectrans.Config as Config
# CallGenBase - split 1/9 of CallGenMixin (bottom parent): module symbol
@@ -30,6 +31,9 @@ import lib.Stub.StubGen.VpiReader as VpiReader
define GLOBAL_FUNC_MAX = 8192
define GLOBAL_DEFAULTS_MAX = 4096
define _CEXPORT_MAX = 256
# C-extern by-value aggregate parameters passed by reference (Win64: sizes other than
# 1/2/4/8). One symbol slot carries a 64-bit mask of the parameter indices needing a copy.
define _EXTERN_BYREF_MAX = 1024
define ASM_MAX_OPERANDS = 16
define ASM_TEXT_BUF_SIZE = 2048
define ASM_CONST_BUF_SIZE = 512
@@ -56,6 +60,20 @@ novirtual class CallGenBase:
CexportFuncArr: bytes = None
CexportCount: int = 0
# --- C-extern aggregate-return registry (symbol names). A C function returning a struct
# follows the target C ABI (Win64: integer register <=8B, hidden sret pointer otherwise),
# which differs from Viper's own aggregate convention. Call emission consults this table.
ExternAggArr: bytes = None
ExternAggCount: int = 0
# --- C-extern by-value aggregate parameter registry (symbol -> 64-bit param mask).
# Win64 passes aggregates whose size is not 1/2/4/8 by reference to a caller-owned copy;
# Viper's default argument lowering passes the address of the variable, so the call site
# must know which parameters need that copy.
ExternByrefArr: bytes = None
ExternByrefMasks: uint64* = None
ExternByrefCount: int = 0
# --- static symbol registry (static / @t.StaticT -> defining module SHA1),
# meaning no cross-module use (C internal linkage); PermPool fixed slots.
StaticSha1Arr: bytes = None
@@ -855,6 +873,224 @@ novirtual class CallGenBase:
return self._is_cexport_func_from_vpi(sha1, func_name)
# ============================================================
# C-extern aggregate return ABI (Win64)
# ============================================================
def register_extern_agg(self, sym: char*) -> int:
"""Record that `sym` is a C extern returning an aggregate in the C ABI (idempotent)."""
if self.Gen is None or sym is None: return 0
if self.Gen.CallGenBase.ExternAggArr is None:
self.Gen.CallGenBase.ExternAggArr = CompilerFactory.factory.PermPool.alloc(_CEXPORT_MAX * 64)
if self.Gen.CallGenBase.ExternAggArr is None: return 0
string.memset(self.Gen.CallGenBase.ExternAggArr, 0, _CEXPORT_MAX * 64)
for i in range(int(self.Gen.CallGenBase.ExternAggCount)):
idx: sizet = sizet(i) * 64
if string.strcmp(self.Gen.CallGenBase.ExternAggArr + idx, sym) == 0: return 1
if self.Gen.CallGenBase.ExternAggCount < _CEXPORT_MAX:
idx2: sizet = sizet(self.Gen.CallGenBase.ExternAggCount) * 64
string.strncpy(self.Gen.CallGenBase.ExternAggArr + idx2, sym, 63)
self.Gen.CallGenBase.ExternAggArr[idx2 + 63] = '\0'
self.Gen.CallGenBase.ExternAggCount += 1
return 1
return 0
def is_extern_agg(self, sym: char*) -> int:
"""1 when `sym` is a C extern returning an aggregate (C ABI at the call boundary)."""
if self.Gen is None or sym is None: return 0
if self.Gen.CallGenBase.ExternAggArr is None: return 0
for i in range(int(self.Gen.CallGenBase.ExternAggCount)):
idx: sizet = sizet(i) * 64
if string.strcmp(self.Gen.CallGenBase.ExternAggArr + idx, sym) == 0: return 1
return 0
def register_extern_byref_param(self, sym: char*, index: int) -> int:
"""Mark param `index` of C extern `sym` as a by-value aggregate passed by reference."""
if self.Gen is None or sym is None or index < 0 or index >= 64: return 0
if self.Gen.CallGenBase.ExternByrefArr is None:
self.Gen.CallGenBase.ExternByrefArr = CompilerFactory.factory.PermPool.alloc(_EXTERN_BYREF_MAX * 64)
self.Gen.CallGenBase.ExternByrefMasks = CompilerFactory.factory.PermPool.alloc(_EXTERN_BYREF_MAX * 8)
if self.Gen.CallGenBase.ExternByrefArr is None or self.Gen.CallGenBase.ExternByrefMasks is None: return 0
string.memset(self.Gen.CallGenBase.ExternByrefArr, 0, _EXTERN_BYREF_MAX * 64)
string.memset(self.Gen.CallGenBase.ExternByrefMasks, 0, _EXTERN_BYREF_MAX * 8)
for i in range(int(self.Gen.CallGenBase.ExternByrefCount)):
idx: sizet = sizet(i) * 64
if string.strcmp(self.Gen.CallGenBase.ExternByrefArr + idx, sym) == 0:
self.Gen.CallGenBase.ExternByrefMasks[i] = self.Gen.CallGenBase.ExternByrefMasks[i] | (uint64(1) << uint64(index))
return 1
if self.Gen.CallGenBase.ExternByrefCount < _EXTERN_BYREF_MAX:
idx2: sizet = sizet(self.Gen.CallGenBase.ExternByrefCount) * 64
string.strncpy(self.Gen.CallGenBase.ExternByrefArr + idx2, sym, 63)
self.Gen.CallGenBase.ExternByrefArr[idx2 + 63] = '\0'
self.Gen.CallGenBase.ExternByrefMasks[self.Gen.CallGenBase.ExternByrefCount] = uint64(1) << uint64(index)
self.Gen.CallGenBase.ExternByrefCount += 1
return 1
return 0
def is_extern_byref_param(self, sym: char*, index: int) -> int:
"""1 when param `index` of C extern `sym` is a by-value aggregate passed by reference."""
if self.Gen is None or sym is None or index < 0 or index >= 64: return 0
if self.Gen.CallGenBase.ExternByrefArr is None: return 0
for i in range(int(self.Gen.CallGenBase.ExternByrefCount)):
idx: sizet = sizet(i) * 64
if string.strcmp(self.Gen.CallGenBase.ExternByrefArr + idx, sym) == 0:
if (self.Gen.CallGenBase.ExternByrefMasks[i] & (uint64(1) << uint64(index))) != 0: return 1
return 0
return 0
def _copy_byref_agg_arg(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, val: mlirlite.Value*, pty: mlirlite.LLVMType*) -> mlirlite.Value*:
"""Pass a copy of a by-value aggregate argument that the C ABI hands over by reference."""
if pool is None or builder is None or val is None or pty is None or val.Ty is None: return val
if pty.kind() != mlirlite.LLVMType.Ptr: return val
match val.Ty:
case mlirlite.LLVMType.Ptr(elem):
if elem is None or elem.kind() != mlirlite.LLVMType.Struct: return val
loaded: mlirlite.Value* = mlirlite.build_load(builder, elem, val)
if loaded is None: return val
tmp: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, elem)
if tmp is None: return val
mlirlite.build_store(builder, loaded, tmp)
if self._call_ty_equal(pool, pty, tmp.Ty) == 0:
bc: mlirlite.Value* = mlirlite.build_bitcast(builder, tmp, pty)
if bc is not None: return bc
return tmp
case _:
return val
def _c_agg_is_packed(self, ty: mlirlite.LLVMType*) -> int:
"""1 when the registered struct type is packed (no inter-field padding)."""
if ty is None or self.Gen is None: return 0
se: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(ty)
if se is not None and se.IsPacked != 0: return 1
return 0
def _c_agg_size(self, ty: mlirlite.LLVMType*) -> int:
"""Byte size of a C aggregate (natural alignment, or exact layout when packed)."""
if ty is None: return 0
match ty:
case mlirlite.LLVMType.Int(bits):
return (bits + 7) // 8
case mlirlite.LLVMType.Float(bits):
return bits // 8
case mlirlite.LLVMType.Ptr(_):
return 8
case mlirlite.LLVMType.Array(elem, count):
return self._c_agg_size(elem) * count
case mlirlite.LLVMType.Struct(fields, fcount, _):
if fcount < 0: return 0
packed: int = self._c_agg_is_packed(ty)
total: int = 0
max_align: int = 1
cur: mlirlite.ParamNode* = fields
i: int = 0
while cur is not None and i < fcount:
fty: mlirlite.LLVMType* = cur.Ty
fsz: int = self._c_agg_size(fty)
fal: int = 1 if packed != 0 else self._c_agg_align(fty)
if fal < 1: fal = 1
if fal > max_align: max_align = fal
total = (total + fal - 1) // fal * fal
total += fsz
cur = cur.Next
i += 1
if packed == 0 and max_align > 1: total = (total + max_align - 1) // max_align * max_align
return total
case _:
return 0
def _c_agg_align(self, ty: mlirlite.LLVMType*) -> int:
"""Natural alignment of a C scalar/aggregate type."""
if ty is None: return 1
match ty:
case mlirlite.LLVMType.Int(bits):
sz_a: int = (bits + 7) // 8
if sz_a < 1: return 1
if sz_a > 8: return 8
return sz_a
case mlirlite.LLVMType.Float(bits):
if bits <= 0: return 1
return bits // 8
case mlirlite.LLVMType.Ptr(_):
return 8
case mlirlite.LLVMType.Array(elem_a, _):
return self._c_agg_align(elem_a)
case mlirlite.LLVMType.Struct(fields_a, fcount_a, _):
if self._c_agg_is_packed(ty) != 0: return 1
max_a: int = 1
cur_a: mlirlite.ParamNode* = fields_a
i_a: int = 0
while cur_a is not None and i_a < fcount_a:
fa: int = self._c_agg_align(cur_a.Ty)
if fa > max_a: max_a = fa
cur_a = cur_a.Next
i_a += 1
return max_a
case _:
return 1
def is_win64_c_abi(self) -> int:
"""1 when C ABI lowering follows Win64 (i.e. the target is not Linux/SysV)."""
if Config.TargetTriple is not None:
if string.strstr(Config.TargetTriple, "linux") is not None: return 0
return 1
def c_agg_int_ty(self, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""Win64: a 1/2/4/8-byte aggregate rides in one integer register; None = by reference."""
if ty is None: return None
sz: int = self._c_agg_size(ty)
if sz == 1: return mlirlite.Int8(pool)
if sz == 2: return mlirlite.Int16(pool)
if sz == 4: return mlirlite.Int32(pool)
if sz == 8: return mlirlite.Int64(pool)
return None
def extern_c_param_agg_ty(self, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""LLVM type of a C extern's by-value aggregate parameter.
Win64 passes exactly 1/2/4/8 bytes as an integer of that width (a struct's field types
are irrelevant) and every other size by reference, with the caller owning the temporary
copy. Emitting the struct value instead makes LLVM split the fields (floats to XMM,
larger aggregates to stack slots), which no C callee expects. Non-Windows keeps the
struct value and lets the backend apply the platform ABI.
"""
if ty is None: return ty
if self.is_win64_c_abi() == 0: return ty
it: mlirlite.LLVMType* = self.c_agg_int_ty(pool, ty)
if it is not None: return it
return mlirlite.Ptr(pool, ty)
def emit_extern_agg_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, call_name: char*, head: mlirlite.Value*, arg_count: int, ret_ty: mlirlite.LLVMType*) -> mlirlite.Value*:
"""Call a C extern whose aggregate return uses the Win64 C ABI; return the Viper struct value."""
if pool is None or builder is None or call_name is None or ret_ty is None: return None
# Only the Windows x64 ABI is adapted here. On Linux/SysV keep the existing direct-call
# path (its eightbyte classification is a separate implementation).
if Config.TargetTriple is not None:
if string.strstr(Config.TargetTriple, "linux") is not None: return None
size_b: int = self._c_agg_size(ret_ty)
if size_b <= 0: return None
fp: mlirlite.Value* = mlirlite.build_addressof(builder, call_name)
if fp is None: return None
abi_int: mlirlite.LLVMType* = None
if size_b == 1: abi_int = mlirlite.Int8(pool)
elif size_b == 2: abi_int = mlirlite.Int16(pool)
elif size_b == 4: abi_int = mlirlite.Int32(pool)
elif size_b == 8: abi_int = mlirlite.Int64(pool)
if abi_int is not None:
# Exact 1/2/4/8 bytes: C returns the aggregate in RAX as an integer of that width.
raw: mlirlite.Value* = mlirlite.build_call_indirect(builder, fp, head, arg_count, abi_int)
if raw is None: return None
slot: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, abi_int)
if slot is None: return None
mlirlite.build_store(builder, raw, slot)
sp: mlirlite.Value* = mlirlite.build_bitcast(builder, slot, mlirlite.Ptr(pool, ret_ty))
if sp is None: return None
return mlirlite.build_load(builder, ret_ty, sp)
# Any other size: C returns through a hidden sret pointer passed as the first argument.
tmp: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, ret_ty)
if tmp is None: return None
tmp.Next = head
mlirlite.build_call_indirect(builder, fp, tmp, arg_count + 1, mlirlite.Void(pool))
return mlirlite.build_load(builder, ret_ty, tmp)
# RegisterStaticSymbol / IsStaticSymbol - static symbol registry: the four
# syntaxes all normalize to t.StaticT; meaning = no cross-module use.
def RegisterStaticSymbol(self, sha1: char*, name: char*) -> int:
@@ -1223,6 +1459,7 @@ novirtual class CallGenBase:
new_tail: mlirlite.Value* = None
cur: mlirlite.Value* = head
prm: mlirlite.Param* = mlirlite.function_get_param_head(func)
pi_c: int = 0
while cur is not None and prm is not None:
nxt: mlirlite.Value* = cur.Next
node_v: mlirlite.Value* = cur
@@ -1230,12 +1467,18 @@ novirtual class CallGenBase:
if pty is not None and cur.Ty is not None and self._call_ty_equal(pool, pty, cur.Ty) == 0:
conv: mlirlite.Value* = self.Gen.CallGenLib._coerce_arg_value_to_ty(pool, builder, cur, pty, node, trans, 0)
if conv is not None: node_v = conv
# Win64 byref aggregate parameter: C takes a private copy, so never pass the
# argument variable's own address (a callee write-back must not reach the caller).
if self.is_extern_byref_param(call_name, pi_c) != 0:
cp_v: mlirlite.Value* = self._copy_byref_agg_arg(pool, builder, node_v, pty)
if cp_v is not None: node_v = cp_v
mlirlite.value_set_next(node_v, None)
if new_head is None: new_head = node_v
else: mlirlite.value_set_next(new_tail, node_v)
new_tail = node_v
cur = nxt
prm = mlirlite.param_get_next(prm)
pi_c += 1
while cur is not None:
nxt2: mlirlite.Value* = cur.Next
mlirlite.value_set_next(cur, None)
@@ -1737,7 +1980,7 @@ novirtual class CallGenBase:
i32_p: mlirlite.LLVMType* = mlirlite.Int32(pb_d)
i8_p: mlirlite.LLVMType* = mlirlite.Int8(pb_d)
i8p_p: mlirlite.LLVMType* = mlirlite.Ptr(pb_d, i8_p)
rv_p: mlirlite.LLVMType* = VpiReader._resolve_vpi_ret_annot_str(pb_d, annot_d, i32_p, i8p_p)
rv_p: mlirlite.LLVMType* = VpiReader._resolve_vpi_ret_annot_str(pb_d, annot_d, i32_p, i8p_p, probe_sha)
if rv_p is None: c_annot_res = "none"
else: c_annot_res = "ok"
# Annotate the tail class of the raw annotation: "missing" = the class was never
@@ -2328,14 +2571,7 @@ novirtual class CallGenBase:
subdir_buf[2] = '\0'
viperlib.snprintf(path_buf, path_len, "%s/%s/%s.text.ll", self.Gen.CallGenBase.TempDir, subdir_buf, sha1)
stdlib.free(subdir_buf)
# existence check via GetFileAttributesA (no file handle opened)
attr: ULONG = win32file.GetFileAttributesA(path_buf)
exists: int = 1
# INVALID_FILE_ATTRIBUTES = missing/inaccessible -> 0.
if attr == 0xFFFFFFFF: exists = 0
# The original CreateFileA rejected directories; exclude them explicitly to
# preserve the equivalent "readable regular file" semantics.
if (attr & win32file.FILE_ATTRIBUTE_DIRECTORY) != 0: exists = 0
exists: int = 1 if os.isfile(path_buf) else 0
stdlib.free(path_buf)
return exists
+1 -1
View File
@@ -1870,7 +1870,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
match entry.Ty:
case mlirlite.LLVMType.Struct(fields, fcount, name):
offset_fb: int = 0
# packed (@c.Attribute(t.attr.packed)) structs have no field padding:
# packed (@Attribute(attr.packed)) structs have no field padding:
# offset = sum of preceding field sizes (LLVM packed struct layout).
is_packed_fb: int = entry.IsPacked
cur_fb: mlirlite.ParamNode* = fields
+18 -5
View File
@@ -678,11 +678,24 @@ novirtual class CallGenCtor(CallGenBase.CallGenBase):
self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: storage allocation for the scalar type '{nm}' failed", "TypeError")
return None
# 零初始化:`new … T()` 走 __before_init__ 是零填充,标量/数组同语义(未写先读不再是垃圾)
zero_v: mlirlite.Value* = mlirlite.ConstZero(pool, prim_ty)
if zero_v is None:
self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot build a zero initializer for the scalar type '{nm}'", "TypeError")
return None
mlirlite.build_store(builder, zero_v, storage)
# 数组走 llvm.memset,不生成"整数组常量存储"IR(`store [N x i8] zeroinitializer`)。
# N 较大时后者在 LLVM 22 llc/Win32 上会崩溃;语法/语义上它仍是数组,实际存储只是
# malloc/memgr 给的字节块,不需要按数组聚合类型整体写一次。
if elem_ty is not None:
zero_byte: mlirlite.Value* = mlirlite.ConstInt(pool, mlirlite.Int8(pool), 0, "0")
zero_len: mlirlite.Value* = mlirlite.const_int64(pool, size_bytes)
if zero_byte is None or zero_len is None:
self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot build a zero initializer for the array type '{nm}'", "TypeError")
return None
if self.Gen.CallGenLib._emit_llvm_memset_intrinsic(pool, builder, mod, storage, zero_byte, zero_len) is None:
self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot zero-initialize the array type '{nm}'", "TypeError")
return None
else:
zero_v: mlirlite.Value* = mlirlite.ConstZero(pool, prim_ty)
if zero_v is None:
self.Gen.TypeGenErr.fatal_error(new_node, f"`new`: cannot build a zero initializer for the scalar type '{nm}'", "TypeError")
return None
mlirlite.build_store(builder, zero_v, storage)
# 返回值:标量 → 该指针本身;数组 → **首元素指针 Elem***(数组名退化,同址零成本 bitcast)
ptr_ty: mlirlite.LLVMType* = None
if elem_ty is not None: ptr_ty = mlirlite.Ptr(pool, elem_ty)
+327 -4
View File
@@ -1426,6 +1426,122 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
return 1
# ---- aggregate default encoding (struct constructor with scalar arguments) ----
# Token format shared with VpiReader._VpiEncodeAggDefault:
# i:<decimal> d:<float> s:<len>:<text> b:<0|1> n:0 (one token per field, '|' split)
def _agg_enc_put(self, dst: bytes, cap: sizet, pos: sizet*, src: char*, n: sizet) -> int:
if dst is None or pos is None or src is None: return 0
if n == 0: return 1
if pos[0] + n + 1 > cap: return 0
string.memcpy(dst + pos[0], src, n)
pos[0] += n
dst[pos[0]] = '\0'
return 1
def _agg_enc_int(self, dst: bytes, cap: sizet, pos: sizet*, v: int64) -> int:
tmp: str = stdlib.malloc(32)
if tmp is None: return 0
viperlib.snprintf(tmp, 32, "%lld", v)
r: int = self._agg_enc_put(dst, cap, pos, tmp, string.strlen(tmp))
stdlib.free(tmp)
return r
def _agg_enc_float(self, dst: bytes, cap: sizet, pos: sizet*, v: double) -> int:
tmp: str = stdlib.malloc(64)
if tmp is None: return 0
viperlib.snprintf(tmp, 64, "%.17g", v)
r: int = self._agg_enc_put(dst, cap, pos, "d:", 2)
if r != 0: r = self._agg_enc_put(dst, cap, pos, tmp, string.strlen(tmp))
stdlib.free(tmp)
return r
def _agg_enc_arg(self, dst: bytes, cap: sizet, pos: sizet*, node: ast.AST*) -> int:
"""Encode one scalar constructor argument; 0 = unsupported."""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn.const_kind == ast.CONST_INT or cn.const_kind == ast.CONST_CHAR:
if self._agg_enc_put(dst, cap, pos, "i:", 2) == 0: return 0
return self._agg_enc_int(dst, cap, pos, cn.int_val)
if cn.const_kind == ast.CONST_FLOAT: return self._agg_enc_float(dst, cap, pos, cn.float_val)
if cn.const_kind == ast.CONST_BOOL:
if cn.int_val != 0: return self._agg_enc_put(dst, cap, pos, "b:1", 3)
return self._agg_enc_put(dst, cap, pos, "b:0", 3)
if cn.const_kind == ast.CONST_NONE: return self._agg_enc_put(dst, cap, pos, "n:0", 3)
if cn.const_kind == ast.CONST_STR and cn.str_val is not None:
sl: sizet = string.strlen(cn.str_val)
if self._agg_enc_put(dst, cap, pos, "s:", 2) == 0: return 0
if self._agg_enc_int(dst, cap, pos, int64(sl)) == 0: return 0
if self._agg_enc_put(dst, cap, pos, ":", 1) == 0: return 0
return self._agg_enc_put(dst, cap, pos, cn.str_val, sl)
return 0
if k == ast.ASTKind.UnaryOp:
du: ast.UnaryOp* = ast.UnaryOp*(node)
if du.operand is None or du.operand.kind() != ast.ASTKind.Constant: return 0
dc: ast.Constant* = ast.Constant*(du.operand)
if dc.const_kind == ast.CONST_INT or dc.const_kind == ast.CONST_CHAR:
v: int64 = dc.int_val
if du.op == ast.OpKind.USub: v = -v
elif du.op == ast.OpKind.Invert: v = ~v
if self._agg_enc_put(dst, cap, pos, "i:", 2) == 0: return 0
return self._agg_enc_int(dst, cap, pos, v)
if dc.const_kind == ast.CONST_FLOAT:
fv: double = dc.float_val
if du.op == ast.OpKind.USub: fv = -fv
return self._agg_enc_float(dst, cap, pos, fv)
return 0
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is None: return 0
cv: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(nm.id)
if self.Gen.TypeGenCDef.is_cdefine_found() == 0: return 0
if self._agg_enc_put(dst, cap, pos, "i:", 2) == 0: return 0
return self._agg_enc_int(dst, cap, pos, cv)
return 0
def _agg_expr_text_at(self, node: ast.AST*, dst: bytes, cap: sizet, pos: sizet*) -> int:
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is None: return 0
return self._agg_enc_put(dst, cap, pos, nm.id, string.strlen(nm.id))
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.value is None: return 0
if self._agg_expr_text_at(at.value, dst, cap, pos) == 0: return 0
if self._agg_enc_put(dst, cap, pos, ".", 1) == 0: return 0
if at.attr is None: return 0
return self._agg_enc_put(dst, cap, pos, at.attr, string.strlen(at.attr))
return 0
def _agg_expr_text(self, node: ast.AST*, dst: bytes, cap: sizet) -> int:
"""Render a Name/Attribute default (enum member / CDefine) into dst; 0 = unsupported."""
if dst is None or cap == 0: return 0
dst[0] = '\0'
pos: sizet = 0
return self._agg_expr_text_at(node, dst, cap, &pos)
def _encode_agg_default(self, cl: ast.Call*, dst: bytes, cap: sizet) -> int:
"""Encode a struct-constructor default's scalar arguments; 0 = unsupported."""
if cl is None or dst is None or cl.func is None: return 0
if cl.keywords is not None and len(cl.keywords) > 0: return 0
if cl.func.kind() != ast.ASTKind.Name: return 0
nm: ast.Name* = ast.Name*(cl.func)
if nm.id is None: return 0
if GenApi.find_struct(nm.id) is None: return 0
if cl.args is None: return 0
pos: sizet = 0
dst[0] = '\0'
al: list[ast.AST*]* = cl.args
an: sizet = len(al)
for ai in range(int(an)):
if ai > 0:
if self._agg_enc_put(dst, cap, &pos, "|", 1) == 0: return 0
if self._agg_enc_arg(dst, cap, &pos, al.get(ai)) == 0: return 0
return 1
def _register_global_defaults(self, name: char*, defaults: rawptr, default_count: int, param_count: int, def_sha1: char* = None) -> int:
"""Register a function's default arguments in the global table; returns 0 on success"""
if name is None or defaults is None: return 1
@@ -1477,8 +1593,18 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
dnode: ast.AST* = dl.get(di)
if dnode is not None and dnode.kind() == ast.ASTKind.Constant:
dcn: ast.Constant* = ast.Constant*(dnode)
kinds[di] = dcn.const_kind
ints[di] = dcn.int_val
if dcn.const_kind == ast.CONST_FLOAT:
# Float defaults live in float_val; int_val is always 0. Store the
# IEEE-754 bit pattern so the snapshot rebuild can recover the value
# (int defaults still use int_val).
fv_reg: double = dcn.float_val
fb_reg: uint64 = 0
*&fb_reg = *&fv_reg
kinds[di] = ast.CONST_FLOAT
ints[di] = int64(fb_reg)
else:
kinds[di] = dcn.const_kind
ints[di] = dcn.int_val
if dcn.const_kind == ast.CONST_STR and dcn.str_val is not None:
slen: sizet = string.strlen(dcn.str_val)
scopy: char* = CompilerFactory.factory.PermPool.alloc(slen + 1)
@@ -1494,6 +1620,16 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
if self.Gen.TypeGenCDef.is_cdefine_found() != 0:
kinds[di] = ast.CONST_INT
ints[di] = cdef_v
else:
# Not a CDefine: keep the name text and let the call site resolve
# it (e.g. an enum member imported unqualified).
txt_n: bytes = CompilerFactory.factory.PermPool.alloc(256)
if txt_n is not None and self._agg_expr_text(dnode, txt_n, 256) != 0:
kinds[di] = GenTypes.CONST_EXPR_TEXT
ints[di] = 0
strs[di] = sizet(txt_n)
else:
kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT
elif dnode is not None and dnode.kind() == ast.ASTKind.UnaryOp:
# unary-literal default (e.g. ex3: int = -1): AST is UnaryOp over a
# Constant; evaluate only when the operand is an int Constant.
@@ -1510,6 +1646,44 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
elif du.op == ast.OpKind.Invert:
kinds[di] = ast.CONST_INT
ints[di] = ~doc.int_val
elif doc.const_kind == ast.CONST_FLOAT:
# Negative/positive float literal (e.g. -1.5): store bits.
fv_u: double = doc.float_val
if du.op == ast.OpKind.USub: fv_u = -fv_u
elif du.op == ast.OpKind.UAdd: pass
else: fv_u = 0.0
fb_u: uint64 = 0
*&fb_u = *&fv_u
kinds[di] = ast.CONST_FLOAT
ints[di] = int64(fb_u)
elif dnode is not None and dnode.kind() == ast.ASTKind.Attribute:
# Qualified constant (enum member `Flags.Name_`, module constant):
# store the text; the call site resolves it after enum registration.
txt_a: bytes = CompilerFactory.factory.PermPool.alloc(256)
if txt_a is not None and self._agg_expr_text(dnode, txt_a, 256) != 0:
kinds[di] = GenTypes.CONST_EXPR_TEXT
ints[di] = 0
strs[di] = sizet(txt_a)
else:
kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT
elif dnode is not None and dnode.kind() == ast.ASTKind.Call:
# Struct-constructor default (e.g. Vec2(1.0, 2.0)): store the literal
# field tokens; cross-module callers rebuild it from the parameter type.
enc_buf: bytes = CompilerFactory.factory.PermPool.alloc(4096)
if enc_buf is not None and self._encode_agg_default(ast.Call*(dnode), enc_buf, 4096) != 0:
kinds[di] = GenTypes.CONST_AGG_DEFAULT
ints[di] = 0
strs[di] = sizet(enc_buf)
else:
kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT
elif dnode is not None:
txt_x: bytes = CompilerFactory.factory.PermPool.alloc(256)
if txt_x is not None and self._agg_expr_text(dnode, txt_x, 256) != 0:
kinds[di] = GenTypes.CONST_EXPR_TEXT
ints[di] = 0
strs[di] = sizet(txt_x)
else:
kinds[di] = GenTypes.CONST_UNKNOWN_DEFAULT
entry.ConstKinds = kinds
entry.ConstInts = ints
entry.ConstStrs = strs
@@ -1583,7 +1757,119 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
return None
def _build_const_from_snapshot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, gd_entry: GenTypes.FuncDefaultsEntry*, di: int, trans: HT.Translator*, builder: mlirlite.IRBuilder*) -> mlirlite.Value*:
def _agg_parse_i64(self, vbuf: bytes, s: sizet, e: sizet) -> int64:
neg: int = 0
i: sizet = s
if i < e and vbuf[i] == '-': neg = 1; i += 1
v: int64 = 0
while i < e:
ch: uint8 = vbuf[i]
if ch < 0x30 or ch > 0x39: break
v = v * 10 + int64(ch - 0x30)
i += 1
if neg != 0: v = -v
return v
def _agg_parse_f64(self, vbuf: bytes, s: sizet, e: sizet) -> double:
n: sizet = e - s
tb: bytes = stdlib.malloc(n + 1)
if tb is None: return 0.0
string.memcpy(tb, vbuf + s, n)
tb[n] = '\0'
dv: double = string.atof(tb)
stdlib.free(tb)
return dv
def _agg_token_value(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, tag: uint8, vbuf: bytes, vs: sizet, ve: sizet, fty: mlirlite.LLVMType*) -> mlirlite.Value*:
"""Convert one encoded default token to a constant of the struct field's LLVM type."""
if fty is None: return None
match fty:
case mlirlite.LLVMType.Int(bits):
if tag == 0x69: return mlirlite.const_int(pool, bits, self._agg_parse_i64(vbuf, vs, ve)) # 'i'
if tag == 0x62: # 'b'
bv: int64 = 1 if (ve > vs and vbuf[vs] == '1') else 0
return mlirlite.const_int(pool, bits, bv)
if tag == 0x6E: return mlirlite.const_int(pool, bits, 0) # 'n'
return None
case mlirlite.LLVMType.Float(bits):
if tag == 0x64: return mlirlite.ConstFloat(pool, fty, self._agg_parse_f64(vbuf, vs, ve)) # 'd'
if tag == 0x69: return mlirlite.ConstFloat(pool, fty, double(self._agg_parse_i64(vbuf, vs, ve)))
return None
case mlirlite.LLVMType.Ptr(_):
if tag == 0x6E: return mlirlite.ConstNull(pool, fty, "null")
if tag == 0x73: # 's'
n: sizet = ve - vs
sv: bytes = CompilerFactory.factory.PermPool.alloc(n + 1)
if sv is None: return None
string.memcpy(sv, vbuf + vs, n)
sv[n] = '\0'
gs: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, mod, sv, trans)
if gs is None: return None
if self._call_ty_equal(pool, gs.Ty, fty) == 0:
bc: mlirlite.Value* = mlirlite.build_bitcast(builder, gs, fty)
if bc is not None: return bc
return gs
return None
case _:
return None
def _build_agg_from_tokens(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, spec: char*, param_ty: mlirlite.LLVMType*) -> mlirlite.Value*:
"""Rebuild an aggregate default from its field tokens and the parameter's struct type."""
if spec is None or param_ty is None: return None
sty: mlirlite.LLVMType* = param_ty
match param_ty:
case mlirlite.LLVMType.Ptr(pe): sty = pe
case _: sty = param_ty
if sty is None or sty.kind() != mlirlite.LLVMType.Struct: return None
fields: mlirlite.ParamNode* = None
fcount: int = 0
match sty:
case mlirlite.LLVMType.Struct(fs, fc, _):
fields = fs
fcount = fc
case _:
return None
if fcount <= 0: return None
slot: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, sty)
if slot is None: return None
sp: char* = spec
cur: mlirlite.ParamNode* = fields
i: int = 0
while cur is not None and i < fcount:
fty: mlirlite.LLVMType* = cur.Ty
tag: uint8 = uint8(sp[0])
if sp[1] != ':': return None
sp = sp + 2
token_base: char* = sp
tl: sizet = 0
if tag == 0x73: # 's': <len>:<text>
while sp[0] >= '0' and sp[0] <= '9':
tl = tl * 10 + sizet(int(sp[0]) - 48)
sp += 1
if sp[0] != ':': return None
sp += 1
token_base = sp
sp = sp + tl
else:
while sp[0] != 0 and sp[0] != '|': sp += 1
tl = sizet(uint64(sp) - uint64(token_base))
val: mlirlite.Value* = self._agg_token_value(pool, builder, mod, trans, tag, token_base, 0, tl, fty)
if val is None: return None
fp: mlirlite.Value* = mlirlite.build_gep_struct(builder, sty, fty, slot, i)
if fp is None: return None
mlirlite.build_store(builder, val, fp)
if sp[0] == '|': sp += 1
cur = cur.Next
i += 1
if cur is not None or sp[0] != 0: return None
if param_ty.kind() == mlirlite.LLVMType.Ptr:
if self._call_ty_equal(pool, param_ty, slot.Ty) == 0:
bc2: mlirlite.Value* = mlirlite.build_bitcast(builder, slot, param_ty)
if bc2 is not None: return bc2
return slot
return mlirlite.build_load(builder, sty, slot)
def _build_const_from_snapshot(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, gd_entry: GenTypes.FuncDefaultsEntry*, di: int, trans: HT.Translator*, builder: mlirlite.IRBuilder*, param_ty: mlirlite.LLVMType* = None) -> mlirlite.Value*:
"""Rebuild the di-th default-value constant IR from the snapshot"""
if gd_entry is None or gd_entry.ConstKinds is None or gd_entry.ConstInts is None: return None
if di < 0 or di >= gd_entry.DefaultCount: return None
@@ -1603,7 +1889,44 @@ novirtual class CallGenLeafKit(CallGenCBuiltin.CallGenCBuiltin):
if gd_entry.ConstStrs is not None and gd_entry.ConstStrs[di] != 0:
sv: char* = rawptr(gd_entry.ConstStrs[di])
return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, sv, trans)
# return None for unreconstructable values (CONST_FLOAT / complex expr); caller falls back to AST
elif kind == ast.CONST_FLOAT:
# The snapshot stores the IEEE-754 bit pattern in the int64 slot.
uv_f: uint64 = uint64(ival)
dv_f: double = 0.0
*&dv_f = *&uv_f
return mlirlite.ConstFloat(pool, mlirlite.Double(pool), dv_f)
elif kind == GenTypes.CONST_AGG_DEFAULT:
if gd_entry.ConstStrs is None or gd_entry.ConstStrs[di] == 0: return None
spec: char* = rawptr(gd_entry.ConstStrs[di])
return self._build_agg_from_tokens(pool, builder, mod, trans, spec, param_ty)
elif kind == GenTypes.CONST_EXPR_TEXT:
# Resolved now (enums/CDefines are registered): enum member `Flags.Name_` or a
# bare CDefine name.
if gd_entry.ConstStrs is None or gd_entry.ConstStrs[di] == 0: return None
sv: char* = rawptr(gd_entry.ConstStrs[di])
dot: char* = string.strrchr(sv, '.')
if dot is not None:
slen: sizet = uint64(dot) - uint64(sv)
if slen == 0: return None
scope: bytes = pool.alloc(slen + 1)
if scope is None: return None
string.strncpy(scope, sv, slen)
scope[slen] = '\0'
me: GenTypes.EnumMember* = self.Gen.StmtGenDecl.lookup_enum_member(scope, dot + 1, None)
if me is None: return None
mbits: int = 32
if me.Ty is not None:
match me.Ty:
case mlirlite.LLVMType.Int(bits_e): mbits = bits_e
case _: mbits = 32
return mlirlite.const_int(pool, mbits, me.Value)
cv: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(sv)
if self.Gen.TypeGenCDef.is_cdefine_found() == 0: return None
if cv > 2147483647 or cv < -2147483648: return mlirlite.const_int64(pool, cv)
return mlirlite.const_int32(pool, cv)
elif kind == GenTypes.CONST_UNKNOWN_DEFAULT:
return None
# return None for unreconstructable values (complex expr); caller falls back to AST
return None
+97 -5
View File
@@ -346,7 +346,23 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
return val
case _:
pass
case mlirlite.LLVMType.Ptr(_):
case mlirlite.LLVMType.Float(bits_fa):
match tgt_ty:
case mlirlite.LLVMType.Float(bits_fb):
if bits_fb == bits_fa: return val
if bits_fb > bits_fa:
nv_fe: mlirlite.Value* = mlirlite.build_fpext(builder, val, tgt_ty)
if nv_fe is not None: return nv_fe
return val
# Narrowing (double default literal -> float param): only constants, so an
# SSA value's precision is never silently dropped.
if val.IsConst != 0:
nv_ft: mlirlite.Value* = mlirlite.build_fptrunc(builder, val, tgt_ty)
if nv_ft is not None: return nv_ft
return val
case _:
pass
case mlirlite.LLVMType.Ptr(ptr_elem):
match tgt_ty:
case mlirlite.LLVMType.Ptr(_):
# same-level pointers: bitcast when the arg type differs from the definition
@@ -355,11 +371,21 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
nv_p: mlirlite.Value* = mlirlite.build_bitcast(builder, val, tgt_ty)
if nv_p is not None: return nv_p
case mlirlite.LLVMType.Int(bits_pi):
# A pointer passed where the callee's parameter degraded to an integer
# (e.g. `rawptr` resolved as uint64 in a stale/mis-typed stub): convert at the
# call boundary instead of failing the implicit-cast rule, so passing a
# rawptr never breaks the build and the callee still receives the address.
if bits_pi > 0:
# Win64 C ABI: a by-value aggregate parameter of exactly 1/2/4/8 bytes
# arrives here as the address of Viper's spilled copy. Pass the aggregate
# BITS, not the address (a C callee reads the struct from the register).
if ptr_elem is not None and ptr_elem.kind() == mlirlite.LLVMType.Struct:
sz_pi: int = self.Gen.CallGenBase._c_agg_size(ptr_elem)
if sz_pi > 0 and sz_pi * 8 == bits_pi and self.Gen.CallGenBase.is_win64_c_abi() != 0:
pi_ip: mlirlite.Value* = mlirlite.build_bitcast(builder, val, mlirlite.Ptr(pool, tgt_ty))
if pi_ip is not None:
pi_ld: mlirlite.Value* = mlirlite.build_load(builder, tgt_ty, pi_ip)
if pi_ld is not None: return pi_ld
# A pointer passed where the callee's parameter degraded to an integer
# (e.g. `rawptr` resolved as uint64 in a stale/mis-typed stub): convert at the
# call boundary instead of failing the implicit-cast rule, so passing a
# rawptr never breaks the build and the callee still receives the address.
nv_p2: mlirlite.Value* = mlirlite.build_ptrtoint(builder, val, tgt_ty)
if nv_p2 is not None: return nv_p2
case _:
@@ -374,6 +400,19 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
if tmp_a is not None:
mlirlite.build_store(builder, val, tmp_a)
return tmp_a
case mlirlite.LLVMType.Int(bits_agg):
# Win64 C ABI: a 1/2/4/8-byte by-value aggregate is passed in one integer
# register. Spill the value and load its bits at the parameter width.
if bits_agg > 0 and self.Gen.CallGenBase.is_win64_c_abi() != 0 and self.Gen.CallGenBase._c_agg_size(val.Ty) * 8 == bits_agg:
vk_b: int = val.Ty.kind()
if vk_b == mlirlite.LLVMType.Struct or vk_b == mlirlite.LLVMType.Array:
tmp_b: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, val.Ty)
if tmp_b is not None:
mlirlite.build_store(builder, val, tmp_b)
ip_b: mlirlite.Value* = mlirlite.build_bitcast(builder, tmp_b, mlirlite.Ptr(pool, tgt_ty))
if ip_b is not None:
ld_b: mlirlite.Value* = mlirlite.build_load(builder, tgt_ty, ip_b)
if ld_b is not None: return ld_b
case _:
pass
return val
@@ -582,6 +621,56 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
# Return dst (matches C memcpy semantics: returns the dest pointer)
return dst_coerced
def _emit_llvm_memset_intrinsic(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, dst_val: mlirlite.Value*, byte_val: mlirlite.Value*, num_val: mlirlite.Value*) -> mlirlite.Value*:
"""Fill memory via the @llvm.memset intrinsic (avoids whole-aggregate constant stores)."""
if pool is None or builder is None or mod is None: return None
if dst_val is None or byte_val is None or num_val is None: return None
void_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
i64_ty: mlirlite.LLVMType* = mlirlite.Int64(pool)
i1_ty: mlirlite.LLVMType* = mlirlite.Int1(pool)
# Declare @llvm.memset (if not already declared): dst, val, len, isvolatile
memset_func: mlirlite.Function* = self.find_func_in_module(mod, "llvm.memset")
if memset_func is None:
memset_func = mlirlite.create_declare(pool, mod, "llvm.memset", void_ty)
if memset_func is not None:
mlirlite.add_param(pool, memset_func, i8_ptr_ty, "dst")
mlirlite.add_param(pool, memset_func, i8_ty, "val")
mlirlite.add_param(pool, memset_func, i64_ty, "len")
mlirlite.add_param(pool, memset_func, i1_ty, "isvolatile")
# dst -> i8*
dst_coerced: mlirlite.Value* = dst_val
dst_ty: mlirlite.LLVMType* = mlirlite.value_get_ty(dst_val)
if self.Gen.ExprGenBase.is_ptr_type(dst_ty) != 0:
dst_coerced = mlirlite.build_bitcast(builder, dst_val, i8_ptr_ty)
else:
dst_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(dst_ty)
if dst_bits != 0:
if dst_bits < 64: dst_coerced = mlirlite.build_zext(builder, dst_val, i64_ty)
dst_coerced = mlirlite.build_inttoptr(builder, dst_coerced, i8_ptr_ty)
# val -> i8; len -> i64
byte_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, byte_val, i8_ty)
num_coerced: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, num_val, i64_ty)
if dst_coerced is None or byte_coerced is None or num_coerced is None: return None
# Append the i1 false arg (isvolatile)
false_val: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty, 0, "false")
mlirlite.value_set_next(dst_coerced, byte_coerced)
mlirlite.value_set_next(byte_coerced, num_coerced)
if false_val is not None:
mlirlite.value_set_next(num_coerced, false_val)
mlirlite.value_set_next(false_val, None)
else: mlirlite.value_set_next(num_coerced, None)
mlirlite.build_call(builder, "llvm.memset", dst_coerced, 4, void_ty, 0)
return dst_coerced
# _translate_call_with_kwargs: keyword-arg call; builds args in found_func.Params order (positional first, else match by param name)
# ============================================================
@@ -682,4 +771,7 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
# memcpy -> @llvm.memcpy intrinsic (avoid a missing cross-module @memcpy declare under monomorphization)
if string.strcmp(func_name, "memcpy") == 0 and actual_count >= 3: return self._emit_llvm_memcpy_intrinsic(pool, builder, mod, mc_dst1, mc_src1, mc_num1)
if call_ret_ty is not None and call_ret_ty.kind() == mlirlite.LLVMType.Struct and self.Gen.CallGenBase.is_extern_agg(call_name) != 0:
agg_kw: mlirlite.Value* = self.Gen.CallGenBase.emit_extern_agg_call(pool, builder, mod, call_name, head, int(actual_count), call_ret_ty)
if agg_kw is not None: return agg_kw
return mlirlite.build_call(builder, call_name, head, actual_count, call_ret_ty, 0)
+70 -2
View File
@@ -179,6 +179,55 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# 早期补参用的是 dfx_callee_sha,链式接收者(`self.Gen.X.m(...)`)解析不出发起模块时它会
# 退化成调用方 SHA1,表查不到;此时 call_name 已带定义模块 SHA1 前缀,是权威键,再补一次。
# 返回补齐后的实参个数。
def _default_param_llvm_ty(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, call_name: char*, idx: int, trans: HT.Translator*) -> mlirlite.LLVMType*:
"""LLVM type of parameter `idx` in the callee's declaration (None when unavailable).
An aggregate default is rebuilt from this type's struct fields, so the declare (created
by the vpi injection / forward declaration) must already exist at default-fill time.
The injected declare may have resolved a bare struct annotation against a same-named
struct in THIS module. Re-resolve it by (name, callee module SHA1) when the declared
struct belongs to another module; otherwise the default is built with the wrong layout.
"""
if mod is None or call_name is None or idx < 0: return None
func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, call_name)
if func is None: return None
prm: mlirlite.Param* = mlirlite.function_get_param_head(func)
j: int = 0
while prm is not None:
if j == idx: break
prm = mlirlite.param_get_next(prm)
j += 1
if prm is None: return None
pty: mlirlite.LLVMType* = mlirlite.param_get_ty(prm)
if pty is None or pool is None: return pty
pe: mlirlite.LLVMType* = None
match pty:
case mlirlite.LLVMType.Ptr(pe0): pe = pe0
case _: pe = None
if pe is None or pe.kind() != mlirlite.LLVMType.Struct: return pty
sname: char* = None
match pe:
case mlirlite.LLVMType.Struct(_, _, sn): sname = sn
case _: sname = None
if sname is None: return pty
sha_pfx: int = GenApi.sha1_prefix_len(call_name)
if sha_pfx == 0: return pty
sha_buf: char* = pool.alloc(17)
if sha_buf is None: return pty
string.strncpy(sha_buf, call_name, 16)
sha_buf[16] = '\0'
# Already the callee module's struct? (name is "{sha1}.Class")
if string.strncmp(sname, sha_buf, 16) == 0 and sname[16] == '.': return pty
dot_s: char* = string.strrchr(sname, '.')
bare: char* = sname
if dot_s is not None: bare = dot_s + 1
VpiReader._ensure_dep_vpi_injected(pool, mod, sha_buf, trans)
se: GenTypes.StructEntry* = GenApi.find_struct_by_module(bare, sha_buf)
if se is not None and se.Ty is not None:
return mlirlite.Ptr(pool, se.Ty)
return pty
def _fill_defaults_late(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, builder: mlirlite.IRBuilder*, trans: HT.Translator*, cl: ast.Call*, call_name: char*, head: mlirlite.Value*, can: sizet) -> sizet:
"""按 call_name 里的定义模块 SHA1 前缀补缺省参数;返回新的实参个数"""
if head is None or can == 0 or call_name is None: return can
@@ -207,7 +256,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
for df_i in range(int(de.DefaultCount)):
df_pidx: int = df_off + df_i
if df_pidx < n: continue
df_val: mlirlite.Value* = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, de, df_i, trans, builder)
df_ty: mlirlite.LLVMType* = None
if de.ConstKinds is not None and de.ConstKinds[df_i] == GenTypes.CONST_AGG_DEFAULT:
df_ty = self._default_param_llvm_ty(pool, mod, call_name, df_pidx, trans)
df_val: mlirlite.Value* = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, de, df_i, trans, builder, df_ty)
if df_val is None:
self.Gen.TypeGenErr.fatal_error(cl, f"default argument {df_i + 1} of '{call_name}' cannot be rebuilt into a value; refusing to emit a call with fewer operands than the callee declares", "TypeError")
df_val.Next = None
@@ -1428,7 +1480,18 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if df_param_idx >= can:
# Prefer rebuilding from the stdlib snapshot (gd_entry.Defaults' in-pool AST may already be reclaimed).
df_arg_val: mlirlite.Value* = None
if gd_entry is not None: df_arg_val = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, gd_entry, df_di, trans, builder)
if gd_entry is not None:
# Aggregate defaults need the callee's parameter type: the injected
# declare may have bound a same-named struct from this module, and
# building from that layout silently passes the wrong bytes.
df_pty: mlirlite.LLVMType* = None
if gd_entry.ConstKinds is not None and gd_entry.ConstKinds[df_di] == GenTypes.CONST_AGG_DEFAULT:
df_call_nm: char* = func_name
if found_func is not None:
df_fn_nm: char* = mlirlite.function_get_name(found_func)
if df_fn_nm is not None: df_call_nm = df_fn_nm
df_pty = self._default_param_llvm_ty(pool, mod, df_call_nm, df_param_idx, trans)
df_arg_val = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, gd_entry, df_di, trans, builder, df_pty)
if df_arg_val is None and df_defaults is not None:
df_defaults_list: list[ast.AST*]* = list[ast.AST*]*(df_defaults)
df_default_node: ast.AST* = df_defaults_list.get(df_di)
@@ -1804,5 +1867,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# instead of emitting an llvm.call the module's llvm.func does not accept.
head = self.Gen.CallGenBase._conform_call_args(pool, builder, mod, call_name, head, cl, trans)
call_ret_ty = self.Gen.CallGenBase._call_symbol_ret_ty(pool, mod, call_name, call_ret_ty, cl)
# C extern returning an aggregate: the C ABI differs from Viper's aggregate convention
# (Win64: i8/i16/i32/i64 return for exactly 1/2/4/8 bytes, hidden sret pointer otherwise).
if call_ret_ty is not None and call_ret_ty.kind() == mlirlite.LLVMType.Struct and self.Gen.CallGenBase.is_extern_agg(call_name) != 0:
agg_res: mlirlite.Value* = self.Gen.CallGenBase.emit_extern_agg_call(pool, builder, mod, call_name, head, int(can), call_ret_ty)
if agg_res is not None: return agg_res
# try-except: when the callee takes EH out-params, append the output args and check for exceptions after the call.
return self.Gen.CallGenLeafKit._emit_eh_guarded_call(builder, pool, mod, trans, found_func, call_name, head, can, call_ret_ty, is_variadic_call)
+106 -1
View File
@@ -4,6 +4,7 @@ import memhub
import string
import stdlib
import viperlib
import mlirlite
import lib.core.Gen.Translator as HT
import lib.core.CompilerFactory as CompilerFactory
import lib.core.Gen.GenTypes as GenTypes
@@ -219,7 +220,7 @@ novirtual class ClassGenBase:
return 1 if string.strcmp(at2.attr, attr_name) == 0 else 0
return 0
def _class_has_tattr(self, decorator_list: list[ast.AST*]*, attr_name: str) -> int:
"""Return 1 if decorator_list contains t.attr.<attr_name> (@c.Attribute(t.attr.xxx) or @t.attr.xxx)."""
"""Return 1 if decorator_list contains t.attr.<attr_name> (@Attribute(attr.xxx) or @t.attr.xxx)."""
if decorator_list is None: return 0
dn: sizet = len(decorator_list)
if dn == 0: return 0
@@ -242,6 +243,110 @@ novirtual class ClassGenBase:
# Direct @t.attr.xxx form.
if self._tattr_name(deco, attr_name) != 0: return 1
return 0
def _decor_expr_text(self, pool: memhub.MemBuddy*, node: ast.AST*, depth: int = 0) -> str:
"""Dotted decorator text for diagnostics (Name / Attribute / Call)."""
if node is None or depth > 8: return "?"
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None: return nm.id
return "?"
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
return self._decor_expr_text(pool, cl.func, depth + 1)
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.attr is None: return "?"
base: str = self._decor_expr_text(pool, at.value, depth + 1)
if base is None or string.strcmp(base, "?") == 0: return at.attr
cap: sizet = string.strlen(base) + string.strlen(at.attr) + 2
buf: str = pool.alloc(cap)
if buf is None: return at.attr
viperlib.snprintf(buf, cap, "%s.%s", base, at.attr)
return buf
return "?"
def _decor_is_builtin_name(self, name: str) -> int:
"""1 when name is a compiler-recognized bare decorator marker (class/function/method)."""
if name is None: return 0
if string.strcmp(name, "staticmethod") == 0: return 1
if string.strcmp(name, "property") == 0: return 1
if string.strcmp(name, "classmethod") == 0: return 1
if string.strcmp(name, "NoVTable") == 0: return 1
if string.strcmp(name, "VTableT") == 0: return 1
if string.strcmp(name, "TLS") == 0: return 1
if string.strcmp(name, "StaticT") == 0: return 1
if string.strcmp(name, "ExportT") == 0: return 1
if string.strcmp(name, "ExternT") == 0: return 1
if string.strcmp(name, "State") == 0: return 1
if string.strcmp(name, "PureT") == 0: return 1
if string.strcmp(name, "DefineT") == 0: return 1
if string.strcmp(name, "InlineT") == 0: return 1
if string.strcmp(name, "Object") == 0: return 1
if string.strcmp(name, "EnumBase") == 0: return 1
if string.strcmp(name, "UnionBase") == 0: return 1
if string.strcmp(name, "REnum") == 0: return 1
if string.strcmp(name, "life") == 0: return 1
if string.strcmp(name, "willFree") == 0: return 1
if string.strcmp(name, "Attribute") == 0: return 1
return 0
def _decor_is_attr_marker(self, name: str) -> int:
"""1 when name is a recognized attribute marker or property accessor."""
if name is None: return 0
if string.strcmp(name, "setter") == 0: return 1
if string.strcmp(name, "getter") == 0: return 1
if string.strcmp(name, "deleter") == 0: return 1
if string.strcmp(name, "packed") == 0: return 1
if string.strcmp(name, "aligned") == 0: return 1
if string.strcmp(name, "section") == 0: return 1
if string.strcmp(name, "always_inline") == 0: return 1
if string.strcmp(name, "noinline") == 0: return 1
if string.strcmp(name, "noreturn") == 0: return 1
if string.strcmp(name, "pure") == 0: return 1
if string.strcmp(name, "const") == 0: return 1
if string.strcmp(name, "nounwind") == 0: return 1
if string.strcmp(name, "noredzone") == 0: return 1
if string.strcmp(name, "stackrealign") == 0: return 1
if string.strcmp(name, "willreturn") == 0: return 1
if string.strcmp(name, "mustprogress") == 0: return 1
if string.strcmp(name, "weak") == 0: return 1
if string.strcmp(name, "visibility") == 0: return 1
if string.strcmp(name, "naked") == 0: return 1
return 0
def _decor_handler_known(self, trans: HT.Translator*, mod: mlirlite.LLVMModule*, name: str) -> int:
"""1 when a bare decorator name resolves to a declared decorator handler function."""
if name is None or trans is None: return 0
if mod is not None and self.Gen.CallGenBase.find_func_in_module(mod, name) is not None: return 1
if self.Gen.CallGenLeafKit.find_func_entry_global(name) is not None: return 1
return 0
def validate_decorators(self, trans: HT.Translator*, mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, owner_name: str, owner_kind: str, decorator_list: list[ast.AST*]*, is_class: int) -> int:
"""Fail fast on decorators the compiler does not recognize (never silently ignore them)."""
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
k: int = deco.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(deco)
if nm.id is not None and self._decor_is_builtin_name(nm.id) != 0: continue
if is_class == 0 and self._decor_handler_known(trans, mod, nm.id) != 0: continue
elif k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(deco)
if cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
cnm: ast.Name* = ast.Name*(cl.func)
if cnm.id is not None and self._decor_is_builtin_name(cnm.id) != 0: continue
if is_class == 0 and self._decor_handler_known(trans, mod, cnm.id) != 0: continue
elif cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
cat: ast.Attribute* = ast.Attribute*(cl.func)
if cat.attr is not None and self._decor_is_attr_marker(cat.attr) != 0: continue
elif k == ast.ASTKind.Attribute:
dat: ast.Attribute* = ast.Attribute*(deco)
if dat.attr is not None and self._decor_is_attr_marker(dat.attr) != 0: continue
dtxt: str = self._decor_expr_text(pool, deco, 0)
self.Gen.TypeGenErr.fatal_error(deco, f"unrecognized decorator '@{dtxt}' on {owner_kind} '{owner_name}': ViperC recognizes built-in decorators, `@Attribute(...)` / attribute markers, property accessors, and custom decorators with a declared handler function", "TypeError")
return 1
return 0
def _tattr_aligned_int(self, node: ast.AST*) -> int:
"""Return N for t.attr.aligned(N); 0 when not aligned or the argument is missing."""
if node is None: return 0
+6 -2
View File
@@ -29,6 +29,10 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
pool: memhub.MemBuddy* = trans.Pool
class_name: str = cd.name
# Reject unknown decorators instead of silently dropping them (e.g. the retired
# `@c.Attribute(...)` spelling); only recognized built-ins/markers are accepted here.
self.Gen.ClassGenBase.validate_decorators(trans, trans.Module, pool, class_name, "class", cd.decorator_list, 1)
if trans.ModuleSha1 is None:
# fail-fast: ModuleSha1 missing while translating a class (namespace isolation depends on it)
VLogger.error("ModuleSha1 missing while translating a class", "CLASS")
@@ -466,7 +470,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
# Register the named struct into the module (emit the %"name" = type { ... } line)
mlirlite.module_add_named_type(trans.Module, pool, struct_ty)
# B5: @c.Attribute(t.attr.packed) emits <{...}> (LLVM packed struct, no padding) as
# B5: @Attribute(attr.packed) emits <{...}> (LLVM packed struct, no padding) as
# needed by binary-format structs like idt/gdt/elf/fat32.
is_packed_cd: int = self.Gen.ClassGenBase._class_has_tattr(cd.decorator_list, "packed")
if is_packed_cd != 0: mlirlite.module_set_named_type_packed(trans.Module, pool, struct_ty, 1)
@@ -499,7 +503,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
# B5: record the packed flag into StructEntry for __sizeof__/layout queries
if is_packed_cd != 0: entry.IsPacked = 1
# B5: record the aligned value (@c.Attribute(t.attr.aligned(N)), e.g. fpu_context 16)
# B5: record the aligned value (@Attribute(attr.aligned(N)), e.g. fpu_context 16)
# for global/local alloca alignment.
align_cd: int = self.Gen.ClassGenBase._class_aligned_value(cd.decorator_list)
if align_cd > 0: entry.Align = align_cd
+10 -9
View File
@@ -14,7 +14,8 @@ from . import ClassGenBase
# Local constant copies (originally defined in HandlesEnum/HandlesClassDef; lost when the file was split).
# Undefined names fold to 0 in constant context, silently skipping enum member collection and EnumBase registration.
define ENUM_MEMBER_MAX_LOCAL = 128
# ENUM_MEMBER_MAX is NOT defined here: it lives in GenTypes and is shared with EnumEntry storage
# (StmtGenDeclMixin) and the .vpi reader (VpiReader), so the three cannot drift apart.
define RENUM_CLASS_MAX = 64
define RENUM_VARIANT_PER_CLASS = 32
define RENUM_ALIAS_MAX = 512
@@ -220,13 +221,13 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
cn: sizet = len(children)
# Temporary arrays holding member info
member_names_buf: sizet* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL)
member_types_buf: sizet* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL)
member_values_buf: int64* = pool.alloc(8 * ENUM_MEMBER_MAX_LOCAL)
member_names_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX)
member_types_buf: sizet* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX)
member_values_buf: int64* = pool.alloc(8 * GenTypes.ENUM_MEMBER_MAX)
if member_names_buf is None or member_types_buf is None or member_values_buf is None: return 0
string.memset(member_names_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL)
string.memset(member_types_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL)
string.memset(member_values_buf, 0, 8 * ENUM_MEMBER_MAX_LOCAL)
string.memset(member_names_buf, 0, 8 * GenTypes.ENUM_MEMBER_MAX)
string.memset(member_types_buf, 0, 8 * GenTypes.ENUM_MEMBER_MAX)
string.memset(member_values_buf, 0, 8 * GenTypes.ENUM_MEMBER_MAX)
member_count: int = 0
next_val: int64 = 0
@@ -273,7 +274,7 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
next_val = cur_val + 1
# Store
if member_count < ENUM_MEMBER_MAX_LOCAL:
if member_count < GenTypes.ENUM_MEMBER_MAX:
member_names_buf[member_count] = sizet(tgt.id)
member_types_buf[member_count] = sizet(member_ty)
member_values_buf[member_count] = cur_val
@@ -303,7 +304,7 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
if t0 is None or t0.kind() != ast.ASTKind.Name: continue
tgt_a: ast.Name* = ast.Name*(t0)
if tgt_a.id is None: continue
if member_count < ENUM_MEMBER_MAX_LOCAL:
if member_count < GenTypes.ENUM_MEMBER_MAX:
member_names_buf[member_count] = sizet(tgt_a.id)
member_types_buf[member_count] = sizet(member_ty_a)
member_values_buf[member_count] = cur_val_a
+40
View File
@@ -572,6 +572,10 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
funcs_ptr: GenTypes.FuncEntry* = trans._funcs
func_count: int = trans._func_count
# Method decorators are validated here as well: unknown markers must not be dropped
# silently just because a method body uses a different translation entry point.
self.Gen.ClassGenBase.validate_decorators(trans, mod, pool, fd.name, "method", fd.decorator_list, 0)
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
self_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, struct_ty)
@@ -818,6 +822,27 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
# module-level function path). No-op when DI is disabled.
dbg_pushed: int = DebugInfoGen.PushFunction(pool, func_builder, func, fd.lineno, fd.col_offset + 1)
# Constructors may be invoked on a null object when __new__ failed (pool exhausted).
# __before_init__ is null-safe; __init__ must be too, or the first field store is an
# access violation (ASan: LspRange.__init__ in the LSP semantic-token path).
if fd.name is not None and string.strcmp(fd.name, "__init__") == 0 and is_static_m == 0:
mi_sp: mlirlite.Param* = mlirlite.function_get_param_head(func)
if mi_sp is not None:
mi_sp_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(mi_sp)
mi_sp_nm: char* = mlirlite.param_get_name(mi_sp)
if mi_sp_ty is not None and mi_sp_nm is not None:
mi_self: mlirlite.Value* = mlirlite.SSAValue(pool, mi_sp_ty, mi_sp_nm)
mi_null: mlirlite.Value* = mlirlite.ConstNull(pool, mi_sp_ty, "null")
mi_null_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.selfnull")
mi_body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.body")
if mi_self is not None and mi_null is not None and mi_null_bb is not None and mi_body_bb is not None:
mi_is_null: mlirlite.Value* = mlirlite.build_icmp(func_builder, mlirlite.ICMP_EQ, mi_self, mi_null)
if mi_is_null is not None:
mlirlite.build_cond_br(func_builder, mi_is_null, mi_null_bb, mi_body_bb)
mlirlite.position_at_end(func_builder, mi_null_bb)
mlirlite.build_ret_void(func_builder)
mlirlite.position_at_end(func_builder, mi_body_bb)
# @t.TLS one-time init guard (same implementation as FuncGenMixin.translate_function_def, also for methods).
if is_tls_m != 0:
# Exact capacities: the reference used to be 64 while the definition was 128, so a long
@@ -965,6 +990,21 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
# Register self as an SSA value.
self_val: mlirlite.Value* = mlirlite.SSAValue(pool, self_ptr_ty, "%self")
# Null-safe: __new__ may return null when the pool is exhausted (e.g. a large LSP
# semantic-token response), and zero-filling through a null self is an access violation
# (ASan: JsonValue.__before_init__). Return immediately; the constructor then yields null
# and callers that already check `if v == None` treat it as OOM instead of crashing.
null_self: mlirlite.Value* = mlirlite.ConstNull(pool, self_ptr_ty, "null")
body_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "body")
null_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "nullret")
if null_self is not None and body_blk is not None and null_blk is not None:
is_null: mlirlite.Value* = mlirlite.build_icmp(func_builder, mlirlite.ICMP_EQ, self_val, null_self)
if is_null is not None:
mlirlite.build_cond_br(func_builder, is_null, null_blk, body_blk)
mlirlite.position_at_end(func_builder, null_blk)
mlirlite.build_ret_void(func_builder)
mlirlite.position_at_end(func_builder, body_blk)
# Zero-fill: store zeroinitializer into %self. Empty structs ({}) cannot use zeroinitializer
# (invalid type for null constant), so field-less classes skip it; @t.VTableT classes always have a slot.
struct_fcount_zi: int = mlirlite.get_struct_fcount(struct_ty)
+368 -374
View File
@@ -1,374 +1,368 @@
from stdint import *
import mlirlite
import memhub
import string
import stdlib
import viperlib
import win32.win32file as win32file
import lib.core.VLogger as VLogger
# DebugInfoGen - emits function-level DWARF metadata (mlirlite text IR), mirroring TPC LLVMCG/DebugInfo.py node-for-node.
# Active only under --traceback (SetEnabled); hooks come from the Gen layer. When disabled every entry point returns before touching DI APIs, keeping IR byte-identical.
# ---- module-scope state (reset per module by BeginModule) ----
_g_enabled: int = 0
_g_finalized: int = 0
_g_mod: mlirlite.LLVMModule* = None
_g_file_path: str = None
_g_abs_file_path: str = None
_g_file_id: int = -1
_g_null_id: int = -1
_g_subtype_id: int = -1
_g_cu_id: int = -1
_g_cur_sp: int = -1
_g_cur_entry_ref: str = None
_g_cur_builder: mlirlite.IRBuilder* = None
_g_depth: int = 0
# Push/Pop save stack: enclosing (sp id + 1, entry DILocation ref, builder) per level; +1 lets 0 mean "no enclosing function".
# The saved builder makes SetStatement reject a builder not pushed by the current DISubprogram (e.g. an async state machine in a bracketed function); such instrs must carry no !dbg or LLVM's verifier fails.
define DBG_STACK_MAX = 256
_g_stk_sp: sizet* = None
_g_stk_ref: str = None
_g_stk_bld: str = None
# DILocation cache: direct-mapped, up to 8-way linear probe, key (sp, line, col).
define DBG_LOC_SLOTS = 8192
define DBG_LOC_PROBE = 8
define DBG_LOC_KEY_STRIDE = 3
_g_loc_key: sizet* = None
_g_loc_ref: str = None
_g_loc_used: bytes = None
# SetEnabled - mirror the --traceback switch (flag -> _g_enabled, forwarded to mlirlite's debug call-site recorder).
def SetEnabled(flag: bool) -> int:
"""Enable/disable DI emission (mirrors BuildPipeline._traceback_mode)."""
global _g_enabled
if flag: _g_enabled = 1
else: _g_enabled = 0
# Forward the same switch to mlirlite's debug call-site recorder (under --traceback we emit a <sha1>.vpcall side file for WriteStackMap+objdump).
mlirlite.DebugSetEnabled(flag)
return 0
# _EnsureStorage - lazily allocate the long-lived stack / cache buffers.
def _EnsureStorage() -> int:
global _g_stk_sp, _g_stk_ref, _g_stk_bld, _g_loc_key, _g_loc_ref, _g_loc_used
if _g_stk_sp is not None: return 0
sbuf1: bytes = stdlib.malloc(DBG_STACK_MAX * 8)
sbuf2: bytes = stdlib.malloc(DBG_STACK_MAX * 8)
sbuf3: bytes = stdlib.malloc(DBG_STACK_MAX * 8)
kbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8)
rbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * 8)
ubuf: bytes = stdlib.malloc(DBG_LOC_SLOTS)
if sbuf1 is None or sbuf2 is None or sbuf3 is None or kbuf is None or rbuf is None or ubuf is None:
VLogger.error("--traceback: failed to allocate DWARF metadata buffer, aborting compilation", "DWARF")
return 1
string.memset(sbuf1, 0, DBG_STACK_MAX * 8)
string.memset(sbuf2, 0, DBG_STACK_MAX * 8)
string.memset(sbuf3, 0, DBG_STACK_MAX * 8)
string.memset(kbuf, 0, DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8)
string.memset(rbuf, 0, DBG_LOC_SLOTS * 8)
string.memset(ubuf, 0, DBG_LOC_SLOTS)
_g_stk_sp = (sizet*)(sbuf1)
_g_stk_ref = sbuf2
_g_stk_bld = sbuf3
_g_loc_key = (sizet*)(kbuf)
_g_loc_ref = rbuf
_g_loc_used = ubuf
return 0
def _RefOf(pool: memhub.MemBuddy*, node_id: int) -> str:
"""Allocate a persistent "!N" reference string for a DI node id."""
buf: str = pool.alloc(24)
if buf is None: return None
viperlib.snprintf(buf, 24, "!%d", node_id)
return buf
# BeginModule - start of one module translation.
def BeginModule(mod: mlirlite.LLVMModule*, file_path: str) -> int:
"""Reset per-module DI state. Call once at the start of each module."""
global _g_mod, _g_file_path, _g_abs_file_path, _g_finalized, _g_file_id, _g_null_id
global _g_subtype_id, _g_cu_id, _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth
if _g_enabled == 0: return 0
if _EnsureStorage() != 0: return 1
_g_mod = mod
_g_file_path = file_path
_g_abs_file_path = None
# Module-boundary fallback: clear any un-Pop'd debug function context (mid-translation failure) so the previous module's caller is not attributed here.
mlirlite.DebugResetFunc()
_g_finalized = 0
_g_file_id = -1
_g_null_id = -1
_g_subtype_id = -1
_g_cu_id = -1
_g_cur_sp = -1
_g_cur_entry_ref = None
_g_cur_builder = None
_g_depth = 0
if _g_loc_used is not None: string.memset(_g_loc_used, 0, DBG_LOC_SLOTS)
return 0
# _EnsureModuleNodes - lazily create DIFile / !{ null } / DISubroutineType / DICompileUnit at the first define (once per module).
def _EnsureModuleNodes(pool: memhub.MemBuddy*) -> int:
global _g_file_id, _g_null_id, _g_subtype_id, _g_cu_id, _g_abs_file_path
if _g_cu_id >= 0: return 0
mod: mlirlite.LLVMModule* = _g_mod
if mod is None: return 1
# DIFile: absolute path (forward slashes), filename keeps the whole path.
# Must be absolute: relative paths make LLVM's DWARF line records degrade to a bare "main.vp" that runtime cannot reload; absolutizing works whether LLVM keeps the full name or splits dir+basename.
src: str = _g_file_path
if src is None: src = ""
slen: sizet = string.strlen(src)
cap_n: sizet = slen + 4112
nbuf: str = pool.alloc(cap_n)
if nbuf is None:
VLogger.error("--traceback: failed to allocate DIFile path buffer, aborting compilation", "DWARF")
return 1
if slen > 0: string.strncpy(nbuf, src, slen)
nbuf[slen] = '\0'
if slen > 0 and nbuf[0] != '/' and not (slen >= 2 and nbuf[1] == ':'):
# Relative path -> prefix CWD via a win32file low-level call (as in Repl.vp), not os._mbuddy which the compiler process may not have initialized.
CwdBuf: bytes = pool.alloc(4112)
if CwdBuf is not None:
CwdBuf[0] = '\0'
nc: ULONG = win32file.GetCurrentDirectoryA(ULONG(4111), CwdBuf)
if nc > 0:
# Defensive: when the buffer is too small GetCurrentDirectoryA writes undefined content; force a NUL.
CwdBuf[4110] = '\0'
viperlib.snprintf(nbuf, cap_n, "%s/%s", CwdBuf, src)
# Normalize all backslashes (the CWD segment may contain '\' too).
flen: sizet = string.strlen(nbuf)
ni: sizet = 0
for ni in range(int(flen)):
if nbuf[ni] == '\\': nbuf[ni] = '/'
# Save the absolute path for mlirlite's debug call-site recorder (.vpcall file paths must match DIFile: absolute + forward slash).
_g_abs_file_path = nbuf
# filename = whole path; directory = its prefix (both NUL-terminated, non-overlapping).
fname: str = nbuf
directory: str = ""
slash: str = string.strrchr(nbuf, '/')
if slash is not None:
dlen: sizet = string.strlen(nbuf) - string.strlen(slash + 1) - 1
dirbuf: str = pool.alloc(dlen + 1)
if dirbuf is not None:
string.strncpy(dirbuf, nbuf, dlen)
dirbuf[dlen] = '\0'
directory = dirbuf
dsize: sizet = string.strlen(fname) + string.strlen(directory) + 96
dbuf: str = pool.alloc(dsize)
if dbuf is None:
VLogger.error("--traceback: failed to allocate DIFile node buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(dbuf, dsize, "!DIFile(filename: \"%s\", directory: \"%s\")", fname, directory)
fid: int = mlirlite.module_add_di_node(mod, pool, dbuf)
if fid < 0:
VLogger.error("--traceback: failed to allocate DIFile metadata node, aborting compilation", "DWARF")
return 1
_g_file_id = fid
# types: !{ null } placeholder list, then DISubroutineType referencing it
nid: int = mlirlite.module_add_di_node(mod, pool, "!{ null }")
if nid < 0:
VLogger.error("--traceback: failed to allocate DISubroutineType type table, aborting compilation", "DWARF")
return 1
_g_null_id = nid
tbuf: str = pool.alloc(64)
if tbuf is None:
VLogger.error("--traceback: failed to allocate DISubroutineType buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(tbuf, 64, "!DISubroutineType(types: !%d)", _g_null_id)
tid: int = mlirlite.module_add_di_node(mod, pool, tbuf)
if tid < 0:
VLogger.error("--traceback: failed to allocate DISubroutineType metadata node, aborting compilation", "DWARF")
return 1
_g_subtype_id = tid
cbuf: str = pool.alloc(256)
if cbuf is None:
VLogger.error("--traceback: failed to allocate DICompileUnit buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(cbuf, 256, "distinct !DICompileUnit(language: DW_LANG_C99, file: !%d, producer: \"TPC\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)", _g_file_id)
cid: int = mlirlite.module_add_di_node(mod, pool, cbuf)
if cid < 0:
VLogger.error("--traceback: failed to allocate DICompileUnit metadata node, aborting compilation", "DWARF")
return 1
_g_cu_id = cid
return 0
# _LocRefAt - read a cached DILocation ref string pointer.
def _LocRefAt(slot: int) -> str:
slotp: str* = (str*)(uint64(_g_loc_ref) + sizet(slot) * 8)
r: str = slotp[0]
return r
# _EnsureLocation - DILocation ref for (sp, line, col), created + cached once.
def _EnsureLocation(pool: memhub.MemBuddy*, sp: int, line: int, col: int) -> str:
if line <= 0: return _g_cur_entry_ref
c: int = col
if c <= 0: c = 1
if _g_loc_used is None or _g_mod is None: return None
h: uint64 = (uint64(sp) * 1000003 + uint64(line) * 31 + uint64(c)) & uint64(DBG_LOC_SLOTS - 1)
pi: int = 0
while pi < DBG_LOC_PROBE:
slot: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1))
if _g_loc_used[slot] == 0: break
kbase: int = slot * DBG_LOC_KEY_STRIDE
if int(_g_loc_key[kbase]) == sp and int(_g_loc_key[kbase + 1]) == line and int(_g_loc_key[kbase + 2]) == c: return _LocRefAt(slot)
pi += 1
lbuf: str = pool.alloc(96)
if lbuf is None: return None
viperlib.snprintf(lbuf, 96, "!DILocation(line: %d, column: %d, scope: !%d)", line, c, sp)
lid: int = mlirlite.module_add_di_node(_g_mod, pool, lbuf)
if lid < 0:
VLogger.error("--traceback: failed to allocate DILocation metadata node, aborting compilation", "DWARF")
return None
lref: str = _RefOf(pool, lid)
if lref is None:
VLogger.error("--traceback: failed to allocate DILocation reference buffer, aborting compilation", "DWARF")
return None
if pi < DBG_LOC_PROBE:
slot2: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1))
kbase2: int = slot2 * DBG_LOC_KEY_STRIDE
_g_loc_key[kbase2] = sizet(sp)
_g_loc_key[kbase2 + 1] = sizet(line)
_g_loc_key[kbase2 + 2] = sizet(c)
rslot: str* = (str*)(uint64(_g_loc_ref) + sizet(slot2) * 8)
rslot[0] = lref
_g_loc_used[slot2] = 1
return lref
# PushFunction - create the DISubprogram for one define.
def PushFunction(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, func: mlirlite.Function*, line: int, col: int) -> int:
"""Attach a DISubprogram to one define and set the builder's entry DILocation."""
global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth
if _g_enabled == 0: return 0
if pool is None or builder is None or func is None or _g_mod is None: return 0
if _g_depth >= DBG_STACK_MAX:
VLogger.error("--traceback: DWARF function nesting depth exceeds the limit, aborting compilation", "DWARF")
return 0
if _EnsureModuleNodes(pool) != 0: return 0
name: str = mlirlite.function_get_name(func)
if name is None:
VLogger.error("--traceback: function name is empty, cannot emit DISubprogram, aborting compilation", "DWARF")
return 0
dline: int = line
if dline <= 0: dline = 1
dcol: int = col
if dcol <= 0: dcol = 1
ssize: sizet = string.strlen(name) + 320
spbuf: str = pool.alloc(ssize)
if spbuf is None:
VLogger.error("--traceback: failed to allocate DISubprogram buffer, aborting compilation", "DWARF")
return 0
viperlib.snprintf(spbuf, ssize, "distinct !DISubprogram(name: \"%s\", scope: !%d, file: !%d, type: !%d, line: %d, scopeLine: %d, unit: !%d, spFlags: DISPFlagDefinition)", name, _g_file_id, _g_file_id, _g_subtype_id, dline, dline, _g_cu_id)
sp_id: int = mlirlite.module_add_di_node(_g_mod, pool, spbuf)
if sp_id < 0:
VLogger.error("--traceback: failed to allocate DISubprogram metadata node, aborting compilation", "DWARF")
return 0
sp_ref: str = _RefOf(pool, sp_id)
if sp_ref is None:
VLogger.error("--traceback: failed to allocate DISubprogram reference buffer, aborting compilation", "DWARF")
return 0
mlirlite.function_set_dbg(func, sp_ref)
# save the enclosing function's DI context (LIFO), then switch to this one
_g_stk_sp[_g_depth] = sizet(_g_cur_sp + 1)
rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8)
rslot[0] = _g_cur_entry_ref
bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8)
bslot[0] = _g_cur_builder
_g_depth += 1
_g_cur_sp = sp_id
_g_cur_builder = builder
entry_ref: str = _EnsureLocation(pool, sp_id, dline, dcol)
if entry_ref is None:
VLogger.error("--traceback: failed to generate entry DILocation, aborting compilation", "DWARF")
return 0
_g_cur_entry_ref = entry_ref
mlirlite.builder_set_debug_location(builder, entry_ref)
# Enter this define: tell mlirlite's call-site recorder the current symbol/file/def-line and write an F record (for the .vpcall file + WriteStackMap entry fallback line).
mlirlite.DebugNoteFunc(name, _g_abs_file_path, dline)
return 1
# PopFunction - restore the enclosing function's DI context.
def PopFunction(builder: mlirlite.IRBuilder*) -> int:
global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth
if _g_enabled == 0: return 0
if _g_depth <= 0: return 0
_g_depth -= 1
_g_cur_sp = int(_g_stk_sp[_g_depth]) - 1
rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8)
_g_cur_entry_ref = rslot[0]
bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8)
_g_cur_builder = (mlirlite.IRBuilder*)(bslot[0])
if builder is not None: mlirlite.builder_clear_debug_location(builder)
# Leave this define: restore mlirlite's outer function context (for nested defines).
mlirlite.DebugClearFunc()
return 0
# SetStatement - point the current builder at a statement DILocation.
def SetStatement(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, line: int, col: int) -> int:
"""Update the builder's DILocation to the current statement (line precision)."""
if _g_enabled == 0 or pool is None or builder is None: return 0
# Record the current statement line: C records from build_call in this statement use it as the call-site line.
mlirlite.DebugNoteLine(line)
if _g_cur_sp < 0: return 0
ref: str = _EnsureLocation(pool, _g_cur_sp, line, col)
if ref is None: return 0
mlirlite.builder_set_debug_location(builder, ref)
return 1
# _ModuleHasDefine - does the module emit any define (IsDeclared == 0)?
def _ModuleHasDefine(mod: mlirlite.LLVMModule*) -> int:
if mod is None: return 0
cur: mlirlite.Function* = mod.FuncHead
steps: int = 0
while cur is not None:
steps += 1
if steps > 100000: break
if mlirlite.function_is_declared(cur) == 0: return 1
cur = mlirlite.function_get_next(cur)
return 0
# Finalize - register !llvm.dbg.cu / !llvm.module.flags before dump_ir.
def Finalize(pool: memhub.MemBuddy*) -> int:
"""Module-level DI tail; idempotent per module, no-op when no SP was emitted."""
global _g_finalized
if _g_enabled == 0: return 0
if _g_finalized != 0: return 0
mod: mlirlite.LLVMModule* = _g_mod
if mod is None or pool is None: return 0
_g_finalized = 1
# Self-check (mirrors TPC Phase2Translator.py:3444): a module that emits a
# define but carries no DI at all loses line info for its frames.
if mod.DIMetaCount == 0 and _ModuleHasDefine(mod) != 0: VLogger.warning("--traceback: this module emits a define but its IR carries no DWARF (!llvm.dbg.cu); its frames will show only addresses in the Ctrl+C stack", "DWARF")
if _g_cu_id < 0: return 0
cu_ref: str = _RefOf(pool, _g_cu_id)
if cu_ref is None:
VLogger.error("--traceback: failed to allocate DICompileUnit reference buffer, aborting compilation", "DWARF")
return 1
mlirlite.module_add_named_metadata(mod, pool, "llvm.dbg.cu", cu_ref)
fbuf: str = pool.alloc(64)
if fbuf is None:
VLogger.error("--traceback: failed to allocate module-level debug flags buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(fbuf, 64, "!{i32 2, !\"Debug Info Version\", i32 3}")
flags_id: int = mlirlite.module_add_di_node(mod, pool, fbuf)
if flags_id < 0:
VLogger.error("--traceback: failed to allocate llvm.module.flags metadata node, aborting compilation", "DWARF")
return 1
fl_ref: str = _RefOf(pool, flags_id)
if fl_ref is None:
VLogger.error("--traceback: failed to allocate llvm.module.flags reference buffer, aborting compilation", "DWARF")
return 1
mlirlite.module_add_named_metadata(mod, pool, "llvm.module.flags", fl_ref)
return 0
from stdint import *
import mlirlite
import memhub
import string
import stdlib
import viperlib
import os
import lib.core.VLogger as VLogger
# DebugInfoGen - emits function-level DWARF metadata (mlirlite text IR), mirroring TPC LLVMCG/DebugInfo.py node-for-node.
# Active only under --traceback (SetEnabled); hooks come from the Gen layer. When disabled every entry point returns before touching DI APIs, keeping IR byte-identical.
# ---- module-scope state (reset per module by BeginModule) ----
_g_enabled: int = 0
_g_finalized: int = 0
_g_mod: mlirlite.LLVMModule* = None
_g_file_path: str = None
_g_abs_file_path: str = None
_g_file_id: int = -1
_g_null_id: int = -1
_g_subtype_id: int = -1
_g_cu_id: int = -1
_g_cur_sp: int = -1
_g_cur_entry_ref: str = None
_g_cur_builder: mlirlite.IRBuilder* = None
_g_depth: int = 0
# Push/Pop save stack: enclosing (sp id + 1, entry DILocation ref, builder) per level; +1 lets 0 mean "no enclosing function".
# The saved builder makes SetStatement reject a builder not pushed by the current DISubprogram (e.g. an async state machine in a bracketed function); such instrs must carry no !dbg or LLVM's verifier fails.
define DBG_STACK_MAX = 256
_g_stk_sp: sizet* = None
_g_stk_ref: str = None
_g_stk_bld: str = None
# DILocation cache: direct-mapped, up to 8-way linear probe, key (sp, line, col).
define DBG_LOC_SLOTS = 8192
define DBG_LOC_PROBE = 8
define DBG_LOC_KEY_STRIDE = 3
_g_loc_key: sizet* = None
_g_loc_ref: str = None
_g_loc_used: bytes = None
# SetEnabled - mirror the --traceback switch (flag -> _g_enabled, forwarded to mlirlite's debug call-site recorder).
def SetEnabled(flag: bool) -> int:
"""Enable/disable DI emission (mirrors BuildPipeline._traceback_mode)."""
global _g_enabled
if flag: _g_enabled = 1
else: _g_enabled = 0
# Forward the same switch to mlirlite's debug call-site recorder (under --traceback we emit a <sha1>.vpcall side file for WriteStackMap+objdump).
mlirlite.DebugSetEnabled(flag)
return 0
# _EnsureStorage - lazily allocate the long-lived stack / cache buffers.
def _EnsureStorage() -> int:
global _g_stk_sp, _g_stk_ref, _g_stk_bld, _g_loc_key, _g_loc_ref, _g_loc_used
if _g_stk_sp is not None: return 0
sbuf1: bytes = stdlib.malloc(DBG_STACK_MAX * 8)
sbuf2: bytes = stdlib.malloc(DBG_STACK_MAX * 8)
sbuf3: bytes = stdlib.malloc(DBG_STACK_MAX * 8)
kbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8)
rbuf: bytes = stdlib.malloc(DBG_LOC_SLOTS * 8)
ubuf: bytes = stdlib.malloc(DBG_LOC_SLOTS)
if sbuf1 is None or sbuf2 is None or sbuf3 is None or kbuf is None or rbuf is None or ubuf is None:
VLogger.error("--traceback: failed to allocate DWARF metadata buffer, aborting compilation", "DWARF")
return 1
string.memset(sbuf1, 0, DBG_STACK_MAX * 8)
string.memset(sbuf2, 0, DBG_STACK_MAX * 8)
string.memset(sbuf3, 0, DBG_STACK_MAX * 8)
string.memset(kbuf, 0, DBG_LOC_SLOTS * DBG_LOC_KEY_STRIDE * 8)
string.memset(rbuf, 0, DBG_LOC_SLOTS * 8)
string.memset(ubuf, 0, DBG_LOC_SLOTS)
_g_stk_sp = (sizet*)(sbuf1)
_g_stk_ref = sbuf2
_g_stk_bld = sbuf3
_g_loc_key = (sizet*)(kbuf)
_g_loc_ref = rbuf
_g_loc_used = ubuf
return 0
def _RefOf(pool: memhub.MemBuddy*, node_id: int) -> str:
"""Allocate a persistent "!N" reference string for a DI node id."""
buf: str = pool.alloc(24)
if buf is None: return None
viperlib.snprintf(buf, 24, "!%d", node_id)
return buf
# BeginModule - start of one module translation.
def BeginModule(mod: mlirlite.LLVMModule*, file_path: str) -> int:
"""Reset per-module DI state. Call once at the start of each module."""
global _g_mod, _g_file_path, _g_abs_file_path, _g_finalized, _g_file_id, _g_null_id
global _g_subtype_id, _g_cu_id, _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth
if _g_enabled == 0: return 0
if _EnsureStorage() != 0: return 1
_g_mod = mod
_g_file_path = file_path
_g_abs_file_path = None
# Module-boundary fallback: clear any un-Pop'd debug function context (mid-translation failure) so the previous module's caller is not attributed here.
mlirlite.DebugResetFunc()
_g_finalized = 0
_g_file_id = -1
_g_null_id = -1
_g_subtype_id = -1
_g_cu_id = -1
_g_cur_sp = -1
_g_cur_entry_ref = None
_g_cur_builder = None
_g_depth = 0
if _g_loc_used is not None: string.memset(_g_loc_used, 0, DBG_LOC_SLOTS)
return 0
# _EnsureModuleNodes - lazily create DIFile / !{ null } / DISubroutineType / DICompileUnit at the first define (once per module).
def _EnsureModuleNodes(pool: memhub.MemBuddy*) -> int:
global _g_file_id, _g_null_id, _g_subtype_id, _g_cu_id, _g_abs_file_path
if _g_cu_id >= 0: return 0
mod: mlirlite.LLVMModule* = _g_mod
if mod is None: return 1
# DIFile: absolute path (forward slashes), filename keeps the whole path.
# Must be absolute: relative paths make LLVM's DWARF line records degrade to a bare "main.vp" that runtime cannot reload; absolutizing works whether LLVM keeps the full name or splits dir+basename.
src: str = _g_file_path
if src is None: src = ""
slen: sizet = string.strlen(src)
cap_n: sizet = slen + 4112
nbuf: str = pool.alloc(cap_n)
if nbuf is None:
VLogger.error("--traceback: failed to allocate DIFile path buffer, aborting compilation", "DWARF")
return 1
if slen > 0: string.strncpy(nbuf, src, slen)
nbuf[slen] = '\0'
if slen > 0 and nbuf[0] != '/' and not (slen >= 2 and nbuf[1] == ':'):
cwd: str = os.getcwd()
if cwd is not None:
viperlib.snprintf(nbuf, cap_n, "%s/%s", cwd, src)
# Normalize all backslashes (the CWD segment may contain '\' too).
flen: sizet = string.strlen(nbuf)
ni: sizet = 0
for ni in range(int(flen)):
if nbuf[ni] == '\\': nbuf[ni] = '/'
# Save the absolute path for mlirlite's debug call-site recorder (.vpcall file paths must match DIFile: absolute + forward slash).
_g_abs_file_path = nbuf
# filename = whole path; directory = its prefix (both NUL-terminated, non-overlapping).
fname: str = nbuf
directory: str = ""
slash: str = string.strrchr(nbuf, '/')
if slash is not None:
dlen: sizet = string.strlen(nbuf) - string.strlen(slash + 1) - 1
dirbuf: str = pool.alloc(dlen + 1)
if dirbuf is not None:
string.strncpy(dirbuf, nbuf, dlen)
dirbuf[dlen] = '\0'
directory = dirbuf
dsize: sizet = string.strlen(fname) + string.strlen(directory) + 96
dbuf: str = pool.alloc(dsize)
if dbuf is None:
VLogger.error("--traceback: failed to allocate DIFile node buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(dbuf, dsize, "!DIFile(filename: \"%s\", directory: \"%s\")", fname, directory)
fid: int = mlirlite.module_add_di_node(mod, pool, dbuf)
if fid < 0:
VLogger.error("--traceback: failed to allocate DIFile metadata node, aborting compilation", "DWARF")
return 1
_g_file_id = fid
# types: !{ null } placeholder list, then DISubroutineType referencing it
nid: int = mlirlite.module_add_di_node(mod, pool, "!{ null }")
if nid < 0:
VLogger.error("--traceback: failed to allocate DISubroutineType type table, aborting compilation", "DWARF")
return 1
_g_null_id = nid
tbuf: str = pool.alloc(64)
if tbuf is None:
VLogger.error("--traceback: failed to allocate DISubroutineType buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(tbuf, 64, "!DISubroutineType(types: !%d)", _g_null_id)
tid: int = mlirlite.module_add_di_node(mod, pool, tbuf)
if tid < 0:
VLogger.error("--traceback: failed to allocate DISubroutineType metadata node, aborting compilation", "DWARF")
return 1
_g_subtype_id = tid
cbuf: str = pool.alloc(256)
if cbuf is None:
VLogger.error("--traceback: failed to allocate DICompileUnit buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(cbuf, 256, "distinct !DICompileUnit(language: DW_LANG_C99, file: !%d, producer: \"TPC\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)", _g_file_id)
cid: int = mlirlite.module_add_di_node(mod, pool, cbuf)
if cid < 0:
VLogger.error("--traceback: failed to allocate DICompileUnit metadata node, aborting compilation", "DWARF")
return 1
_g_cu_id = cid
return 0
# _LocRefAt - read a cached DILocation ref string pointer.
def _LocRefAt(slot: int) -> str:
slotp: str* = (str*)(uint64(_g_loc_ref) + sizet(slot) * 8)
r: str = slotp[0]
return r
# _EnsureLocation - DILocation ref for (sp, line, col), created + cached once.
def _EnsureLocation(pool: memhub.MemBuddy*, sp: int, line: int, col: int) -> str:
if line <= 0: return _g_cur_entry_ref
c: int = col
if c <= 0: c = 1
if _g_loc_used is None or _g_mod is None: return None
h: uint64 = (uint64(sp) * 1000003 + uint64(line) * 31 + uint64(c)) & uint64(DBG_LOC_SLOTS - 1)
pi: int = 0
while pi < DBG_LOC_PROBE:
slot: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1))
if _g_loc_used[slot] == 0: break
kbase: int = slot * DBG_LOC_KEY_STRIDE
if int(_g_loc_key[kbase]) == sp and int(_g_loc_key[kbase + 1]) == line and int(_g_loc_key[kbase + 2]) == c: return _LocRefAt(slot)
pi += 1
lbuf: str = pool.alloc(96)
if lbuf is None: return None
viperlib.snprintf(lbuf, 96, "!DILocation(line: %d, column: %d, scope: !%d)", line, c, sp)
lid: int = mlirlite.module_add_di_node(_g_mod, pool, lbuf)
if lid < 0:
VLogger.error("--traceback: failed to allocate DILocation metadata node, aborting compilation", "DWARF")
return None
lref: str = _RefOf(pool, lid)
if lref is None:
VLogger.error("--traceback: failed to allocate DILocation reference buffer, aborting compilation", "DWARF")
return None
if pi < DBG_LOC_PROBE:
slot2: int = int((h + uint64(pi)) & uint64(DBG_LOC_SLOTS - 1))
kbase2: int = slot2 * DBG_LOC_KEY_STRIDE
_g_loc_key[kbase2] = sizet(sp)
_g_loc_key[kbase2 + 1] = sizet(line)
_g_loc_key[kbase2 + 2] = sizet(c)
rslot: str* = (str*)(uint64(_g_loc_ref) + sizet(slot2) * 8)
rslot[0] = lref
_g_loc_used[slot2] = 1
return lref
# PushFunction - create the DISubprogram for one define.
def PushFunction(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, func: mlirlite.Function*, line: int, col: int) -> int:
"""Attach a DISubprogram to one define and set the builder's entry DILocation."""
global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth
if _g_enabled == 0: return 0
if pool is None or builder is None or func is None or _g_mod is None: return 0
if _g_depth >= DBG_STACK_MAX:
VLogger.error("--traceback: DWARF function nesting depth exceeds the limit, aborting compilation", "DWARF")
return 0
if _EnsureModuleNodes(pool) != 0: return 0
name: str = mlirlite.function_get_name(func)
if name is None:
VLogger.error("--traceback: function name is empty, cannot emit DISubprogram, aborting compilation", "DWARF")
return 0
dline: int = line
if dline <= 0: dline = 1
dcol: int = col
if dcol <= 0: dcol = 1
ssize: sizet = string.strlen(name) + 320
spbuf: str = pool.alloc(ssize)
if spbuf is None:
VLogger.error("--traceback: failed to allocate DISubprogram buffer, aborting compilation", "DWARF")
return 0
viperlib.snprintf(spbuf, ssize, "distinct !DISubprogram(name: \"%s\", scope: !%d, file: !%d, type: !%d, line: %d, scopeLine: %d, unit: !%d, spFlags: DISPFlagDefinition)", name, _g_file_id, _g_file_id, _g_subtype_id, dline, dline, _g_cu_id)
sp_id: int = mlirlite.module_add_di_node(_g_mod, pool, spbuf)
if sp_id < 0:
VLogger.error("--traceback: failed to allocate DISubprogram metadata node, aborting compilation", "DWARF")
return 0
sp_ref: str = _RefOf(pool, sp_id)
if sp_ref is None:
VLogger.error("--traceback: failed to allocate DISubprogram reference buffer, aborting compilation", "DWARF")
return 0
mlirlite.function_set_dbg(func, sp_ref)
# save the enclosing function's DI context (LIFO), then switch to this one
_g_stk_sp[_g_depth] = sizet(_g_cur_sp + 1)
rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8)
rslot[0] = _g_cur_entry_ref
bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8)
bslot[0] = _g_cur_builder
_g_depth += 1
_g_cur_sp = sp_id
_g_cur_builder = builder
entry_ref: str = _EnsureLocation(pool, sp_id, dline, dcol)
if entry_ref is None:
VLogger.error("--traceback: failed to generate entry DILocation, aborting compilation", "DWARF")
return 0
_g_cur_entry_ref = entry_ref
mlirlite.builder_set_debug_location(builder, entry_ref)
# Enter this define: tell mlirlite's call-site recorder the current symbol/file/def-line and write an F record (for the .vpcall file + WriteStackMap entry fallback line).
mlirlite.DebugNoteFunc(name, _g_abs_file_path, dline)
return 1
# PopFunction - restore the enclosing function's DI context.
def PopFunction(builder: mlirlite.IRBuilder*) -> int:
global _g_cur_sp, _g_cur_entry_ref, _g_cur_builder, _g_depth
if _g_enabled == 0: return 0
if _g_depth <= 0: return 0
_g_depth -= 1
_g_cur_sp = int(_g_stk_sp[_g_depth]) - 1
rslot: str* = (str*)(uint64(_g_stk_ref) + sizet(_g_depth) * 8)
_g_cur_entry_ref = rslot[0]
bslot: str* = (str*)(uint64(_g_stk_bld) + sizet(_g_depth) * 8)
_g_cur_builder = (mlirlite.IRBuilder*)(bslot[0])
if builder is not None: mlirlite.builder_clear_debug_location(builder)
# Leave this define: restore mlirlite's outer function context (for nested defines).
mlirlite.DebugClearFunc()
return 0
# SetStatement - point the current builder at a statement DILocation.
def SetStatement(pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, line: int, col: int) -> int:
"""Update the builder's DILocation to the current statement (line precision)."""
if _g_enabled == 0 or pool is None or builder is None: return 0
# Record the current statement line: C records from build_call in this statement use it as the call-site line.
mlirlite.DebugNoteLine(line)
if _g_cur_sp < 0: return 0
ref: str = _EnsureLocation(pool, _g_cur_sp, line, col)
if ref is None: return 0
mlirlite.builder_set_debug_location(builder, ref)
return 1
# _ModuleHasDefine - does the module emit any define (IsDeclared == 0)?
def _ModuleHasDefine(mod: mlirlite.LLVMModule*) -> int:
if mod is None: return 0
cur: mlirlite.Function* = mod.FuncHead
steps: int = 0
while cur is not None:
steps += 1
if steps > 100000: break
if mlirlite.function_is_declared(cur) == 0: return 1
cur = mlirlite.function_get_next(cur)
return 0
# Finalize - register !llvm.dbg.cu / !llvm.module.flags before dump_ir.
def Finalize(pool: memhub.MemBuddy*) -> int:
"""Module-level DI tail; idempotent per module, no-op when no SP was emitted."""
global _g_finalized
if _g_enabled == 0: return 0
if _g_finalized != 0: return 0
mod: mlirlite.LLVMModule* = _g_mod
if mod is None or pool is None: return 0
_g_finalized = 1
# Self-check (mirrors TPC Phase2Translator.py:3444): a module that emits a
# define but carries no DI at all loses line info for its frames.
if mod.DIMetaCount == 0 and _ModuleHasDefine(mod) != 0: VLogger.warning("--traceback: this module emits a define but its IR carries no DWARF (!llvm.dbg.cu); its frames will show only addresses in the Ctrl+C stack", "DWARF")
if _g_cu_id < 0: return 0
cu_ref: str = _RefOf(pool, _g_cu_id)
if cu_ref is None:
VLogger.error("--traceback: failed to allocate DICompileUnit reference buffer, aborting compilation", "DWARF")
return 1
mlirlite.module_add_named_metadata(mod, pool, "llvm.dbg.cu", cu_ref)
fbuf: str = pool.alloc(64)
if fbuf is None:
VLogger.error("--traceback: failed to allocate module-level debug flags buffer, aborting compilation", "DWARF")
return 1
viperlib.snprintf(fbuf, 64, "!{i32 2, !\"Debug Info Version\", i32 3}")
flags_id: int = mlirlite.module_add_di_node(mod, pool, fbuf)
if flags_id < 0:
VLogger.error("--traceback: failed to allocate llvm.module.flags metadata node, aborting compilation", "DWARF")
return 1
fl_ref: str = _RefOf(pool, flags_id)
if fl_ref is None:
VLogger.error("--traceback: failed to allocate llvm.module.flags reference buffer, aborting compilation", "DWARF")
return 1
mlirlite.module_add_named_metadata(mod, pool, "llvm.module.flags", fl_ref)
return 0
+80 -4
View File
@@ -439,7 +439,13 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
submod_buf: str = pool.alloc(submod_len)
if submod_buf is not None:
viperlib.snprintf(submod_buf, submod_len, "%s.%s", nm_ma.id, at.attr)
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, submod_buf) == 1: is_submod = 1
# Imported package submodule (e.g. `import os` then `os.path.x`): the
# submodule must be recognized even when the current file only imported
# the package. `is_module_imported` alone misses it, so `path` was
# treated as a data global of `os` (`@<os_sha>.path`), emitted a dead
# addressof/load, and the linker reported the symbol as undefined
# (`os/__init__` only aliases the submodule, it never defines it).
if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, submod_buf) == 1: is_submod = 1
# also check from_imports: the at.attr alias maps to the real module win32.win32file
if is_submod == 0 and trans._from_imports is not None:
fi_mod_raw: str = self.Gen.ImportsGen.lookup_from_import(trans._from_imports, at.attr, 0)
@@ -609,6 +615,12 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
if obj_ptr is None or obj_ptr.Ty is None: return None
# Aggregate rvalue base (e.g. `make().field` / `a.make().field`): the translated base is a
# struct/array SSA value, not an address, so GEP cannot be applied directly. Materialize it
# into a frame temporary and continue on the address path below.
obj_ptr = self._attr_base_as_ptr(builder, obj_ptr)
if obj_ptr is None or obj_ptr.Ty is None: return None
# if obj_ptr is Ptr(Ptr(Struct)) (the alloca of an X* variable), load to deref and get Ptr(Struct)
obj_ptr = self._deref_if_ptr_ptr(builder, obj_ptr)
@@ -794,6 +806,62 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
# explicit return after fatal_error: the final match arm must be terminated, else falling off
# the end is misjudged by the function tail as fail-fast
return None
# _attr_base_as_ptr - make an attribute base addressable: aggregate rvalues (call results,
# literals, temporaries) are spilled to a frame alloca; pointers pass through unchanged.
def _attr_base_as_ptr(self, builder: mlirlite.IRBuilder*, obj_ptr: mlirlite.Value*) -> mlirlite.Value*:
"""Spill a non-pointer aggregate base into a frame temp; return its address."""
if obj_ptr is None or obj_ptr.Ty is None: return obj_ptr
if self.is_ptr_type(obj_ptr.Ty) != 0: return obj_ptr
bk: int = obj_ptr.Ty.kind()
if bk != mlirlite.LLVMType.Struct and bk != mlirlite.LLVMType.Array: return obj_ptr
tmp: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, obj_ptr.Ty)
if tmp is None: return None
mlirlite.build_store(builder, obj_ptr, tmp)
return tmp
# _call_ret_class_name - class name of a call's return type (`f().field` chains).
# A normal function call is not a pointer cast: using the callee name (`make_o`) as the class
# makes lookup_field_by_class search for a class named after the function instead of the
# struct the call actually returns.
def _call_ret_class_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, call_node: ast.AST*) -> str:
"""Class name of a call's return struct/class, or None."""
if pool is None or trans is None or call_node is None: return None
if call_node.kind() != ast.ASTKind.Call: return None
cl_cr: ast.Call* = ast.Call*(call_node)
if cl_cr is None or cl_cr.func is None: return None
fname_cr: str = None
mod_sha1_cr: str = None
if cl_cr.func.kind() == ast.ASTKind.Name:
nm_cr: ast.Name* = ast.Name*(cl_cr.func)
fname_cr = nm_cr.id
mod_sha1_cr = trans.ModuleSha1
elif cl_cr.func.kind() == ast.ASTKind.Attribute:
at_cr: ast.Attribute* = ast.Attribute*(cl_cr.func)
fname_cr = at_cr.attr
if at_cr.value is not None:
if at_cr.value.kind() == ast.ASTKind.Name:
mn_cr: ast.Name* = ast.Name*(at_cr.value)
if mn_cr.id is not None and self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mn_cr.id) != 0:
mod_sha1_cr = self.Gen.CallGen._lookup_module_sha1(mn_cr.id)
elif at_cr.value.kind() == ast.ASTKind.Attribute:
mp_cr: str = self._attr_module_path_str(pool, at_cr.value)
if mp_cr is not None and self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mp_cr) != 0:
mod_sha1_cr = self.Gen.CallGen._lookup_module_sha1(mp_cr)
if fname_cr is None or mod_sha1_cr is None: return None
f_cr: mlirlite.Function* = None
sym_len_cr: sizet = string.strlen(mod_sha1_cr) + string.strlen(fname_cr) + 2
sym_buf_cr: str = pool.alloc(sym_len_cr)
if sym_buf_cr is not None:
viperlib.snprintf(sym_buf_cr, sym_len_cr, "%s.%s", mod_sha1_cr, fname_cr)
f_cr = self.Gen.CallGen.find_func_in_module(trans.Module, sym_buf_cr)
if f_cr is None: f_cr = self.Gen.CallGen.find_func_in_module(trans.Module, fname_cr)
if f_cr is None: return None
ret_cr: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(f_cr)
if ret_cr is None: return None
# class-instance return (`-> T` is T* at the ABI): take the pointee struct name
if self.is_ptr_type(ret_cr) != 0 and ret_cr.Pointee is not None: ret_cr = ret_cr.Pointee
return self.Gen.StructGen.get_class_name_by_type(pool, ret_cr)
# get_attribute_ptr - get the field pointer for an attribute access (no value load, for assignment);
# for a Name obj, use the alloca pointer directly (no struct load).
def _resolve_attr_field_class(self, pool: memhub.MemBuddy*, trans: HT.Translator*, attr_node: ast.AST*, allow_module_global: int = 0) -> str:
@@ -818,9 +886,12 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
parent_class = self.Gen.VarGen.var_annot_class_name(var_entry)
elif at.value.kind() == ast.ASTKind.Attribute: parent_class = self._resolve_attr_field_class(pool, trans, at.value, allow_module_global)
elif at.value.kind() == ast.ASTKind.Call:
# pointer-cast base (the recursive parent of `(T*(x)).f.g`): extract the class name from the
# cast annotation (see the _base_class_from_cast_base header)
parent_class = self._base_class_from_cast_base(pool, trans, at.value)
# Pointer-cast base OR normal call. A cast/constructor names a type (`T*(x)`, `T(...)`),
# but a function call's class must come from its RETURN type: using the callee name
# (`make_o`) makes lookup_field_by_class search for a class named after the function
# (chained `a.make_o().inner.z`).
parent_class = self._call_ret_class_name(pool, trans, at.value)
if parent_class is None: parent_class = self._base_class_from_cast_base(pool, trans, at.value)
else: return None
if parent_class is None: return None
# look up the at.attr field in the parent class and return its annotation class name
@@ -1057,6 +1128,11 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
if obj_ptr is None or obj_ptr.Ty is None: return None
# Aggregate rvalue base (same rule as the read path): spill it to a frame temporary so
# `f().field = v` resolves the field address instead of failing on a non-pointer base.
obj_ptr = self._attr_base_as_ptr(builder, obj_ptr)
if obj_ptr is None or obj_ptr.Ty is None: return None
# if obj_ptr is Ptr(Ptr(Struct)) (the alloca of an X* variable), load to deref and get Ptr(Struct)
obj_ptr = self._deref_if_ptr_ptr(builder, obj_ptr)
+35 -1
View File
@@ -524,7 +524,41 @@ novirtual class ExprGenBase:
at: ast.Attribute* = ast.Attribute*(cl.func)
callee = at.attr
if callee is None: return None
ent: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(callee)
# Module-aware resolution: the ctor dispatch (CallGenMixin) already resolves the struct
# by module SHA1. A bare find_struct here returns the FIRST same-named struct (possibly
# from another module), so this "construction materialization" type would disagree with
# the value the ctor actually produced and `p: T = T()` would fail with a CastError
# whenever the same-named T is defined in another imported module.
ent: GenTypes.StructEntry* = None
if trans is not None:
if cl.func.kind() == ast.ASTKind.Name:
if trans.ModuleSha1 is not None: ent = self.Gen.StructGen.find_struct_by_module(callee, trans.ModuleSha1)
if ent is None and trans._from_imports is not None:
# from X import T (or `as Alias`): resolve through the from-import table so the
# defining module's T wins over a same-named struct from another module.
fi_sha: str = self.Gen.CallGen._lookup_mod_sha1(trans.Pool, trans._from_imports, callee, 1)
if fi_sha is not None:
fi_name: str = callee
fi_orig: str = self.Gen.ImportsGen.lookup_from_import_original(trans._from_imports, callee)
if fi_orig is not None:
fi_len: sizet = 0
while fi_orig[fi_len] != ' ' and fi_orig[fi_len] != '\0': fi_len += 1
fi_buf: str = trans.Pool.alloc(fi_len + 1)
if fi_buf is not None:
for fi_i in range(int(fi_len)): fi_buf[fi_i] = fi_orig[fi_i]
fi_buf[fi_len] = '\0'
fi_name = fi_buf
ent = self.Gen.StructGen.find_struct_by_module(fi_name, fi_sha)
else:
at_c: ast.Attribute* = ast.Attribute*(cl.func)
mod_path_c: str = None
if at_c.value is not None:
if at_c.value.kind() == ast.ASTKind.Name: mod_path_c = (ast.Name*(at_c.value)).id
elif at_c.value.kind() == ast.ASTKind.Attribute: mod_path_c = self.Gen.ExprGenAttr._attr_module_path_str(trans.Pool, at_c.value)
if mod_path_c is not None and self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mod_path_c) != 0:
mod_sha1_c: str = self.Gen.CallGen._lookup_module_sha1(mod_path_c)
if mod_sha1_c is not None: ent = self.Gen.StructGen.find_struct_by_module(callee, mod_sha1_c)
if ent is None: ent = self.Gen.StructGen.find_struct(callee)
if ent is not None and ent.Ty is not None: return ent.Ty
# REnum variant ctor `EnumType.Variant(...)`: the callee is not a struct
# name, but materializes the base enum, so return that aggregate type.
+3 -3
View File
@@ -20,10 +20,10 @@ define FG_SCOPE_FUNCTION = 1
novirtual class FuncGenBase(CallGenBase.CallGenBase):
"""Shared helpers for function attributes, naming, mangling and creation."""
# extract_func_attrs - extract c.Attribute attrs from decorator_list.
# Supports @c.Attribute(t.attr.xxx()/t.attr.xxx/t.attr.llvm.xxx); returns LLVM IR attr string or None.
# extract_func_attrs - extract Attribute attrs from decorator_list.
# Supports @Attribute(attr.xxx()/attr.xxx/attr.llvm.xxx); returns LLVM IR attr string or None.
def extract_func_attrs(self, pool: memhub.MemBuddy*, decorator_list: list[ast.AST*]*, imported_modules: str) -> str:
"""Extract c.Attribute attrs from decorators; return the LLVM IR attr string."""
"""Extract Attribute attrs from decorators; return the LLVM IR attr string."""
if decorator_list is None: return None
dn: sizet = len(decorator_list)
if dn == 0: return None
+48 -6
View File
@@ -683,6 +683,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
if ags.args is not None:
alist: list[ast.AST*]* = ags.args
an: sizet = len(alist)
p_idx: int = 0
for ai in range(int(an)):
arg: ast.Arg* = ast.Arg*(alist.get(ai))
if arg is None or arg.arg is None: continue
@@ -698,7 +699,8 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
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); ArrayT 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.
# Exception (keep in sync in 3 places): C ABI extern bare struct params use the
# C ABI type: Win64 iN for 1/2/4/8 bytes, otherwise Ptr(struct) (byref copy).
match resolved:
case mlirlite.LLVMType.Void():
# void is not a legal param type (void is only for void* and typedef of an opaque struct);
@@ -708,7 +710,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
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
param_ty = self.Gen.CallGenBase.extern_c_param_agg_ty(pool, resolved)
# Win64 byref: the call site must pass a copy, not the variable address.
if param_ty is not None and param_ty.kind() == mlirlite.LLVMType.Ptr:
self.Gen.CallGenBase.register_extern_byref_param(mangled_name, p_idx)
else: param_ty = mlirlite.Ptr(pool, resolved)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
@@ -717,6 +722,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
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)
p_idx += 1
# *args variadic: set IsVarArg=1 to emit the ... suffix
if ags.vararg is not None: func.IsVarArg = 1
@@ -737,6 +743,11 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
fd: ast.FunctionDef* = ast.FunctionDef*(node)
if fd is None or fd.name is None: return 0
# Reject unknown decorators instead of silently dropping them (e.g. the retired
# `@c.Attribute(...)` spelling). Runs before the generic/async early returns so every
# function form is checked.
self.Gen.ClassGenBase.validate_decorators(trans, trans.Module, trans.Pool, fd.name, "function", fd.decorator_list, 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:
@@ -878,6 +889,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
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:
# C extern returning an aggregate: remember the symbol so call sites emit the C ABI
# (integer register / hidden sret) instead of Viper's aggregate-return convention.
if ret_ty is not None and ret_ty.kind() == mlirlite.LLVMType.Struct:
self.Gen.CallGenBase.register_extern_agg(mangled_name)
# extern <lib> name(...):runtime=True 在这里也生成 thunk(Phase A 的 extern 往往不走
# forward_declare_functions,两处都挂才覆盖全部路径;另一处靠 find_func_in_module 去重)。
if fd.ffi_lib is not None:
@@ -917,6 +932,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
if ags_e.args is not None:
alist_e: list[ast.AST*]* = ags_e.args
an_e: sizet = len(alist_e)
p_idx_e: int = 0
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:
@@ -932,7 +948,8 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
resolved_e = i32_ty # unreachable (fatal_error already exited)
if resolved_e is not None:
# Bare class param -> Ptr(struct); ArrayT 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).
# always a C ABI extern declare: bare struct params use the C ABI type (Win64 iN for
# 1/2/4/8 bytes, otherwise Ptr(struct) as a byref copy; 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)
@@ -940,7 +957,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
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
param_ty_e = self.Gen.CallGenBase.extern_c_param_agg_ty(pool, resolved_e)
# Win64 byref: the call site must pass a copy, not the variable address.
if param_ty_e is not None and param_ty_e.kind() == mlirlite.LLVMType.Ptr:
self.Gen.CallGenBase.register_extern_byref_param(mangled_name, p_idx_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)
@@ -949,8 +969,9 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
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)
p_idx_e += 1
# Extract @c.Attribute decorator attributes and set them on the function
# Extract @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
@@ -1010,7 +1031,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
VLogger.error(f"create_function {fd.name} failed", "FUNC")
return 0
# Extract @c.Attribute decorator attributes and set them on the function
# Extract @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="...")
@@ -1074,6 +1095,27 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# (refined per statement); no-op when DI is disabled.
dbg_pushed: int = DebugInfoGen.PushFunction(pool, func_builder, func, fd.lineno, fd.col_offset + 1)
# Constructors may be invoked on a null object when __new__ failed (pool exhausted).
# __before_init__ is null-safe; __init__ must be too, or its first field store is an
# access violation (ASan: LspRange.__init__ in the LSP semantic-token path).
if fd.name is not None and string.strcmp(fd.name, "__init__") == 0:
init_sp: mlirlite.Param* = mlirlite.function_get_param_head(func)
if init_sp is not None:
init_sp_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(init_sp)
init_sp_nm: char* = mlirlite.param_get_name(init_sp)
if init_sp_ty is not None and init_sp_nm is not None and string.strcmp(init_sp_nm, "%self") == 0:
init_self: mlirlite.Value* = mlirlite.SSAValue(pool, init_sp_ty, init_sp_nm)
init_null: mlirlite.Value* = mlirlite.ConstNull(pool, init_sp_ty, "null")
init_null_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.selfnull")
init_body_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "init.body")
if init_self is not None and init_null is not None and init_null_bb is not None and init_body_bb is not None:
init_is_null: mlirlite.Value* = mlirlite.build_icmp(func_builder, mlirlite.ICMP_EQ, init_self, init_null)
if init_is_null is not None:
mlirlite.build_cond_br(func_builder, init_is_null, init_null_bb, init_body_bb)
mlirlite.position_at_end(func_builder, init_null_bb)
mlirlite.build_ret_void(func_builder)
mlirlite.position_at_end(func_builder, init_body_bb)
# @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:
+40
View File
@@ -453,6 +453,46 @@ def get_enum_base_type_compat(enum_name: str) -> mlirlite.LLVMType*:
return None
def get_enum_base_type_in_module(enum_name: str, sha1: str) -> mlirlite.LLVMType*:
"""按 (名字, 定义模块 SHA1) 取枚举基类型:跨模块同名枚举按模块区分,避免取到先登记的同名项。"""
if _g_gen is not None: return _g_gen.StmtGenDecl.get_enum_base_type_in_module(enum_name, sha1)
return None
# ---------------- C-extern aggregate return registry ----------------
# A C function returning a struct follows the C ABI (Win64: RAX integer <=8B, hidden sret
# pointer otherwise), unlike Viper's own aggregate return. Register the bare symbol when the
# extern declare is created; the call site consults it before emitting a direct call.
def register_extern_agg(sym: str) -> int:
"""Delegate to CallGenBase.register_extern_agg (idempotent)."""
if _g_gen is not None: return _g_gen.CallGenBase.register_extern_agg(sym)
return 0
def is_extern_agg(sym: str) -> int:
"""Delegate to CallGenBase.is_extern_agg."""
if _g_gen is not None: return _g_gen.CallGenBase.is_extern_agg(sym)
return 0
def is_win64_c_abi() -> int:
"""Delegate to CallGenBase.is_win64_c_abi."""
if _g_gen is not None: return _g_gen.CallGenBase.is_win64_c_abi()
return 1
def extern_c_param_agg_ty(pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""Delegate to CallGenBase.extern_c_param_agg_ty (C ABI by-value aggregate parameter type)."""
if _g_gen is not None: return _g_gen.CallGenBase.extern_c_param_agg_ty(pool, ty)
return ty
def register_extern_byref_param(sym: str, index: int) -> int:
"""Delegate to CallGenBase.register_extern_byref_param."""
if _g_gen is not None: return _g_gen.CallGenBase.register_extern_byref_param(sym, index)
return 0
# ---------------- add_visible_struct (delegates to StructGenMixin.add_visible_struct) ----------------
def add_visible_struct(pool: memhub.MemBuddy*, name: str):
"""Delegate to gen.add_visible_struct"""
+21 -2
View File
@@ -31,6 +31,12 @@ define FIELD_NAME_MAX = 64
define PARENT_MAX = 64
define MRO_MAX = 64
# ============================================================
# Enum capacities
# 单个枚举的成员数上限。唯一定义点:ClassGenEnum 的翻译期暂存数组、StmtGenDeclMixin 的
# EnumEntry.Members 预分配、VpiReader 的 .vpi 预扫描都引用这一个值,禁止再写本地副本。
define ENUM_MEMBER_MAX = 256
# ============================================================
# for-iteration sequence kinds (formerly HandlesFor.SEQ_KIND_*)
define SEQ_KIND_RANGE = 1
@@ -150,8 +156,8 @@ novirtual class StructEntry:
FieldCount: int # Field count
Fields: FieldEntry* # Field array (FIELD_MAX slots)
IsUnion: int # 1=union / 0=normal struct
IsPacked: int # 1=@c.Attribute(t.attr.packed) packed struct (no padding)
Align: int # @c.Attribute(t.attr.aligned(N)) alignment in bytes (0=default)
IsPacked: int # 1=@Attribute(attr.packed) packed struct (no padding)
Align: int # @Attribute(attr.aligned(N)) alignment in bytes (0=default)
IsOOP: int # 1=OOP struct (has methods) / 0=plain memory struct
HasInit: int # 1=has __init__ / 0=no
HasNew: int # 1=has __new__ / 0=no
@@ -224,6 +230,19 @@ define FUNC_RET_SIGN_UNSIGNED = 1
define FUNC_RET_SIGN_SIGNED = 2
# Snapshot kinds beyond ast.CONST_* (1..6):
# CONST_AGG_DEFAULT - aggregate literal (struct constructor with scalar arguments); the
# encoded field tokens live in FuncDefaultsEntry.ConstStrs[di].
# CONST_UNKNOWN_DEFAULT - a default that cannot be rebuilt off-module; the call site fails
# fast instead of silently passing a null/uninitialized value.
define CONST_AGG_DEFAULT = 100
define CONST_UNKNOWN_DEFAULT = 101
# CONST_EXPR_TEXT - name/qualified-name default (enum member, CDefine); the text lives
# in ConstStrs[di] and is resolved at the call site (the .vpi pre-scan
# runs before enums are registered).
define CONST_EXPR_TEXT = 102
# ============================================================
# FuncDefaultsEntry - default-argument entry (formerly HandlesExprCall.FuncDefaultsEntry)
novirtual class FuncDefaultsEntry:
+18 -5
View File
@@ -22,8 +22,8 @@ import lib.core.Gen.GenApi as GenApi
# ============================================================
# Constants
# 枚举表容量:键是 (定义模块 SHA1, 名字),同名枚举按模块各占一条(不再合并),故留足余量
# ENUM_MEMBER_MAX(单枚举成员上限)唯一定义在 GenTypes,与 ClassGenEnum 暂存数组和 .vpi 预扫描共用。
define ENUM_MAX = 256
define ENUM_MEMBER_MAX = 128
# ============================================================
# StmtGenDeclMixin - declaration/enum/nonlocal (mid layer)
@@ -123,6 +123,19 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin):
if k == ast.ASTKind.AnnAssign: return self.Gen.AssignGen.translate_annassign_prescan(trans, node)
if k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(node)
# cif/celif: only prescan the compile-time-selected branch, otherwise a pruned
# branch's platform-specific annotations (e.g. win32 types on a Linux target)
# would still be resolved and fail the build.
if self._is_cif_call(ifn.test) == 1:
cif_val_pre: int = self._prune_cif_test(trans.Pool, trans, ifn.test)
if cif_val_pre != 0:
if ifn.children is not None:
bn_pre_ci: sizet = len(ifn.children)
for bi_pre_ci in range(int(bn_pre_ci)): self.pre_scan_allocas(trans, ifn.children.get(bi_pre_ci))
elif ifn.orelse is not None:
bn_pre_ce: sizet = len(ifn.orelse)
for bi_pre_ce in range(int(bn_pre_ce)): self.pre_scan_allocas(trans, ifn.orelse.get(bi_pre_ce))
return 0
if ifn.children is not None:
bn_pre_i: sizet = len(ifn.children)
for bi_pre_i in range(int(bn_pre_i)): self.pre_scan_allocas(trans, ifn.children.get(bi_pre_i))
@@ -364,7 +377,7 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin):
def _get_enum_member(self, enum_entry: GenTypes.EnumEntry*, i: int) -> GenTypes.EnumMember*:
"""Get the i-th enum member entry"""
if enum_entry is None or i < 0 or i >= ENUM_MEMBER_MAX: return None
if enum_entry is None or i < 0 or i >= GenTypes.ENUM_MEMBER_MAX: return None
member_size: sizet = GenTypes.EnumMember.__sizeof__()
addr: uint64 = uint64(enum_entry.Members) + i * member_size
return GenTypes.EnumMember*(addr)
@@ -392,9 +405,9 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin):
# Allocate the member array (permanent pool: survives mark/reset like EnumTable)
member_size: sizet = GenTypes.EnumMember.__sizeof__()
entry.Members = CompilerFactory.factory.PermPool.alloc(member_size * ENUM_MEMBER_MAX)
entry.Members = CompilerFactory.factory.PermPool.alloc(member_size * GenTypes.ENUM_MEMBER_MAX)
if entry.Members is None: return None
string.memset(entry.Members, 0, member_size * ENUM_MEMBER_MAX)
string.memset(entry.Members, 0, member_size * GenTypes.ENUM_MEMBER_MAX)
# Copy the class name (permanent-pool deep copy)
name_len: sizet = string.strlen(name)
@@ -435,7 +448,7 @@ novirtual class StmtGenDeclMixin(StmtGenCifMixin.StmtGenCifMixin):
if ex_d is not None and ex_d.Name is not None:
if string.strcmp(ex_d.Name, member_name) == 0: return ei_d
if enum_entry.MemberCount >= ENUM_MEMBER_MAX:
if enum_entry.MemberCount >= GenTypes.ENUM_MEMBER_MAX:
VLogger.error(f"member table full for {enum_entry.Name}", "ENUM")
return -1
+1 -1
View File
@@ -777,7 +777,7 @@ novirtual class TranslatorGenMixin:
# 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.
# B5: apply alignment to global instances of @Attribute(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:
+349 -388
View File
@@ -1,388 +1,349 @@
from stdint import *
import memhub
import string
import stdlib
import win32.win32file as win32file
import win32.win32base as win32base
import io
import hashlib
import viperlib
import lib.Projectrans.Utils as Utils
import lib.core.VLogger as VLogger
# IncludesScanner - recursive scan of the includes directory (Win32 FindFirstFileA/
# FindNextFileA); collects .vp paths and SHA1s for Phase1 stub gen / Phase2 merge.
# global mbuddy pointer
_mbuddy: memhub.MemBuddy*
# max files per scan
define MAX_FILES = 1024
# FileEntry - file entry (path + SHA1)
novirtual class FileEntry:
"""A scanned file entry."""
Path: str # absolute file path
Sha1: str # first 16 SHA1 chars (16 bytes + null)
RelPath: str # path relative to the includes root
ModuleName: str # module name (e.g. "ast.parser")
IncDir: str # owning includes root dir (multi-root support)
# ScanResult - scan result
novirtual class ScanResult:
"""A collection of scan results."""
Entries: FileEntry* # FileEntry array
Count: int
Capacity: int
# create_scan_result - create the scan result container
def create_scan_result(pool: memhub.MemBuddy*) -> ScanResult*:
"""Create the scan result container with MAX_FILES preallocated slots."""
if pool is None: return None
size: sizet = MAX_FILES * FileEntry.__sizeof__()
# use stdlib.malloc to avoid exhausting the mbuddy pool (consistent with StubMerger)
entries: FileEntry* = stdlib.malloc(size)
if entries is None: return None
string.memset(entries, 0, size)
result: ScanResult* = stdlib.malloc(ScanResult.__sizeof__())
if result is None: return None
string.memset(result, 0, ScanResult.__sizeof__())
result.Entries = entries
result.Count = 0
result.Capacity = MAX_FILES
return result
# add_file_entry - append a file entry to the scan result
def add_file_entry(result: ScanResult*, pool: memhub.MemBuddy*, abs_path: str, rel_path: str, sha1: str, inc_dir: str) -> int:
"""Append a file entry to the scan result; inc_dir is the owning includes root."""
if result is None or pool is None: return 1
if result.Count >= result.Capacity: return 1
# compute module name: '/' -> '.', strip .vp/.py (Utils handles __init__.vp)
mod_buf: str = Utils.PathToModuleName(rel_path, 0)
if mod_buf is None: return 1
# get the entry address
entry_size: sizet = FileEntry.__sizeof__()
entry_addr: uint64 = uint64(result.Entries) + result.Count * entry_size
entry: FileEntry* = (FileEntry*)(entry_addr)
if entry is None: return 1
# copy the path string
abs_len: sizet = string.strlen(abs_path)
abs_buf: str = stdlib.malloc(abs_len + 1)
if abs_buf is None: return 1
string.strcpy(abs_buf, abs_path)
entry.Path = abs_buf
# copy the SHA1
sha1_buf: str = stdlib.malloc(17)
if sha1_buf is None: return 1
string.strcpy(sha1_buf, sha1)
entry.Sha1 = sha1_buf
# copy the relative path
rel_buf: str = stdlib.malloc(string.strlen(rel_path) + 1)
if rel_buf is None: return 1
string.strcpy(rel_buf, rel_path)
entry.RelPath = rel_buf
# copy the module name
entry.ModuleName = mod_buf
# copy the owning includes root dir (used by Phase2 path joining)
if inc_dir is not None:
inc_len: sizet = string.strlen(inc_dir)
inc_buf: str = stdlib.malloc(inc_len + 1)
if inc_buf is None: return 1
string.strcpy(inc_buf, inc_dir)
entry.IncDir = inc_buf
result.Count += 1
return 0
# compute_file_sha1 - read file content and compute SHA1 (CRLF->LF, matching Projectrans.py)
def compute_file_sha1(pool: memhub.MemBuddy*, file_path: str) -> str:
"""Read a file, normalize CRLF to LF, and return the first 16 SHA1 hex chars."""
if pool is None or file_path is None: return None
# read file content; BUF_SIZE must match SRC_BUF_SIZE (1MB) or trailing edits won't change SHA1
BUF_SIZE: sizet = 1048576
# Refuse larger files: hashing only the first 1 MiB gives two files that differ later the same
# SHA1, and the SHA1 keys the stub/cache files (colliding modules would reuse each other's IR).
sf_sz: io.File* = io.File(file_path, io.MODE.R)
if sf_sz.closed: return None
too_big: int = 0
if sf_sz.size() >= BUF_SIZE: too_big = 1
sf_sz.close()
if too_big != 0:
VLogger.error(f"source file exceeds the {BUF_SIZE}-byte limit and cannot be hashed completely: {file_path}", "IncludesScanner")
return None
buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE)
if buf is None: return None
# strip \r in place (CRLF -> LF), matching Projectrans.py text-mode reads
buf_len: sizet = string.strlen(buf)
write_pos: sizet = 0
for ch in buf:
if ch != '\r':
buf[write_pos] = ch
write_pos += 1
# add a null terminator
buf[write_pos] = 0
# compute SHA1
ctx: hashlib.sha1* = hashlib.sha1()
if ctx is None:
stdlib.free(buf)
return None
ctx.update(buf)
digest: bytes = stdlib.malloc(hashlib.SHA1_DIGEST_LEN)
if digest is None:
stdlib.free(buf)
return None
ctx.final(digest)
# convert to hex (first 8 bytes = 16 hex chars; stdlib allocation unchanged)
hex_buf: str = Utils.bytes_to_hex(pool, digest, 8, 0, 1)
stdlib.free(buf)
stdlib.free(digest)
return hex_buf
# scan_directory_recursive - recursively scan a directory (FindFirstFileA/FindNextFileA)
def scan_directory_recursive(pool: memhub.MemBuddy*, root_dir: str, rel_prefix: str, result: ScanResult*, inc_dir: str) -> int:
"""Recursively collect .vp/.py files under a directory; inc_dir is the includes root."""
if pool is None or root_dir is None or result is None: return 1
# subdir recursion failure count (not ignored; fail-fast at the summary)
subdir_failed: int = 0
# build search pattern: root_dir/*
root_len: sizet = string.strlen(root_dir)
pattern: bytes = stdlib.malloc(root_len + 4)
if pattern is None: return 1
viperlib.snprintf(pattern, root_len + 4, "%s/*", root_dir)
# find_data buffer +16 slack: VPC's WIN32_FIND_DATAA.__sizeof__() undercounts Windows writes
fd_size_s: sizet = win32file.WIN32_FIND_DATAA.__sizeof__()
find_data: win32file.WIN32_FIND_DATAA* = stdlib.malloc(fd_size_s + 16)
if find_data is None: return 1
string.memset(find_data, 0, fd_size_s + 16)
handle: win32base.HANDLE = win32file.FindFirstFileA(pattern, find_data)
if handle == win32base.INVALID_HANDLE_VALUE: return 1
# iterate all files and subdirectories
while True:
# skip . and ..
fname: str = find_data.cFileName
if fname is not None:
fname0: char = fname[0]
if fname0 == '.':
fname1: char = fname[1]
if fname1 == '\0':
# "."
if win32file.FindNextFileA(handle, find_data) == 0: break
continue
elif fname1 == '.':
fname2: char = fname[2]
if fname2 == '\0':
# ".."
if win32file.FindNextFileA(handle, find_data) == 0: break
continue
# check whether it is a directory
is_dir: int = find_data.dwFileAttributes & win32file.FILE_ATTRIBUTE_DIRECTORY
# build the full path
fname_len: sizet = string.strlen(fname)
full_path: bytes = Utils.JoinPath(root_dir, fname)
if full_path is None: break
# build the relative path
prefix_len: sizet = 0
if rel_prefix is not None: prefix_len = string.strlen(rel_prefix)
rel_path: bytes = None
if rel_prefix is not None and prefix_len > 0: rel_path = Utils.JoinPath(rel_prefix, fname)
else:
rel_path = stdlib.malloc(fname_len + 1)
if rel_path is None: break
string.strcpy(rel_path, fname)
if rel_path is None: break
if is_dir != 0:
# recurse into the subdirectory
if scan_directory_recursive(pool, full_path, rel_path, result, inc_dir) != 0: subdir_failed += 1
else:
# check for a .vp source file (Viper uses .vp only; no longer .py)
is_py: int = 0
if Utils.is_vp_at(fname, 0, fname_len) != 0: is_py = 1
if is_py != 0:
# skip files under __pycache__
is_pycache: int = 0
if rel_prefix is not None:
if string.strcmp(rel_prefix, "__pycache__") == 0: is_pycache = 1
if is_pycache == 0:
# compute SHA1
sha1: str = compute_file_sha1(pool, full_path)
if sha1 is not None:
# fail-fast: a full table used to drop the file silently, so it never got a
# SHA1 entry / dependency edge and was therefore never compiled.
if add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) != 0:
VLogger.error(f"includes scan could not register '{full_path}' (the {MAX_FILES}-file capacity or an allocation failed); raise MAX_FILES", "scan")
VLogger.debug(f"scan: {rel_path} -> {sha1}", "scan")
else: VLogger.warning(f"SHA1 computation failed: {full_path}", "scan")
# continue to the next file
if win32file.FindNextFileA(handle, find_data) == 0: break
win32file.FindClose(handle)
if subdir_failed > 0: VLogger.error(f"subdirectory scan failed for {subdir_failed} entries", "scan")
return 0
# scan_includes - scan entry point: return all .vp file paths and SHA1s
def scan_includes(pool: memhub.MemBuddy*, includes_dirs: str) -> ScanResult*:
"""Scan space-separated includes dirs and return all scanned .vp/.py files."""
if pool is None or includes_dirs is None: return None
result: ScanResult* = create_scan_result(pool)
if result is None: return None
# parse space-separated includes dirs, recursively scanning and merging each
dirs_total: sizet = string.strlen(includes_dirs)
d_i: sizet = 0
while d_i < dirs_total:
while d_i < dirs_total and includes_dirs[d_i] == ' ': d_i += 1
if d_i >= dirs_total: break
d_start: sizet = d_i
while d_i < dirs_total and includes_dirs[d_i] != ' ': d_i += 1
d_len: sizet = d_i - d_start
if d_len == 0: continue
dir_buf: bytes = stdlib.malloc(d_len + 1)
if dir_buf is None: VLogger.error("failed to allocate the includes directory; aborting scan", "scan")
string.strncpy(dir_buf, includes_dirs + d_start, d_len)
dir_buf[d_len] = '\0'
scan_directory_recursive(pool, dir_buf, None, result, dir_buf)
stdlib.free(dir_buf)
return result
# _PathExistsA - path existence check (GetFileAttributesA)
def _PathExistsA(path: str) -> int:
"""Return 1 if the path exists, else 0, using GetFileAttributesA."""
if path is None: return 0
attr: ULONG = win32file.GetFileAttributesA(path)
if attr == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES
return 0
return 1
# _TryResolveModuleFile - try one candidate path and write outputs (1=hit, 0=miss);
# out_abs_path/out_rel_path/out_inc_dir are rawptr output slots.
def _TryResolveModuleFile(inc_dir: str, rel_seg: bytes, rel_len: sizet, suffix: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int:
if inc_dir is None or rel_seg is None or suffix is None: return 0
inc_len: sizet = string.strlen(inc_dir)
suf_len: sizet = string.strlen(suffix)
# candidate absolute path: {inc}/{rel}{suffix} (hand-built to avoid %. *s)
cand_len: sizet = inc_len + 1 + rel_len + suf_len
cand: bytes = stdlib.malloc(cand_len + 1)
if cand is None: return 0
string.strcpy(cand, inc_dir)
cand[inc_len] = '/'
string.strncpy(cand + inc_len + 1, rel_seg, rel_len)
string.strcpy(cand + inc_len + 1 + rel_len, suffix)
if _PathExistsA(cand) == 0:
stdlib.free(cand)
return 0
# path relative to the includes root: {rel}{suffix}
rel_path: bytes = stdlib.malloc(rel_len + suf_len + 1)
if rel_path is None:
stdlib.free(cand)
return 0
string.strncpy(rel_path, rel_seg, rel_len)
rel_path[rel_len] = '\0'
string.strcat(rel_path, suffix)
# Deep-copy the owning includes root dir; allocate everything before writing
# slots to avoid a half-written state when inc allocation fails.
inc_buf: bytes = None
if out_inc_dir is not None:
inc_buf = stdlib.malloc(inc_len + 1)
if inc_buf is None:
stdlib.free(cand)
stdlib.free(rel_path)
return 0
string.strcpy(inc_buf, inc_dir)
# Write output slots (stdlib deep copies; caller frees). Slots must be cast to
# (sizet*): rawptr* folds to rawptr and [0] becomes a 1-byte access.
if out_abs_path is not None:
abs_slot: sizet* = (sizet*)(out_abs_path)
abs_slot[0] = sizet(cand)
else: stdlib.free(cand)
if out_rel_path is not None:
rel_slot: sizet* = (sizet*)(out_rel_path)
rel_slot[0] = sizet(rel_path)
else: stdlib.free(rel_path)
if out_inc_dir is not None and inc_buf is not None:
inc_slot: sizet* = (sizet*)(out_inc_dir)
inc_slot[0] = sizet(inc_buf)
return 1
# ResolveModuleToPath - module name -> file path, resolved on demand (no full scan).
# Tries each includes dir, preferring file modules over packages. Returns 0 found, 1 not.
def ResolveModuleToPath(includes_dirs: str, mod_name: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int:
if includes_dirs is None or mod_name is None: return 1
mn_len: sizet = string.strlen(mod_name)
if mn_len == 0: return 1
# mod_name -> relative path segment ('.' -> '/'): ast.parser -> ast/parser
rel_seg: bytes = stdlib.malloc(mn_len + 1)
if rel_seg is None: return 1
rel_len: sizet = 0
for mi in range(int(mn_len)):
ch_m: char = mod_name[mi]
if ch_m == '.': rel_seg[rel_len] = '/'
else: rel_seg[rel_len] = ch_m
rel_len += 1
rel_seg[rel_len] = '\0'
found: int = 0
d_total: sizet = string.strlen(includes_dirs)
d_i: sizet = 0
while d_i < d_total:
while d_i < d_total and includes_dirs[d_i] == ' ': d_i += 1
if d_i >= d_total: break
d_start: sizet = d_i
while d_i < d_total and includes_dirs[d_i] != ' ': d_i += 1
d_len: sizet = d_i - d_start
if d_len == 0: continue
inc_dir_buf: bytes = stdlib.malloc(d_len + 1)
if inc_dir_buf is None: continue
string.strncpy(inc_dir_buf, includes_dirs + d_start, d_len)
inc_dir_buf[d_len] = '\0'
# candidate 1: {inc}/{rel}.vp (file module)
if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, ".vp", out_abs_path, out_rel_path, out_inc_dir) != 0:
found = 1
stdlib.free(inc_dir_buf)
break
# candidate 2: {inc}/{rel}/__init__.vp (package module)
if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, "/__init__.vp", out_abs_path, out_rel_path, out_inc_dir) != 0:
found = 1
stdlib.free(inc_dir_buf)
if found != 0: break
stdlib.free(rel_seg)
if found != 0: return 0
return 1
from stdint import *
import memhub
import string
import stdlib
import os
import io
import hashlib
import viperlib
import lib.Projectrans.Utils as Utils
import lib.core.VLogger as VLogger
# IncludesScanner - recursive scan of the includes directory via os.scandir;
# collects .vp paths and SHA1s for Phase1 stub gen / Phase2 merge.
# global mbuddy pointer
_mbuddy: memhub.MemBuddy*
# max files per scan
define MAX_FILES = 1024
# FileEntry - file entry (path + SHA1)
novirtual class FileEntry:
"""A scanned file entry."""
Path: str # absolute file path
Sha1: str # first 16 SHA1 chars (16 bytes + null)
RelPath: str # path relative to the includes root
ModuleName: str # module name (e.g. "ast.parser")
IncDir: str # owning includes root dir (multi-root support)
# ScanResult - scan result
novirtual class ScanResult:
"""A collection of scan results."""
Entries: FileEntry* # FileEntry array
Count: int
Capacity: int
# create_scan_result - create the scan result container
def create_scan_result(pool: memhub.MemBuddy*) -> ScanResult*:
"""Create the scan result container with MAX_FILES preallocated slots."""
if pool is None: return None
size: sizet = MAX_FILES * FileEntry.__sizeof__()
# use stdlib.malloc to avoid exhausting the mbuddy pool (consistent with StubMerger)
entries: FileEntry* = stdlib.malloc(size)
if entries is None: return None
string.memset(entries, 0, size)
result: ScanResult* = stdlib.malloc(ScanResult.__sizeof__())
if result is None: return None
string.memset(result, 0, ScanResult.__sizeof__())
result.Entries = entries
result.Count = 0
result.Capacity = MAX_FILES
return result
# add_file_entry - append a file entry to the scan result
def add_file_entry(result: ScanResult*, pool: memhub.MemBuddy*, abs_path: str, rel_path: str, sha1: str, inc_dir: str) -> int:
"""Append a file entry to the scan result; inc_dir is the owning includes root."""
if result is None or pool is None: return 1
if result.Count >= result.Capacity: return 1
# compute module name: '/' -> '.', strip .vp/.py (Utils handles __init__.vp)
mod_buf: str = Utils.PathToModuleName(rel_path, 0)
if mod_buf is None: return 1
# get the entry address
entry_size: sizet = FileEntry.__sizeof__()
entry_addr: uint64 = uint64(result.Entries) + result.Count * entry_size
entry: FileEntry* = (FileEntry*)(entry_addr)
if entry is None: return 1
# copy the path string
abs_len: sizet = string.strlen(abs_path)
abs_buf: str = stdlib.malloc(abs_len + 1)
if abs_buf is None: return 1
string.strcpy(abs_buf, abs_path)
entry.Path = abs_buf
# copy the SHA1
sha1_buf: str = stdlib.malloc(17)
if sha1_buf is None: return 1
string.strcpy(sha1_buf, sha1)
entry.Sha1 = sha1_buf
# copy the relative path
rel_buf: str = stdlib.malloc(string.strlen(rel_path) + 1)
if rel_buf is None: return 1
string.strcpy(rel_buf, rel_path)
entry.RelPath = rel_buf
# copy the module name
entry.ModuleName = mod_buf
# copy the owning includes root dir (used by Phase2 path joining)
if inc_dir is not None:
inc_len: sizet = string.strlen(inc_dir)
inc_buf: str = stdlib.malloc(inc_len + 1)
if inc_buf is None: return 1
string.strcpy(inc_buf, inc_dir)
entry.IncDir = inc_buf
result.Count += 1
return 0
# compute_file_sha1 - read file content and compute SHA1 (CRLF->LF, matching Projectrans.py)
def compute_file_sha1(pool: memhub.MemBuddy*, file_path: str) -> str:
"""Read a file, normalize CRLF to LF, and return the first 16 SHA1 hex chars."""
if pool is None or file_path is None: return None
# read file content; BUF_SIZE must match SRC_BUF_SIZE (1MB) or trailing edits won't change SHA1
BUF_SIZE: sizet = 1048576
# Refuse larger files: hashing only the first 1 MiB gives two files that differ later the same
# SHA1, and the SHA1 keys the stub/cache files (colliding modules would reuse each other's IR).
sf_sz: io.File* = io.File(file_path, io.MODE.R)
if sf_sz.closed: return None
too_big: int = 0
if sf_sz.size() >= BUF_SIZE: too_big = 1
sf_sz.close()
if too_big != 0:
VLogger.error(f"source file exceeds the {BUF_SIZE}-byte limit and cannot be hashed completely: {file_path}", "IncludesScanner")
return None
buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE)
if buf is None: return None
# strip \r in place (CRLF -> LF), matching Projectrans.py text-mode reads
buf_len: sizet = string.strlen(buf)
write_pos: sizet = 0
for ch in buf:
if ch != '\r':
buf[write_pos] = ch
write_pos += 1
# add a null terminator
buf[write_pos] = 0
# compute SHA1
ctx: hashlib.sha1* = hashlib.sha1()
if ctx is None:
stdlib.free(buf)
return None
ctx.update(buf)
digest: bytes = stdlib.malloc(hashlib.SHA1_DIGEST_LEN)
if digest is None:
stdlib.free(buf)
return None
ctx.final(digest)
# convert to hex (first 8 bytes = 16 hex chars; stdlib allocation unchanged)
hex_buf: str = Utils.bytes_to_hex(pool, digest, 8, 0, 1)
stdlib.free(buf)
stdlib.free(digest)
return hex_buf
# scan_directory_recursive - recursively scan a directory via os.scandir
def scan_directory_recursive(pool: memhub.MemBuddy*, root_dir: str, rel_prefix: str, result: ScanResult*, inc_dir: str) -> int:
"""Recursively collect .vp/.py files under a directory; inc_dir is the includes root."""
if pool is None or root_dir is None or result is None: return 1
subdir_failed: int = 0
reader: os.DirReader* = os.scandir(root_dir)
if reader is None: return 1
entry: os.DirEntry = os.DirEntry()
while True:
rr: int = os.readdir(reader, &entry)
if rr == 0: break
if rr < 0:
os.closedir(reader)
return 1
fname: str = entry.name
fname_len: sizet = string.strlen(fname)
is_dir: int = 1 if entry.is_dir else 0
full_path: bytes = Utils.JoinPath(root_dir, fname)
if full_path is None:
os.closedir(reader)
return 1
prefix_len: sizet = 0
if rel_prefix is not None: prefix_len = string.strlen(rel_prefix)
rel_path: bytes = None
if rel_prefix is not None and prefix_len > 0: rel_path = Utils.JoinPath(rel_prefix, fname)
else:
rel_path = stdlib.malloc(fname_len + 1)
if rel_path is None:
os.closedir(reader)
return 1
string.strcpy(rel_path, fname)
if rel_path is None:
os.closedir(reader)
return 1
if is_dir != 0:
if scan_directory_recursive(pool, full_path, rel_path, result, inc_dir) != 0: subdir_failed += 1
else:
is_py: int = 0
if Utils.is_vp_at(fname, 0, fname_len) != 0: is_py = 1
if is_py != 0:
is_pycache: int = 0
if rel_prefix is not None:
if string.strcmp(rel_prefix, "__pycache__") == 0: is_pycache = 1
if is_pycache == 0:
sha1: str = compute_file_sha1(pool, full_path)
if sha1 is not None:
if add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) != 0:
VLogger.error(f"includes scan could not register '{full_path}' (the {MAX_FILES}-file capacity or an allocation failed); raise MAX_FILES", "scan")
VLogger.debug(f"scan: {rel_path} -> {sha1}", "scan")
else: VLogger.warning(f"SHA1 computation failed: {full_path}", "scan")
os.closedir(reader)
if subdir_failed > 0: VLogger.error(f"subdirectory scan failed for {subdir_failed} entries", "scan")
return 0
# scan_includes - scan entry point: return all .vp file paths and SHA1s
def scan_includes(pool: memhub.MemBuddy*, includes_dirs: str) -> ScanResult*:
"""Scan space-separated includes dirs and return all scanned .vp/.py files."""
if pool is None or includes_dirs is None: return None
result: ScanResult* = create_scan_result(pool)
if result is None: return None
# parse space-separated includes dirs, recursively scanning and merging each
dirs_total: sizet = string.strlen(includes_dirs)
d_i: sizet = 0
while d_i < dirs_total:
while d_i < dirs_total and includes_dirs[d_i] == ' ': d_i += 1
if d_i >= dirs_total: break
d_start: sizet = d_i
while d_i < dirs_total and includes_dirs[d_i] != ' ': d_i += 1
d_len: sizet = d_i - d_start
if d_len == 0: continue
dir_buf: bytes = stdlib.malloc(d_len + 1)
if dir_buf is None: VLogger.error("failed to allocate the includes directory; aborting scan", "scan")
string.strncpy(dir_buf, includes_dirs + d_start, d_len)
dir_buf[d_len] = '\0'
scan_directory_recursive(pool, dir_buf, None, result, dir_buf)
stdlib.free(dir_buf)
return result
# _PathExistsA - path existence check (os.exists)
def _PathExistsA(path: str) -> int:
"""Return 1 if the path exists, else 0."""
return 1 if os.exists(path) else 0
# _TryResolveModuleFile - try one candidate path and write outputs (1=hit, 0=miss);
# out_abs_path/out_rel_path/out_inc_dir are rawptr output slots.
def _TryResolveModuleFile(inc_dir: str, rel_seg: bytes, rel_len: sizet, suffix: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int:
if inc_dir is None or rel_seg is None or suffix is None: return 0
inc_len: sizet = string.strlen(inc_dir)
suf_len: sizet = string.strlen(suffix)
# candidate absolute path: {inc}/{rel}{suffix} (hand-built to avoid %. *s)
cand_len: sizet = inc_len + 1 + rel_len + suf_len
cand: bytes = stdlib.malloc(cand_len + 1)
if cand is None: return 0
string.strcpy(cand, inc_dir)
cand[inc_len] = '/'
string.strncpy(cand + inc_len + 1, rel_seg, rel_len)
string.strcpy(cand + inc_len + 1 + rel_len, suffix)
if _PathExistsA(cand) == 0:
stdlib.free(cand)
return 0
# path relative to the includes root: {rel}{suffix}
rel_path: bytes = stdlib.malloc(rel_len + suf_len + 1)
if rel_path is None:
stdlib.free(cand)
return 0
string.strncpy(rel_path, rel_seg, rel_len)
rel_path[rel_len] = '\0'
string.strcat(rel_path, suffix)
# Deep-copy the owning includes root dir; allocate everything before writing
# slots to avoid a half-written state when inc allocation fails.
inc_buf: bytes = None
if out_inc_dir is not None:
inc_buf = stdlib.malloc(inc_len + 1)
if inc_buf is None:
stdlib.free(cand)
stdlib.free(rel_path)
return 0
string.strcpy(inc_buf, inc_dir)
# Write output slots (stdlib deep copies; caller frees). Slots must be cast to
# (sizet*): rawptr* folds to rawptr and [0] becomes a 1-byte access.
if out_abs_path is not None:
abs_slot: sizet* = (sizet*)(out_abs_path)
abs_slot[0] = sizet(cand)
else: stdlib.free(cand)
if out_rel_path is not None:
rel_slot: sizet* = (sizet*)(out_rel_path)
rel_slot[0] = sizet(rel_path)
else: stdlib.free(rel_path)
if out_inc_dir is not None and inc_buf is not None:
inc_slot: sizet* = (sizet*)(out_inc_dir)
inc_slot[0] = sizet(inc_buf)
return 1
# ResolveModuleToPath - module name -> file path, resolved on demand (no full scan).
# Tries each includes dir, preferring file modules over packages. Returns 0 found, 1 not.
def ResolveModuleToPath(includes_dirs: str, mod_name: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int:
if includes_dirs is None or mod_name is None: return 1
mn_len: sizet = string.strlen(mod_name)
if mn_len == 0: return 1
# mod_name -> relative path segment ('.' -> '/'): ast.parser -> ast/parser
rel_seg: bytes = stdlib.malloc(mn_len + 1)
if rel_seg is None: return 1
rel_len: sizet = 0
for mi in range(int(mn_len)):
ch_m: char = mod_name[mi]
if ch_m == '.': rel_seg[rel_len] = '/'
else: rel_seg[rel_len] = ch_m
rel_len += 1
rel_seg[rel_len] = '\0'
found: int = 0
d_total: sizet = string.strlen(includes_dirs)
d_i: sizet = 0
while d_i < d_total:
while d_i < d_total and includes_dirs[d_i] == ' ': d_i += 1
if d_i >= d_total: break
d_start: sizet = d_i
while d_i < d_total and includes_dirs[d_i] != ' ': d_i += 1
d_len: sizet = d_i - d_start
if d_len == 0: continue
inc_dir_buf: bytes = stdlib.malloc(d_len + 1)
if inc_dir_buf is None: continue
string.strncpy(inc_dir_buf, includes_dirs + d_start, d_len)
inc_dir_buf[d_len] = '\0'
# candidate 1: {inc}/{rel}.vp (file module)
if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, ".vp", out_abs_path, out_rel_path, out_inc_dir) != 0:
found = 1
stdlib.free(inc_dir_buf)
break
# candidate 2: {inc}/{rel}/__init__.vp (package module)
if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, "/__init__.vp", out_abs_path, out_rel_path, out_inc_dir) != 0:
found = 1
stdlib.free(inc_dir_buf)
if found != 0: break
stdlib.free(rel_seg)
if found != 0: return 0
return 1
+2097 -2109
View File
File diff suppressed because it is too large. Load diff
+272 -307
View File
@@ -1,307 +1,272 @@
from stdint import *
import stdio
import stdlib
import string
import viperlib
import os
import os.path
import win32.win32base
import win32.win32file
import win32.win32process
import lib.Phase.BuildPipeline as BuildPipeline
define SELF_EXE_CAP = 4096
define SELF_BUILD_DIR = "__vpcbuild_2__"
define COPY_DELAY_SECONDS = 5
def _GetSelfExe() -> str:
buf: bytes = stdlib.malloc(sizet(SELF_EXE_CAP))
if buf is None:
return None
n: uint32 = win32.win32file.GetModuleFileNameA(None, buf, uint32(SELF_EXE_CAP))
if int(n) <= 0:
stdlib.free(buf)
return None
ni: int = int(n)
if ni >= SELF_EXE_CAP:
ni = SELF_EXE_CAP - 1
buf[ni] = 0
return str(buf)
def _TryRoot(dir: str) -> str:
if dir is None or len(dir) == 0:
return None
root: str = os.path.normpath(dir)
if root is None:
return None
project: str = os.path.join(root, "project.vpj")
copyself_project: str = os.path.join(os.path.join(root, "copyself"), "project.vpj")
if project is not None and copyself_project is not None:
if os.isfile(project) and os.isfile(copyself_project):
return root
return None
def _FindViperCRoot(self_exe: str) -> str:
base: str = os.path.dirname(self_exe)
if base is None:
base = ""
parent1: str = os.path.dirname(base)
parent2: str = os.path.dirname(parent1)
# Executable-derived candidates first: cwd is normally the workspace root,
# not the compiler tree.
root: str = _TryRoot(base)
if root is not None:
return root
root = _TryRoot(parent1)
if root is not None:
return root
root = _TryRoot(parent2)
if root is not None:
return root
root = _TryRoot(os.path.join(base, "ViperC"))
if root is not None:
return root
root = _TryRoot(os.path.join(base, "../ViperC"))
if root is not None:
return root
root = _TryRoot(os.path.join(base, "../../ViperC"))
if root is not None:
return root
# Fallback only when the executable is detached from the compiler tree.
cwd: str = os.getcwd()
root = _TryRoot(cwd)
if root is not None:
return root
if cwd is not None:
root = _TryRoot(os.path.join(cwd, "ViperC"))
if root is not None:
return root
root = _TryRoot(os.path.join(os.path.dirname(cwd), "ViperC"))
if root is not None:
return root
return None
def CollectForwardedArgs(argc: int, argv: str*) -> str:
"""--build-self: argv minus --build-self / --project / --build-dir (and their values).
No whitelist: any current or future flag is forwarded verbatim, so a self-build can
enable the same modes as a normal build (e.g. `viperc --build-self --spend-file p.txt
--vpf 3`). The three excluded entries are the ones the self-build invocation owns.
"""
if argv is None or argc <= 1: return ""
total: sizet = 1
i: int = 1
while i < argc:
a: str = argv[i]
if a is not None:
if string.strcmp(a, "--build-self") == 0:
i += 1
continue
if string.strcmp(a, "--project") == 0 or string.strcmp(a, "--build-dir") == 0:
i += 2
continue
total += len(a) + 1
i += 1
buf: str = stdlib.malloc(total + 1)
if buf is None: return ""
buf[0] = '\0'
i = 1
while i < argc:
a2: str = argv[i]
if a2 is not None:
if string.strcmp(a2, "--build-self") == 0:
i += 1
continue
if string.strcmp(a2, "--project") == 0 or string.strcmp(a2, "--build-dir") == 0:
i += 2
continue
if buf[0] != '\0': string.strcat(buf, " ")
string.strcat(buf, a2)
i += 1
return buf
def _FormatSelfBuildCommand(self_exe: str, project: str, build_dir: str, extra: str) -> str:
# `extra` is the caller's forwarded argument string (empty when nothing was forwarded):
# the rebuilt compiler then carries exactly the modes the user asked for.
extra_len: sizet = 0
if extra is not None: extra_len = len(extra)
need: sizet = len(self_exe) + len(project) + len(build_dir) + extra_len + 64
buf: bytes = stdlib.malloc(need)
if buf is None:
return None
viperlib.snprintf(buf, need, "\"%s\" --project \"%s\" --clean --build-dir \"%s\"",
self_exe, project, build_dir)
if extra is not None and extra_len > 0:
pos: sizet = string.strlen(str(buf))
viperlib.snprintf(buf + pos, need - pos, " %s", extra)
return str(buf)
def _FindCopySelfExe(root: str) -> str:
base: str = os.path.join(root, "copyself")
standard: str = os.path.join(os.path.join(os.path.join(base, "__vpcbuild__"), "output"), "copyself.exe")
if os.isfile(standard):
return standard
legacy: str = os.path.join(os.path.join(base, "output"), "copyself.exe")
if os.isfile(legacy):
return legacy
return None
def _FormatCopySelfCommand(copyself_exe: str) -> str:
need: sizet = len(copyself_exe) + 128
buf: bytes = stdlib.malloc(need)
if buf is None:
return None
viperlib.snprintf(buf, need, "\"%s\" %d ../../__vpcbuild_2__ ../../__vpcbuild__",
copyself_exe, COPY_DELAY_SECONDS)
return str(buf)
def _SpawnDetached(cmd: str, cwd: str) -> int:
if cmd is None or cwd is None:
stdio.printf("FAIL: --build-self cannot launch copyself without a command and working directory\n")
return -1
clen: sizet = len(cmd)
cmd_line: bytes = stdlib.malloc(clen + 1)
if cmd_line is None:
stdio.printf("FAIL: --build-self failed to allocate the copyself command line\n")
return -1
string.strcpy(cmd_line, cmd)
si: win32.win32process.STARTUPINFOA = win32.win32process.STARTUPINFOA()
pi: win32.win32process.PROCESS_INFORMATION = win32.win32process.PROCESS_INFORMATION()
string.memset(&si, 0, win32.win32process.STARTUPINFOA.__sizeof__())
string.memset(&pi, 0, win32.win32process.PROCESS_INFORMATION.__sizeof__())
si.cb = win32.win32process.STARTUPINFOA.__sizeof__()
ok: int = win32.win32process.CreateProcessA(None, cmd_line, None, None, 1, 0,
None, cwd, &si, &pi)
if ok == 0:
err: uint32 = win32.win32base.GetLastError()
stdio.printf("FAIL: --build-self could not launch copyself (Win32 error %d)\n", int(err))
stdio.printf(" command: %s\n", cmd)
stdlib.free(cmd_line)
return -1
stdio.printf("build-self: copyself launched (pid=%d)\n", int(pi.dwProcessId))
win32.win32base.CloseHandle(pi.hProcess)
win32.win32base.CloseHandle(pi.hThread)
stdlib.free(cmd_line)
return 0
# _SelfExeDir - directory containing the running compiler ("D:/x/viperc.exe" -> "D:/x").
def _SelfExeDir(self_exe: str) -> str:
if self_exe is None: return None
n: sizet = string.strlen(self_exe)
cut: sizet = n
while cut > 0:
ch: char = self_exe[cut - 1]
if ch == '\\' or ch == '/':
cut -= 1
break
cut -= 1
if cut >= n: return None
out: str = stdlib.malloc(cut + 1)
if out is None: return None
if cut > 0: string.strncpy(out, self_exe, cut)
out[cut] = '\0'
return out
# _CrashHint - name the common NTSTATUS exit codes. The child inherits the parent console, so
# its own output is already visible; these codes are raised as fail-fast and carry no SEH
# exception, which is why a traceback handler inside the child cannot print a stack.
def _CrashHint(rc: int) -> None:
u: uint32 = uint32(rc)
if u == uint32(0xC0000005):
stdio.printf(" child crashed: access violation (0xC0000005)\n")
elif u == uint32(0xC0000374):
stdio.printf(" child crashed: heap corruption (0xC0000374) -- fail-fast, no SEH traceback\n")
elif u == uint32(0xC00000FD):
stdio.printf(" child crashed: stack overflow (0xC00000FD)\n")
elif u == uint32(0xC0000409):
stdio.printf(" child crashed: fail-fast / security check (0xC0000409)\n")
def RunSelfBuild(extra: str) -> int:
self_exe: str = _GetSelfExe()
if self_exe is None:
stdio.printf("FAIL: --build-self could not resolve the compiler executable path\n")
return 1
root: str = _FindViperCRoot(self_exe)
if root is None:
stdio.printf("FAIL: --build-self could not locate the ViperC root\n")
stdio.printf(" expected both: project.vpj and copyself/project.vpj\n")
return 1
project: str = os.path.join(root, "project.vpj")
build_dir: str = os.path.join(root, SELF_BUILD_DIR)
copyself_exe: str = _FindCopySelfExe(root)
if copyself_exe is None:
stdio.printf("FAIL: copyself.exe not found under %s\n", os.path.join(root, "copyself"))
stdio.printf(" expected: output/copyself.exe or __vpcbuild__/output/copyself.exe\n")
stdio.printf(" build it first: viperc --project %s --clean\n",
os.path.join(os.path.join(root, "copyself"), "project.vpj"))
return 1
cmd: str = _FormatSelfBuildCommand(self_exe, project, build_dir, extra)
if cmd is None:
stdio.printf("FAIL: --build-self failed to build the compiler command line\n")
return 1
stdio.printf("build-self: %s\n", cmd)
rc: int = BuildPipeline._run_cmd_wait(cmd)
if rc != 0:
stdio.printf("FAIL: --build-self compilation failed (exit=%d)\n", rc)
_CrashHint(rc)
return 1
built_exe: str = os.path.join(os.path.join(build_dir, "output"), "viperc.exe")
if not os.isfile(built_exe):
stdio.printf("FAIL: --build-self did not produce %s\n", built_exe)
return 1
# The requested arguments are relative to ViperC/copyself/output. Use that
# directory as the launch cwd even when copyself.exe lives under
# copyself/__vpcbuild__/output.
copyself_cwd: str = os.path.join(os.path.join(root, "copyself"), "output")
if not os.isdir(copyself_cwd):
if os.mkdir(copyself_cwd) != 0:
if not os.isdir(copyself_cwd):
err_mkdir: ULONG = win32.win32base.GetLastError()
stdio.printf("FAIL: --build-self could not create %s (Win32 error %d)\n",
copyself_cwd, int(err_mkdir))
return 1
# copyself replaces ViperC\__vpcbuild__ wholesale, so everything the compiler needs next to
# viperc.exe must be in the new output directory *before* the copy runs. ircore.dll is the
# important one: without it every PhaseB module silently falls back to spawning
# mlir-opt/mlir-translate/llc (3 processes per module), which is both slower and a
# different code path than the one being profiled.
self_dir: str = _SelfExeDir(self_exe)
if self_dir is not None:
src_dll: str = os.path.join(self_dir, "ircore.dll")
dst_dll: str = os.path.join(os.path.join(build_dir, "output"), "ircore.dll")
if os.isfile(src_dll) and not os.isfile(dst_dll):
cop: BOOL = win32.win32file.CopyFileA(src_dll, dst_dll, 0)
if cop == 0:
stdio.printf("build-self: warning: could not copy ircore.dll next to the new viperc.exe\n")
else:
stdio.printf("build-self: copied ircore.dll into the new output directory\n")
copy_cmd: str = _FormatCopySelfCommand(copyself_exe)
if copy_cmd is None:
stdio.printf("FAIL: --build-self failed to build the copyself command line\n")
return 1
if _SpawnDetached(copy_cmd, copyself_cwd) != 0:
return 1
stdio.printf("build-self: __vpcbuild__ will be replaced after %d seconds\n", COPY_DELAY_SECONDS)
return 0
from stdint import *
import stdio
import stdlib
import string
import viperlib
import os
import os.path
import sys
import shutil
import lib.Phase.BuildPipeline as BuildPipeline
define SELF_EXE_CAP = 4096
define SELF_BUILD_DIR = "__vpcbuild_2__"
define COPY_DELAY_SECONDS = 5
def _GetSelfExe() -> str:
return sys.executable()
def _TryRoot(dir: str) -> str:
if dir is None or len(dir) == 0:
return None
root: str = os.path.normpath(dir)
if root is None:
return None
project: str = os.path.join(root, "project.vpj")
copyself_project: str = os.path.join(os.path.join(root, "copyself"), "project.vpj")
if project is not None and copyself_project is not None:
if os.isfile(project) and os.isfile(copyself_project):
return root
return None
def _FindViperCRoot(self_exe: str) -> str:
base: str = os.path.dirname(self_exe)
if base is None:
base = ""
parent1: str = os.path.dirname(base)
parent2: str = os.path.dirname(parent1)
# Executable-derived candidates first: cwd is normally the workspace root,
# not the compiler tree.
root: str = _TryRoot(base)
if root is not None:
return root
root = _TryRoot(parent1)
if root is not None:
return root
root = _TryRoot(parent2)
if root is not None:
return root
root = _TryRoot(os.path.join(base, "ViperC"))
if root is not None:
return root
root = _TryRoot(os.path.join(base, "../ViperC"))
if root is not None:
return root
root = _TryRoot(os.path.join(base, "../../ViperC"))
if root is not None:
return root
# Fallback only when the executable is detached from the compiler tree.
cwd: str = os.getcwd()
root = _TryRoot(cwd)
if root is not None:
return root
if cwd is not None:
root = _TryRoot(os.path.join(cwd, "ViperC"))
if root is not None:
return root
root = _TryRoot(os.path.join(os.path.dirname(cwd), "ViperC"))
if root is not None:
return root
return None
def CollectForwardedArgs(argc: int, argv: str*) -> str:
"""--build-self: argv minus --build-self / --project / --build-dir (and their values).
No whitelist: any current or future flag is forwarded verbatim, so a self-build can
enable the same modes as a normal build (e.g. `viperc --build-self --spend-file p.txt
--vpf 3`). The three excluded entries are the ones the self-build invocation owns.
"""
if argv is None or argc <= 1: return ""
total: sizet = 1
i: int = 1
while i < argc:
a: str = argv[i]
if a is not None:
if string.strcmp(a, "--build-self") == 0:
i += 1
continue
if string.strcmp(a, "--project") == 0 or string.strcmp(a, "--build-dir") == 0:
i += 2
continue
total += len(a) + 1
i += 1
buf: str = stdlib.malloc(total + 1)
if buf is None: return ""
buf[0] = '\0'
i = 1
while i < argc:
a2: str = argv[i]
if a2 is not None:
if string.strcmp(a2, "--build-self") == 0:
i += 1
continue
if string.strcmp(a2, "--project") == 0 or string.strcmp(a2, "--build-dir") == 0:
i += 2
continue
if buf[0] != '\0': string.strcat(buf, " ")
string.strcat(buf, a2)
i += 1
return buf
def _FormatSelfBuildCommand(self_exe: str, project: str, build_dir: str, extra: str) -> str:
# `extra` is the caller's forwarded argument string (empty when nothing was forwarded):
# the rebuilt compiler then carries exactly the modes the user asked for.
extra_len: sizet = 0
if extra is not None: extra_len = len(extra)
need: sizet = len(self_exe) + len(project) + len(build_dir) + extra_len + 64
buf: bytes = stdlib.malloc(need)
if buf is None:
return None
viperlib.snprintf(buf, need, "\"%s\" --project \"%s\" --clean --build-dir \"%s\"",
self_exe, project, build_dir)
if extra is not None and extra_len > 0:
pos: sizet = string.strlen(str(buf))
viperlib.snprintf(buf + pos, need - pos, " %s", extra)
return str(buf)
def _FindCopySelfExe(root: str) -> str:
base: str = os.path.join(root, "copyself")
standard: str = os.path.join(os.path.join(os.path.join(base, "__vpcbuild__"), "output"), "copyself.exe")
if os.isfile(standard):
return standard
legacy: str = os.path.join(os.path.join(base, "output"), "copyself.exe")
if os.isfile(legacy):
return legacy
return None
def _FormatCopySelfCommand(copyself_exe: str) -> str:
need: sizet = len(copyself_exe) + 128
buf: bytes = stdlib.malloc(need)
if buf is None:
return None
viperlib.snprintf(buf, need, "\"%s\" %d ../../__vpcbuild_2__ ../../__vpcbuild__",
copyself_exe, COPY_DELAY_SECONDS)
return str(buf)
def _SpawnDetached(cmd: str, cwd: str) -> int:
if cmd is None or cwd is None:
stdio.printf("FAIL: --build-self cannot launch copyself without a command and working directory\n")
return -1
pid: int = sys.spawn_detached(cmd, cwd)
if pid < 0:
stdio.printf("FAIL: --build-self could not launch copyself\n")
stdio.printf(" command: %s\n", cmd)
return -1
stdio.printf("build-self: copyself launched (pid=%d)\n", pid)
return 0
# _SelfExeDir - directory containing the running compiler ("D:/x/viperc.exe" -> "D:/x").
def _SelfExeDir(self_exe: str) -> str:
if self_exe is None: return None
n: sizet = string.strlen(self_exe)
cut: sizet = n
while cut > 0:
ch: char = self_exe[cut - 1]
if ch == '\\' or ch == '/':
cut -= 1
break
cut -= 1
if cut >= n: return None
out: str = stdlib.malloc(cut + 1)
if out is None: return None
if cut > 0: string.strncpy(out, self_exe, cut)
out[cut] = '\0'
return out
# _CrashHint - name the common NTSTATUS exit codes. The child inherits the parent console, so
# its own output is already visible; these codes are raised as fail-fast and carry no SEH
# exception, which is why a traceback handler inside the child cannot print a stack.
def _CrashHint(rc: int) -> None:
u: uint32 = uint32(rc)
if u == uint32(0xC0000005):
stdio.printf(" child crashed: access violation (0xC0000005)\n")
elif u == uint32(0xC0000374):
stdio.printf(" child crashed: heap corruption (0xC0000374) -- fail-fast, no SEH traceback\n")
elif u == uint32(0xC00000FD):
stdio.printf(" child crashed: stack overflow (0xC00000FD)\n")
elif u == uint32(0xC0000409):
stdio.printf(" child crashed: fail-fast / security check (0xC0000409)\n")
def RunSelfBuild(extra: str) -> int:
self_exe: str = _GetSelfExe()
if self_exe is None:
stdio.printf("FAIL: --build-self could not resolve the compiler executable path\n")
return 1
root: str = _FindViperCRoot(self_exe)
if root is None:
stdio.printf("FAIL: --build-self could not locate the ViperC root\n")
stdio.printf(" expected both: project.vpj and copyself/project.vpj\n")
return 1
project: str = os.path.join(root, "project.vpj")
build_dir: str = os.path.join(root, SELF_BUILD_DIR)
copyself_exe: str = _FindCopySelfExe(root)
if copyself_exe is None:
stdio.printf("FAIL: copyself.exe not found under %s\n", os.path.join(root, "copyself"))
stdio.printf(" expected: output/copyself.exe or __vpcbuild__/output/copyself.exe\n")
stdio.printf(" build it first: viperc --project %s --clean\n",
os.path.join(os.path.join(root, "copyself"), "project.vpj"))
return 1
cmd: str = _FormatSelfBuildCommand(self_exe, project, build_dir, extra)
if cmd is None:
stdio.printf("FAIL: --build-self failed to build the compiler command line\n")
return 1
stdio.printf("build-self: %s\n", cmd)
rc: int = BuildPipeline._run_cmd_wait(cmd)
if rc != 0:
stdio.printf("FAIL: --build-self compilation failed (exit=%d)\n", rc)
_CrashHint(rc)
return 1
built_exe: str = os.path.join(os.path.join(build_dir, "output"), "viperc.exe")
if not os.isfile(built_exe):
stdio.printf("FAIL: --build-self did not produce %s\n", built_exe)
return 1
# The requested arguments are relative to ViperC/copyself/output. Use that
# directory as the launch cwd even when copyself.exe lives under
# copyself/__vpcbuild__/output.
copyself_cwd: str = os.path.join(os.path.join(root, "copyself"), "output")
if not os.isdir(copyself_cwd):
if os.mkdir(copyself_cwd) != 0:
if not os.isdir(copyself_cwd):
stdio.printf("FAIL: --build-self could not create %s\n", copyself_cwd)
return 1
# copyself replaces ViperC\__vpcbuild__ wholesale, so everything the compiler needs next to
# viperc.exe must be in the new output directory *before* the copy runs. ircore.dll is the
# important one: without it every PhaseB module silently falls back to spawning
# mlir-opt/mlir-translate/llc (3 processes per module), which is both slower and a
# different code path than the one being profiled.
self_dir: str = _SelfExeDir(self_exe)
if self_dir is not None:
src_dll: str = os.path.join(self_dir, "ircore.dll")
dst_dll: str = os.path.join(os.path.join(build_dir, "output"), "ircore.dll")
if os.isfile(src_dll) and not os.isfile(dst_dll):
if shutil.copyfile(src_dll, dst_dll) != 0:
stdio.printf("build-self: warning: could not copy ircore.dll next to the new viperc.exe\n")
else:
stdio.printf("build-self: copied ircore.dll into the new output directory\n")
copy_cmd: str = _FormatCopySelfCommand(copyself_exe)
if copy_cmd is None:
stdio.printf("FAIL: --build-self failed to build the copyself command line\n")
return 1
if _SpawnDetached(copy_cmd, copyself_cwd) != 0:
return 1
stdio.printf("build-self: __vpcbuild__ will be replaced after %d seconds\n", COPY_DELAY_SECONDS)
return 0
+194 -197
View File
@@ -1,197 +1,194 @@
from stdint import *
import stdio
import memhub
import sys
import win32.win32console as win32cmd
import win32.win32file as win32file
import win32.win32base as win32base
# VLogger - native logger with colored Windows console output.
# Format: ├ LEVEL: message or ├ LEVEL[category]: message (aligned with TPC lib/core/VLogger.py).
# Error recovery (REPL/JIT): after error, longjmp back to the REPL setjmp point.
# mingw-w64 x86_64: longjmp is void longjmp(jmp_buf, int); setjmp expands to _setjmp(buf, NULL).
extern def longjmp(buf: rawptr, val: int)
# REPL recovery-point pointer: registered at session start, cleared at session end.
# Covers the whole fragment pipeline; batch mode never sets it, so it stays None and exits via sys.exit(1).
_repl_jmpbuf: rawptr = None
# repl_abort - the single "terminate after diagnosis" exit in VPC.
# Caller already printed the diagnostic; returns to REPL via longjmp if a recovery point is set, else sys.exit(1).
def repl_abort() -> int:
"""Unified exit after diagnosis: longjmp to the REPL if a recovery point is set, else exit."""
if _repl_jmpbuf is not None:
stdio.fflush(None)
longjmp(_repl_jmpbuf, 1)
sys.exit(1)
return 0
def set_repl_jmpbuf(buf: rawptr) -> int:
"""Register the REPL jmp_buf; idempotent at each fragment entry."""
global _repl_jmpbuf
_repl_jmpbuf = buf
return 0
def clear_repl_jmpbuf() -> int:
"""Clear the registered jmp_buf at REPL session end."""
global _repl_jmpbuf
_repl_jmpbuf = None
return 0
# Log levels
enum LogLevel:
DEBUG, INFO, WARNING, ERROR, SUCCESS
# Win32 console foreground color attributes
# 值用 WORD(...) 显式定宽为 uint16(与 _log 的 sym_color/level_color 形参一致)。
# 注意:仅写 `define X: WORD = 0x0002` 不够 —— define 的宽度由**右侧表达式的类型**
# 决定(win32file.vp 的 `define STD_OUTPUT_HANDLE: DWORD = DWORD(-10)` 同款),
# 否则常量仍是 i32 → 传给 WORD 形参即 32→16 隐式窄化。
define FOREGROUND_BLUE: WORD = WORD(0x0001)
define FOREGROUND_GREEN: WORD = WORD(0x0002)
define FOREGROUND_RED: WORD = WORD(0x0004)
define FOREGROUND_INTENSITY: WORD = WORD(0x0008)
define FOREGROUND_WHITE: WORD = WORD(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
define FOREGROUND_YELLOW: WORD = WORD(FOREGROUND_RED | FOREGROUND_GREEN)
novirtual class Logger:
_level: int
_console_handle: win32base.HANDLE
_use_color: int
jit_flag: bool
__mbuddy__: memhub.MemBuddy*
def __init__(self, level: int = 1):
self._level = level
self._console_handle = win32file.GetStdHandle(win32file.STD_OUTPUT_HANDLE)
self._use_color = 1
self.jit_flag = False
self.__mbuddy__ = _mbuddy
def _set_color(self, attr: WORD) -> int:
uc: int = self._use_color
if uc:
ch: win32base.HANDLE = self._console_handle
ret: int = win32cmd.SetConsoleTextAttribute(ch, attr)
return ret
return 0
def _reset_color(self) -> int:
if self._use_color: return win32cmd.SetConsoleTextAttribute(self._console_handle, FOREGROUND_WHITE)
return 0
# _log - core log output with segmented coloring (prefix symbol / level tag / message).
# Format: ├ LEVEL: msg or ├ LEVEL[category]: msg.
def _log(self, level: int, prefix_sym: str, level_str: str, msg: str, sym_color: WORD, level_color: WORD, category: str = "") -> int:
# Default level is INFO; DEBUG messages stay silent unless the level is lowered.
if level < self._level: return 0
# Color the prefix symbol
self._set_color(sym_color)
stdio.printf("%s ", prefix_sym)
# Color the level tag
self._set_color(level_color)
stdio.printf("%s", level_str)
self._reset_color()
# Optional category
if category is not None and category[0] != 0: stdio.printf(" [%s]", category)
stdio.printf(": %s\n", msg)
return 0
def debug(self, msg: str, category: str = "") -> int:
return self._log(LogLevel.DEBUG, "├", "DEBUG", msg, FOREGROUND_INTENSITY, FOREGROUND_INTENSITY, category)
def info(self, msg: str, category: str = "") -> int:
ret: int = self._log(LogLevel.INFO, "├", "INFO", msg, FOREGROUND_GREEN, WORD(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category)
return ret
def warning(self, msg: str, category: str = "") -> int:
return self._log(LogLevel.WARNING, "├", "WARN", msg, FOREGROUND_YELLOW, WORD(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY), category)
def error(self, msg: str, category: str = "") -> int:
ret: int = self._log(LogLevel.ERROR, "├", "ERROR", msg, FOREGROUND_RED, WORD(FOREGROUND_RED | FOREGROUND_INTENSITY), category)
# JIT/REPL: diagnostic already printed; jump back to the REPL loop via the unified exit.
if self.jit_flag:
repl_abort()
return ret
# Batch mode: terminate immediately on error.
sys.exit(1)
return ret
def success(self, msg: str, category: str = "") -> int:
return self._log(LogLevel.SUCCESS, "├", "SUCCESS", msg, FOREGROUND_GREEN, WORD(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category)
def set_jit_flag(self, v: bool) -> int:
"""Set the JIT/REPL flag: True makes error longjmp back to the REPL loop."""
self.jit_flag = v
return 0
# Advanced log methods.
def banner(self, msg: str) -> int:
"""Print a blue-highlighted section banner."""
self._set_color(FOREGROUND_BLUE | FOREGROUND_INTENSITY)
stdio.printf("\n=== %s ===\n\n", msg)
self._reset_color()
return 0
# Global mbuddy pointer (injected by lib.InitLib).
_mbuddy: memhub.MemBuddy*
# Global logger pointer
logger: Logger*
log: Logger*
def init_logger():
"""Initialize the global logger instance."""
global logger, log
raw: rawptr = _mbuddy.alloc(Logger.__sizeof__())
# if raw is None: return None (memory management still unclear)
logger = raw
logger.__before_init__()
logger.__init__(LogLevel.INFO)
log = logger
# if fmtbuffer is None: return None
# --nowarn-register switch: suppresses Phase A dependency WARN output (output only).
_nowarn_register: int = 0
# Module-level convenience functions (aligned with TPC lib.core.VLogger; no get_logger() needed).
def info(msg: str, category: str = "") -> int:
"""Log at INFO level."""
if log is not None: return log.info(msg, category)
return 0
def warning(msg: str, category: str = "") -> int:
"""Log at WARN level."""
if log is not None: return log.warning(msg, category)
return 0
def error(msg: str, category: str = "") -> int:
"""Log at ERROR level: exit in batch mode, longjmp to REPL under JIT/REPL."""
if log is not None: return log.error(msg, category)
# When the logger is unavailable, still use the unified exit (REPL can recover if registered)
repl_abort()
return 0
def success(msg: str, category: str = "") -> int:
"""Log at SUCCESS level."""
if log is not None: return log.success(msg, category)
return 0
def debug(msg: str, category: str = "") -> int:
"""Log at DEBUG level."""
if log is not None: return log.debug(msg, category)
return 0
from stdint import *
import stdio
import memhub
import sys
import win32.win32console as win32cmd
import win32.win32file as win32file
import win32.win32base as win32base
# VLogger - native logger with colored Windows console output.
# Format: ├ LEVEL: message or ├ LEVEL[category]: message (aligned with TPC lib/core/VLogger.py).
# Error recovery (REPL/JIT): after error, longjmp back to the REPL setjmp point.
# mingw-w64 x86_64: longjmp is void longjmp(jmp_buf, int); setjmp expands to _setjmp(buf, NULL).
extern def longjmp(buf: rawptr, val: int)
# REPL recovery-point pointer: registered at session start, cleared at session end.
# Covers the whole fragment pipeline; batch mode never sets it, so it stays None and exits via sys.exit(1).
_repl_jmpbuf: rawptr = None
# repl_abort - the single "terminate after diagnosis" exit in VPC.
# Caller already printed the diagnostic; returns to REPL via longjmp if a recovery point is set, else sys.exit(1).
def repl_abort() -> int:
"""Unified exit after diagnosis: longjmp to the REPL if a recovery point is set, else exit."""
if _repl_jmpbuf is not None:
stdio.fflush(None)
longjmp(_repl_jmpbuf, 1)
sys.exit(1)
return 0
def set_repl_jmpbuf(buf: rawptr) -> int:
"""Register the REPL jmp_buf; idempotent at each fragment entry."""
global _repl_jmpbuf
_repl_jmpbuf = buf
return 0
def clear_repl_jmpbuf() -> int:
"""Clear the registered jmp_buf at REPL session end."""
global _repl_jmpbuf
_repl_jmpbuf = None
return 0
# Log levels
enum LogLevel:
DEBUG, INFO, WARNING, ERROR, SUCCESS
# Console color attributes (restored: color output is enabled on the Windows console).
# 值用 uint16(...) 显式定宽为 uint16(与 _log 的 sym_color/level_color 形参一致)。
# 注意:仅写 `define X: WORD = 0x0002` 不够 —— define 的宽度由**右侧表达式的类型**
# 决定(win32file.vp 的 `define STD_OUTPUT_HANDLE: DWORD = Duint16(-10)` 同款),
# 否则常量仍是 i32 → 传给 WORD 形参即 32→16 隐式窄化。
define FOREGROUND_BLUE: uint16 = uint16(0x0001)
define FOREGROUND_GREEN: uint16 = uint16(0x0002)
define FOREGROUND_RED: uint16 = uint16(0x0004)
define FOREGROUND_INTENSITY: uint16 = uint16(0x0008)
define FOREGROUND_WHITE: uint16 = uint16(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
define FOREGROUND_YELLOW: uint16 = uint16(FOREGROUND_RED | FOREGROUND_GREEN)
novirtual class Logger:
_level: int
_console_handle: win32base.HANDLE
_use_color: int
jit_flag: bool
__mbuddy__: memhub.MemBuddy*
def __init__(self, level: int = 1):
self._level = level
self._console_handle = win32file.GetStdHandle(win32file.STD_OUTPUT_HANDLE)
self._use_color = 1
self.jit_flag = False
self.__mbuddy__ = _mbuddy
def _set_color(self, attr: uint16) -> int:
if self._use_color:
return win32cmd.SetConsoleTextAttribute(self._console_handle, attr)
return 0
def _reset_color(self) -> int:
if self._use_color: return win32cmd.SetConsoleTextAttribute(self._console_handle, FOREGROUND_WHITE)
return 0
# _log - core log output with segmented coloring (prefix symbol / level tag / message).
# Format: ├ LEVEL: msg or ├ LEVEL[category]: msg.
def _log(self, level: int, prefix_sym: str, level_str: str, msg: str, sym_color: uint16, level_color: uint16, category: str = "") -> int:
# Default level is INFO; DEBUG messages stay silent unless the level is lowered.
if level < self._level: return 0
# Color the prefix symbol
self._set_color(sym_color)
stdio.printf("%s ", prefix_sym)
# Color the level tag
self._set_color(level_color)
stdio.printf("%s", level_str)
self._reset_color()
# Optional category
if category is not None and category[0] != 0: stdio.printf(" [%s]", category)
stdio.printf(": %s\n", msg)
return 0
def debug(self, msg: str, category: str = "") -> int:
return self._log(LogLevel.DEBUG, "├", "DEBUG", msg, FOREGROUND_INTENSITY, FOREGROUND_INTENSITY, category)
def info(self, msg: str, category: str = "") -> int:
ret: int = self._log(LogLevel.INFO, "├", "INFO", msg, FOREGROUND_GREEN, uint16(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category)
return ret
def warning(self, msg: str, category: str = "") -> int:
return self._log(LogLevel.WARNING, "├", "WARN", msg, FOREGROUND_YELLOW, uint16(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY), category)
def error(self, msg: str, category: str = "") -> int:
ret: int = self._log(LogLevel.ERROR, "├", "ERROR", msg, FOREGROUND_RED, uint16(FOREGROUND_RED | FOREGROUND_INTENSITY), category)
# JIT/REPL: diagnostic already printed; jump back to the REPL loop via the unified exit.
if self.jit_flag:
repl_abort()
return ret
# Batch mode: terminate immediately on error.
sys.exit(1)
return ret
def success(self, msg: str, category: str = "") -> int:
return self._log(LogLevel.SUCCESS, "├", "SUCCESS", msg, FOREGROUND_GREEN, uint16(FOREGROUND_GREEN | FOREGROUND_INTENSITY), category)
def set_jit_flag(self, v: bool) -> int:
"""Set the JIT/REPL flag: True makes error longjmp back to the REPL loop."""
self.jit_flag = v
return 0
# Advanced log methods.
def banner(self, msg: str) -> int:
"""Print a blue-highlighted section banner."""
self._set_color(FOREGROUND_BLUE | FOREGROUND_INTENSITY)
stdio.printf("\n=== %s ===\n\n", msg)
self._reset_color()
return 0
# Global mbuddy pointer (injected by lib.InitLib).
_mbuddy: memhub.MemBuddy*
# Global logger pointer
logger: Logger*
log: Logger*
def init_logger():
"""Initialize the global logger instance."""
global logger, log
raw: rawptr = _mbuddy.alloc(Logger.__sizeof__())
# if raw is None: return None (memory management still unclear)
logger = raw
logger.__before_init__()
logger.__init__(LogLevel.INFO)
log = logger
# if fmtbuffer is None: return None
# --nowarn-register switch: suppresses Phase A dependency WARN output (output only).
_nowarn_register: int = 0
# Module-level convenience functions (aligned with TPC lib.core.VLogger; no get_logger() needed).
def info(msg: str, category: str = "") -> int:
"""Log at INFO level."""
if log is not None: return log.info(msg, category)
return 0
def warning(msg: str, category: str = "") -> int:
"""Log at WARN level."""
if log is not None: return log.warning(msg, category)
return 0
def error(msg: str, category: str = "") -> int:
"""Log at ERROR level: exit in batch mode, longjmp to REPL under JIT/REPL."""
if log is not None: return log.error(msg, category)
# When the logger is unavailable, still use the unified exit (REPL can recover if registered)
repl_abort()
return 0
def success(msg: str, category: str = "") -> int:
"""Log at SUCCESS level."""
if log is not None: return log.success(msg, category)
return 0
def debug(msg: str, category: str = "") -> int:
"""Log at DEBUG level."""
if log is not None: return log.debug(msg, category)
return 0
File diff suppressed because it is too large. Load diff
+34 -6
View File
@@ -578,7 +578,9 @@ def _AppendInt(pool: memhub.MemManager*, arr: json.JsonValue*, value: int,
return tail
node: json.JsonValue* = json.int_val(pool, int64(value))
if node is None:
return tail
# Pool exhausted: signal failure so _Encode can stop at a token boundary instead of
# emitting a partial token (the constructor is now null-safe and returns None).
return None
if tail is None:
arr.child = node
else:
@@ -587,6 +589,35 @@ def _AppendInt(pool: memhub.MemManager*, arr: json.JsonValue*, value: int,
return node
def _AppendRollback(arr: json.JsonValue*, tail_before: json.JsonValue*, count_before: sizet) -> json.JsonValue*:
"""Undo a partially appended token after an allocation failure."""
if tail_before is None:
arr.child = None
else:
tail_before.next = None
arr.child_count = count_before
return None
def _AppendToken(pool: memhub.MemManager*, arr: json.JsonValue*,
dl: int, dc: int, length: int, typ: int, mods: int,
tail: json.JsonValue*) -> json.JsonValue*:
"""Append one 5-int semantic token atomically; None when the pool is exhausted."""
tail_before: json.JsonValue* = tail
count_before: sizet = arr.child_count
tail = _AppendInt(pool, arr, dl, tail)
if tail is None: return _AppendRollback(arr, tail_before, count_before)
tail = _AppendInt(pool, arr, dc, tail)
if tail is None: return _AppendRollback(arr, tail_before, count_before)
tail = _AppendInt(pool, arr, length, tail)
if tail is None: return _AppendRollback(arr, tail_before, count_before)
tail = _AppendInt(pool, arr, typ, tail)
if tail is None: return _AppendRollback(arr, tail_before, count_before)
tail = _AppendInt(pool, arr, mods, tail)
if tail is None: return _AppendRollback(arr, tail_before, count_before)
return tail
def _Encode(pool: memhub.MemManager*, out: json.JsonValue*,
ast_tokens: list[LspSemToken*]*, lex_tokens: list[LspSemToken*]*,
has_range: int, start_line: int, end_line: int):
@@ -628,11 +659,8 @@ def _Encode(pool: memhub.MemManager*, out: json.JsonValue*,
length: int = t.ec - t.sc
if length <= 0:
continue
tail = _AppendInt(pool, out, dl, tail)
tail = _AppendInt(pool, out, dc, tail)
tail = _AppendInt(pool, out, length, tail)
tail = _AppendInt(pool, out, t.typ, tail)
tail = _AppendInt(pool, out, t.mods, tail)
tail = _AppendToken(pool, out, dl, dc, length, t.typ, t.mods, tail)
if tail is None: break
prev_line = t.sl
prev_char = t.sc
last = t
+1 -7
View File
@@ -3,9 +3,6 @@ import string
import json
import memhub
import os
import os._win32
import os._posix
import os.path
import lib.lsp.LspProtocol as LspProtocol
import lib.lsp.LspTransport as LspTransport
import lib.lsp.LspRpc as LspRpc
@@ -448,10 +445,7 @@ def _WritePingResult(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int:
global _cancel_int, _cancel_str
os._mbuddy = perm_pool
os._win32._mbuddy = perm_pool
os._posix._mbuddy = perm_pool
os.path._mbuddy = perm_pool
os.set_mbuddy(perm_pool)
tr: LspTransport.LspTransport* = LspTransport.LspTransport(perm_pool, pool)
if tr is None or not tr.IsValid():
+143 -165
View File
@@ -1,165 +1,143 @@
from stdint import *
import stdlib
import string
import viperlib
import memhub
import win32.win32base
import win32.win32file
define LSP_READ_BUFFER_SIZE = 65536
define LSP_HEADER_LINE_MAX = 512
define LSP_MAX_MESSAGE_BYTES = 16777216
class LspTransport:
pool: memhub.MemManager*
in_handle: win32.win32base.HANDLE
out_handle: win32.win32base.HANDLE
buf: bytes
buf_cap: sizet
buf_len: int
buf_pos: int
eof: int
error: int
def __new__(self, owner: memhub.MemManager*, request: memhub.MemManager*) -> rawptr:
ptr: LspTransport* = (LspTransport*)(owner.alloc(LspTransport.__sizeof__()))
if ptr:
string.memset(ptr, 0, LspTransport.__sizeof__())
return rawptr(ptr)
def __init__(self, owner: memhub.MemManager*, request: memhub.MemManager*):
self.pool = request
self.in_handle = win32.win32file.GetStdHandle(win32.win32file.STD_INPUT_HANDLE)
self.out_handle = win32.win32file.GetStdHandle(win32.win32file.STD_OUTPUT_HANDLE)
self.buf_cap = sizet(LSP_READ_BUFFER_SIZE)
self.buf = stdlib.malloc(self.buf_cap)
self.buf_len = 0
self.buf_pos = 0
self.eof = 0
self.error = 0
def IsValid(self) -> bool:
if self.in_handle == win32.win32base.INVALID_HANDLE_VALUE:
return False
if self.out_handle == win32.win32base.INVALID_HANDLE_VALUE:
return False
if self.buf is None:
return False
return True
def _ReadByte(self) -> int:
if self.buf_pos >= self.buf_len:
if self.eof != 0:
return -1
n: ULONG = 0
ok: int = win32.win32file.ReadFile(self.in_handle, self.buf,
ULONG(self.buf_cap), &n, None)
if ok == 0 or int(n) == 0:
self.eof = 1
return -1
self.buf_len = int(n)
self.buf_pos = 0
ch: char = self.buf[self.buf_pos]
self.buf_pos += 1
return int(ch)
def ReadLine(self) -> str:
line: bytes = self.pool.alloc(sizet(LSP_HEADER_LINE_MAX))
if line is None:
self.error = 1
return None
i: int = 0
while True:
b: int = self._ReadByte()
if b < 0:
self.error = 1
return None
if b == 10:
break
if b == 13:
continue
if i >= LSP_HEADER_LINE_MAX - 1:
self.error = 1
return None
line[i] = char(b)
i += 1
line[i] = 0
return str(line)
def ReadExact(self, n: sizet) -> str:
out: bytes = self.pool.alloc(n + 1)
if out is None:
self.error = 1
return None
copied: sizet = 0
while copied < n:
if self.buf_pos < self.buf_len:
avail: sizet = sizet(self.buf_len - self.buf_pos)
take: sizet = n - copied
if take > avail:
take = avail
string.memcpy(out + copied, self.buf + self.buf_pos, take)
self.buf_pos += int(take)
copied += take
else:
got: ULONG = 0
need: ULONG = ULONG(n - copied)
ok: int = win32.win32file.ReadFile(self.in_handle, out + copied,
need, &got, None)
if ok == 0 or int(got) == 0:
self.error = 1
return None
copied += sizet(int(got))
out[n] = 0
return str(out)
def ReadMessage(self) -> str:
content_len: int = -1
while True:
line: str = self.ReadLine()
if line is None:
return None
if len(line) == 0:
break
if string.strncmp(line, "Content-Length:", 15) == 0:
content_len = string.atoi(line + 15)
if content_len <= 0 or content_len > LSP_MAX_MESSAGE_BYTES:
self.error = 1
return None
return self.ReadExact(sizet(content_len))
def _WriteAll(self, data: str, n: sizet) -> int:
sent: sizet = 0
while sent < n:
written: ULONG = 0
ok: int = win32.win32file.WriteFile(self.out_handle, data + sent,
ULONG(n - sent), &written, None)
if ok == 0 or int(written) == 0:
return -1
sent += sizet(int(written))
return 0
def WriteMessage(self, body: str) -> int:
if body is None:
return -1
blen: sizet = string.strlen(body)
header: bytes = self.pool.alloc(sizet(64))
if header is None:
return -1
viperlib.snprintf(header, sizet(64), "Content-Length: %d\r\n\r\n", int(blen))
if self._WriteAll(str(header), string.strlen(header)) != 0:
return -1
return self._WriteAll(body, blen)
def StderrWrite(msg: str) -> int:
if msg is None:
return 0
h: win32.win32base.HANDLE = win32.win32file.GetStdHandle(win32.win32file.STD_ERROR_HANDLE)
if h == win32.win32base.INVALID_HANDLE_VALUE:
return -1
written: ULONG = 0
win32.win32file.WriteFile(h, msg, ULONG(string.strlen(msg)), &written, None)
return 0
from stdint import *
import stdlib
import string
import viperlib
import memhub
import stdio
define LSP_READ_BUFFER_SIZE = 65536
define LSP_HEADER_LINE_MAX = 512
define LSP_MAX_MESSAGE_BYTES = 16777216
class LspTransport:
pool: memhub.MemManager*
buf: bytes
buf_cap: sizet
buf_len: int
buf_pos: int
eof: int
error: int
def __new__(self, owner: memhub.MemManager*, request: memhub.MemManager*) -> rawptr:
ptr: LspTransport* = (LspTransport*)(owner.alloc(LspTransport.__sizeof__()))
if ptr:
string.memset(ptr, 0, LspTransport.__sizeof__())
return rawptr(ptr)
def __init__(self, owner: memhub.MemManager*, request: memhub.MemManager*):
self.pool = request
self.buf_cap = sizet(LSP_READ_BUFFER_SIZE)
self.buf = stdlib.malloc(self.buf_cap)
self.buf_len = 0
self.buf_pos = 0
self.eof = 0
self.error = 0
def IsValid(self) -> bool:
return self.buf is not None
def _ReadByte(self) -> int:
if self.buf_pos >= self.buf_len:
if self.eof != 0:
return -1
n: sizet = stdio.read_fd(0, self.buf, self.buf_cap)
if n == 0:
self.eof = 1
return -1
self.buf_len = int(n)
self.buf_pos = 0
ch: char = self.buf[self.buf_pos]
self.buf_pos += 1
return int(ch)
def ReadLine(self) -> str:
line: bytes = self.pool.alloc(sizet(LSP_HEADER_LINE_MAX))
if line is None:
self.error = 1
return None
i: int = 0
while True:
b: int = self._ReadByte()
if b < 0:
self.error = 1
return None
if b == 10:
break
if b == 13:
continue
if i >= LSP_HEADER_LINE_MAX - 1:
self.error = 1
return None
line[i] = char(b)
i += 1
line[i] = 0
return str(line)
def ReadExact(self, n: sizet) -> str:
out: bytes = self.pool.alloc(n + 1)
if out is None:
self.error = 1
return None
copied: sizet = 0
while copied < n:
if self.buf_pos < self.buf_len:
avail: sizet = sizet(self.buf_len - self.buf_pos)
take: sizet = n - copied
if take > avail:
take = avail
string.memcpy(out + copied, self.buf + self.buf_pos, take)
self.buf_pos += int(take)
copied += take
else:
got: sizet = stdio.read_fd(0, out + copied, n - copied)
if got == 0:
self.error = 1
return None
copied += got
out[n] = 0
return str(out)
def ReadMessage(self) -> str:
content_len: int = -1
while True:
line: str = self.ReadLine()
if line is None:
return None
if len(line) == 0:
break
if string.strncmp(line, "Content-Length:", 15) == 0:
content_len = string.atoi(line + 15)
if content_len <= 0 or content_len > LSP_MAX_MESSAGE_BYTES:
self.error = 1
return None
return self.ReadExact(sizet(content_len))
def _WriteAll(self, data: str, n: sizet) -> int:
sent: sizet = 0
while sent < n:
written: sizet = stdio.write_fd(1, data + sent, n - sent)
if written == 0:
return -1
sent += written
return 0
def WriteMessage(self, body: str) -> int:
if body is None:
return -1
blen: sizet = string.strlen(body)
header: bytes = self.pool.alloc(sizet(64))
if header is None:
return -1
viperlib.snprintf(header, sizet(64), "Content-Length: %d\r\n\r\n", int(blen))
if self._WriteAll(str(header), string.strlen(header)) != 0:
return -1
return self._WriteAll(body, blen)
def StderrWrite(msg: str) -> int:
if msg is None:
return 0
written: sizet = stdio.write_fd(2, msg, string.strlen(msg))
return 0 if written == string.strlen(msg) else -1
-2
View File
@@ -1,8 +1,6 @@
from stdint import *
import string
import os
import os._win32
import os._posix
import os.path
import memhub
import lib.lsp.LspPools as LspPools
+638 -639
View File
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# linux_link.sh - Windows -> Linux link wrapper for project.linux.vpj.
#
# ViperC emits ELF objects for x86_64-pc-linux-gnu, but Windows has no native
# Linux linker available. Invoke this through WSL:
#
# wsl.exe -d Ubuntu -- bash Tools/linux_link.sh <clang++ args...>
#
# project.linux.vpj sets linker.cmd to exactly that command. WSL starts in the
# calling Windows directory, so the relative script path resolves; every
# Windows path argument (D:/... or D:\...) is converted with wslpath.
set -e
args=()
for a in "$@"; do
case "$a" in
[A-Za-z]:[\\/]*)
args+=("$(wslpath -u "$a")")
;;
*)
args+=("$a")
;;
esac
done
exec clang++ "${args[@]}"
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json",
"name": "ViperC-Linux",
"version": "1.0.0",
"source_dir": "./App",
"build_dir": "./__vpcbuild_linux__",
"compiler": {
"cmd": "llc",
"flags": [
"-filetype=obj",
"-relocation-model=pic",
"-O0"
]
},
"linker": {
"cmd": "wsl.exe -d Ubuntu -- bash Tools/linux_link.sh",
"flags": [
"-fuse-ld=lld",
"-lpthread",
"-lm",
"-ldl",
"-g"
],
"output": "viperc"
},
"includes": [
"../includes"
],
"target": {
"triple": "x86_64-pc-linux-gnu",
"datalayout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
},
"options": {
"slice_level": 3,
"sha1_slice_level": 1,
"strict_mode": true
}
}