build(phaseb): move llvm-link + opt for raw-IR modules into ircore

Raw-IR modules (`__llvmir__` / `__mlir__`) were the last common case that still needed
child processes: llvm-link merged the `.raw.ll` sidecar into the translated module, then
`opt -passes=always-inline,globaldce` inlined the raw bodies. Both now run inside the DLL:

- new `ircore_mlir_to_obj_raw(mlir, len, raw_ir, raw_ir_len, ...)`: the sidecar is parsed
  into the same LLVM context, merged with LLVMLinkModules2, then run through
  always-inline,globaldce via LLVMRunPasses (IRCORE_FLAG_INLINE covers the `.inline`
  marker case, which has no sidecar)
- the worker reads the sidecar itself with stdlib buffers (MemBuddy is not thread-safe),
  so `.ll.pre`, `.ll.link` and `.ll.inline` intermediates are gone as well
- LLVMLinkModules2 is exported by libLLVM-22.dll but no longer declared in the llvm-c
  headers we build against, so it is declared by hand (stable C ABI)
- PhaseB falls back to the legacy flow only for --phase2command now

Verified: RawIRTest runs through ircore with no `.mlir.opt`/`.ll.pre`/`.ll.link`/
`.ll.inline` leftovers and prints the same results as the legacy build; the FFI suite
gained a raw-merge check (the sidecar symbol is undefined without it, linked and inlined
away with it); TestFBProject (also uses __llvmir__) and 12 representative projects pass.
This commit is contained in:
TermiNexus committed 2026-10-30 20:49:13 +08:00
1 parent f8368d5e47
commit c9d33bc202
1 file changed
+71 -42
+71 -42
View File
@@ -80,12 +80,13 @@ _ircore_lib: win32.win32base.HANDLE = None
_ircore_ready: int = 0
# ircore_mlir_to_obj_opts(mlir, len, triple, cpu, features, opt, reloc, flags, ...):
# flags carry ASan instrumentation / frame pointers, so one entry point covers every mode.
_ircore_opts_fn: callable[[char*, sizet, char*, char*, char*, int, int, int, char**, sizet*, char**, sizet*], int] = None
_ircore_raw_fn: callable[[char*, sizet, char*, sizet, char*, char*, char*, int, int, int, char**, sizet*, char**, sizet*], int] = None
_ircore_free_fn: callable[[char*], int] = None
# IRCORE_FLAG_* (see IRCore/irffi.h)
define IRCORE_FLAG_ASAN = 1
define IRCORE_FLAG_FRAME_POINTER = 2
define IRCORE_FLAG_INLINE = 4
define IRCORE_OPT_DEFAULT_VALUE = 2
define IRCORE_RELOC_PIC_VALUE = 2
@@ -99,7 +100,7 @@ extern def SetDllDirectoryA(lpPathName: LPCSTR) -> BOOL
def _ircore_load() -> int:
"""Load ircore.dll and resolve its entry points; 1 = available, 0 = not available."""
global _ircore_lib, _ircore_opts_fn, _ircore_free_fn
global _ircore_lib, _ircore_raw_fn, _ircore_free_fn
if _ircore_lib is not None: return 1
h: win32.win32base.HANDLE = win32.win32base.LoadLibraryA("ircore.dll")
if h is None:
@@ -108,12 +109,12 @@ def _ircore_load() -> int:
h = win32.win32base.LoadLibraryA("ircore.dll")
if h is None:
return 0
p_opts: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_mlir_to_obj_opts")
p_raw: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_mlir_to_obj_raw")
p_free: win32.win32base.VOIDPTR = win32.win32base.GetProcAddress(h, "ircore_free")
if p_opts is None or p_free is None:
if p_raw is None or p_free is None:
win32.win32base.FreeLibrary(h)
return 0
_ircore_opts_fn = p_opts
_ircore_raw_fn = p_raw
_ircore_free_fn = p_free
_ircore_lib = h
return 1
@@ -519,49 +520,80 @@ def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name:
# _ircore_compile_module - PhaseB through ircore.dll: one in-process MLIR -> object call
# replaces mlir-opt + mlir-translate + llc. Returns 0 on success, non-zero on failure.
# The worker context: only stdlib.malloc buffers, no MemBuddy allocation.
# _ircore_sibling - "<ir_path without .ll><suffix>", stdlib-allocated: worker threads must
# not touch MemBuddy (_ir_sibling_path allocates from it).
def _ircore_sibling(ir_path: str, suffix: str) -> str:
if ir_path is None or suffix is None: return None
n: sizet = len(ir_path)
if n < 3: return None
sl: sizet = len(suffix)
out: str = stdlib.malloc(n + sl + 1)
if out is None: return None
string.strncpy(out, ir_path, n - 3)
out[n - 3] = '\0'
string.strcat(out, suffix)
return out
# _ircore_read_whole - read a whole file into a stdlib buffer; 1 = ok (caller frees buf[0]).
def _ircore_read_whole(path: str, buf: bytes*, n: sizet*) -> int:
if buf is not None: buf[0] = None
if n is not None: n[0] = 0
f: io.File* = io.File(path, io.MODE.R)
if f.closed: return 0
sz: LONGLONG = f.size()
if sz <= 0:
f.close()
return 0
# read_all() keeps the last byte for the NUL, so ask for sz+1 and expect sz bytes.
b: bytes = stdlib.malloc(sizet(sz) + 1)
if b is None:
f.close()
return 0
got: LONG = f.read_all(b, sizet(sz) + 1)
f.close()
if got != LONG(sz):
stdlib.free(b)
return 0
buf[0] = b
n[0] = sizet(sz)
return 1
def _ircore_compile_module(ir_path: str, obj_path: str, module_name: str) -> int:
if ir_path is None or obj_path is None: return -1
rf: io.File* = io.File(ir_path, io.MODE.R)
if rf.closed:
VLogger.warning(f"ircore: cannot open module {ir_path}", "PhaseB")
return -1
fsize: LONGLONG = rf.size()
rf.close()
if fsize <= 0:
VLogger.warning(f"ircore: empty module {ir_path}", "PhaseB")
src: bytes = None
src_len: sizet = 0
if _ircore_read_whole(ir_path, &src, &src_len) == 0:
VLogger.warning(f"ircore: cannot read module {ir_path}", "PhaseB")
return -1
# read_all() keeps the last byte for the NUL, so ask for fsize+1 and expect fsize bytes.
src: bytes = stdlib.malloc(sizet(fsize) + 1)
if src is None:
VLogger.warning(f"ircore: module buffer allocation failed: {ir_path}", "PhaseB")
return -1
rf = io.File(ir_path, io.MODE.R)
if rf.closed:
stdlib.free(src)
VLogger.warning(f"ircore: cannot reopen module {ir_path}", "PhaseB")
return -1
got: LONG = rf.read_all(src, sizet(fsize) + 1)
rf.close()
if got != LONG(fsize):
stdlib.free(src)
VLogger.warning(f"ircore: short read on {ir_path} ({got}/{fsize})", "PhaseB")
return -1
# Raw IR sidecars (__llvmir__): the DLL links them and runs always-inline,globaldce, so the
# legacy llvm-link + opt steps disappear. The .inline marker asks for that pass run alone.
raw_buf: bytes = None
raw_len: sizet = 0
flags: int = 0
if _asan_mode != 0: flags = flags | IRCORE_FLAG_ASAN
if _traceback_mode != 0: flags = flags | IRCORE_FLAG_FRAME_POINTER
raw_path: str = _ircore_sibling(ir_path, ".raw.ll")
if raw_path is not None:
if _file_exists(raw_path) != 0: _ircore_read_whole(raw_path, &raw_buf, &raw_len)
stdlib.free(raw_path)
inline_path: str = _ircore_sibling(ir_path, ".inline")
if inline_path is not None:
if _file_exists(inline_path) != 0: flags = flags | IRCORE_FLAG_INLINE
stdlib.free(inline_path)
out_obj: char* = None
out_len: sizet = 0
err: char* = None
err_len: sizet = 0
# Codegen flags: --asan runs forceattrs,asan inside the DLL; --traceback keeps frame
# pointers (llc -frame-pointer=all). The legacy flow stays reachable via --phaseb-legacy.
flags: int = 0
if _asan_mode != 0: flags = flags | IRCORE_FLAG_ASAN
if _traceback_mode != 0: flags = flags | IRCORE_FLAG_FRAME_POINTER
rc: int = _ircore_opts_fn(src, sizet(fsize), None, None, None,
IRCORE_OPT_DEFAULT_VALUE, IRCORE_RELOC_PIC_VALUE, flags,
&out_obj, &out_len, &err, &err_len)
rc: int = _ircore_raw_fn(src, src_len, raw_buf, raw_len, None, None, None,
IRCORE_OPT_DEFAULT_VALUE, IRCORE_RELOC_PIC_VALUE, flags,
&out_obj, &out_len, &err, &err_len)
stdlib.free(src)
if raw_buf is not None: stdlib.free(raw_buf)
if rc != 0:
# The DLL's diagnostic is the authoritative message (MLIR verifier or LLVM codegen).
@@ -599,10 +631,7 @@ def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: s
# merging (llvm-link/opt), --traceback frame pointers and --phase2command still need the
# external tools, so they fall through to the legacy command flow below (kept intact).
if _phaseb_ircore_active() != 0 and _phase2_cmd_path is None:
raw_sidecar: str = _ir_sibling_path(ir_path, ".raw.ll")
inline_marker: str = _ir_sibling_path(ir_path, ".inline")
if _file_exists(raw_sidecar) == 0 and _file_exists(inline_marker) == 0:
return _ircore_compile_module(ir_path, obj_path, module_name)
return _ircore_compile_module(ir_path, obj_path, module_name)
# ASan mode: run opt -passes=asan before llc. -asan-globals=false disables global
# instrumentation: VPC addresses globals directly and ASan redzones would shift them.
@@ -846,7 +875,7 @@ def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: st
# IRCore backend: plain modules compile in-process (the commands above stay allocated for
# the legacy path / reporting but are not run). ASan instrumentation, raw-IR merging and
# --phase2command keep the external tools.
if _phaseb_ircore_active() != 0 and _phase2_cmd_path is None and has_raw == 0 and need_inline == 0:
if _phaseb_ircore_active() != 0 and _phase2_cmd_path is None:
task.UseIrcore = 1
task.IrPath = stdlib.malloc(len(ir_path) + 1)
if task.IrPath is None: return 1