feat(codegen): inline arbitrary LLVM IR and raw MLIR blocks

This commit is contained in:
TermiNexus committed 2026-10-27 14:13:38 +08:00
1 parent 645e193729
commit 6a15adbad0
3 files changed
+407 -72

No files matched your search

+165 -13
View File
@@ -51,6 +51,22 @@ _phase2_bat_init: int = 0
MLIR_OPT_CMD: str = "mlir-opt"
MLIR_TRANSLATE_CMD: str = "mlir-translate"
# Raw-IR support (__llvmir__/__mlir__): raw LLVM function bodies are split into <sha1>.raw.ll
# and merged with the lowered module; always-inline then removes the call boundary so the raw
# body behaves like inline IR again.
LLVM_LINK_CMD: str = "llvm-link"
INLINE_PASSES: str = "always-inline,globaldce"
define RAW_LLVM_MARKER = "//__VPC_RAW_LLVM__"
def _file_exists(path: str) -> int:
"""1 when path exists (opens it for reading and closes), else 0."""
if path is None: return 0
f: io.File* = io.File(path, io.MODE.R)
if f.closed: return 0
f.close()
return 1
def _ir_sibling_path(ir_path: str, suffix: str) -> str:
"""Derive a sibling path from <...>.ll: strip the .ll suffix and append suffix; mbuddy-allocated."""
@@ -339,9 +355,20 @@ def build_sliced_path(base_dir: str, sha1: str, ext: str) -> str:
def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: str) -> int:
"""Write the IR buffer to a .ll file; 0 on success, non-zero on failure."""
"""Write the IR buffer to <sha1>.ll; raw LLVM text is split into <sha1>.raw.ll."""
if ir_buf is None or output_dir is None or module_name is None: return 1
# Raw LLVM functions (__llvmir__) follow a marker after the MLIR module: split them out so
# mlir-opt only ever receives the MLIR module.
raw_pos: str = string.strstr(ir_buf, RAW_LLVM_MARKER)
mlir_len: sizet = ir_len
raw_start: sizet = 0
if raw_pos is not None:
mlir_len = sizet(uint64(raw_pos) - uint64(ir_buf))
raw_start = mlir_len + string.strlen(RAW_LLVM_MARKER)
while raw_start < ir_len and (ir_buf[raw_start] == '\r' or ir_buf[raw_start] == '\n'):
raw_start += 1
# build sliced path: output_dir/{sha1 prefix}/{module_name}.ll
path: str = build_sliced_path(output_dir, module_name, "ll")
if path is None: return 1
@@ -350,9 +377,30 @@ def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name:
f: io.File* = io.File(path, io.MODE.W)
if f.closed: return 1
written: int64 = f.write(ir_buf, ir_len)
written: int64 = f.write(ir_buf, mlir_len)
f.close()
if written < 0: return 1
if raw_pos is not None and ir_len > raw_start:
raw_path: str = build_sliced_path(output_dir, module_name, "raw.ll")
if raw_path is None: return 1
rf: io.File* = io.File(raw_path, io.MODE.W)
if rf.closed: return 1
rw: int64 = rf.write(ir_buf + raw_start, ir_len - raw_start)
rf.close()
if rw < 0: return 1
# Inline-pass marker: raw LLVM functions and/or a raw-MLIR body (__mlir__) are present, so
# the object must go through opt -passes=always-inline,globaldce before llc.
need_inline: int = 0
if raw_pos is not None: need_inline = 1
elif string.strstr(ir_buf, ".__vpc_mlir_") is not None: need_inline = 1
if need_inline != 0:
flag_path: str = build_sliced_path(output_dir, module_name, "inline")
if flag_path is None: return 1
ff: io.File* = io.File(flag_path, io.MODE.W)
if ff.closed: return 1
ff.close()
return 0
@@ -369,22 +417,56 @@ def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: s
compile_input: str = ir_path
# ---- MLIR lowering (fixed, no switch): mlir-opt (validate) -> mlir-translate ----
# Must run before ASan opt and llc, replacing compile_input with <...>.ll.local.
# Must run before ASan opt and llc, replacing compile_input with the final LLVM IR file.
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
if ll_local_path is None or mlir_opt_out is None: return 1
raw_path: str = _ir_sibling_path(ir_path, ".raw.ll")
inline_flag: str = _ir_sibling_path(ir_path, ".inline")
ll_pre_path: str = _ir_sibling_path(ir_path, ".ll.pre")
ll_link_path: str = _ir_sibling_path(ir_path, ".ll.link")
ll_inline_path: str = _ir_sibling_path(ir_path, ".ll.inline")
if ll_local_path is None or mlir_opt_out is None or raw_path is None or inline_flag is None \
or ll_pre_path is None or ll_link_path is None or ll_inline_path is None: return 1
has_raw: int = _file_exists(raw_path)
translate_out: str = ll_local_path
if has_raw != 0: translate_out = ll_pre_path
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
cmd_mo: str = _mbuddy.alloc(mo_len)
if cmd_mo is None: return 1
viperlib.snprintf(cmd_mo, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(translate_out) + 40
cmd_mt: str = _mbuddy.alloc(mt_len)
if cmd_mt is None: return 1
viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
viperlib.snprintf(cmd_mt, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, translate_out)
# Raw LLVM functions (__llvmir__): merge the sidecar into the lowered module.
cmd_link: str = None
if has_raw != 0:
lk_len: sizet = len(LLVM_LINK_CMD) + len(ll_pre_path) + len(raw_path) + len(ll_link_path) + 32
cmd_link = _mbuddy.alloc(lk_len)
if cmd_link is None: return 1
viperlib.snprintf(cmd_link, lk_len, "%s %s %s -S -o %s", LLVM_LINK_CMD, ll_pre_path, raw_path, ll_link_path)
# Always-inline pass: restores inline semantics for raw functions/blocks (raw LLVM bodies and
# raw MLIR bodies both become ordinary calls first).
need_inline: int = 0
if has_raw != 0: need_inline = 1
elif _file_exists(inline_flag) != 0: need_inline = 1
cmd_inline: str = None
inline_src: str = ll_local_path
if has_raw != 0: inline_src = ll_link_path
if need_inline != 0:
in_len: sizet = len("opt -passes=") + len(INLINE_PASSES) + len(inline_src) + len(ll_inline_path) + 32
cmd_inline = _mbuddy.alloc(in_len)
if cmd_inline is None: return 1
viperlib.snprintf(cmd_inline, in_len, "opt -passes=%s -S %s -o %s", INLINE_PASSES, inline_src, ll_inline_path)
if _phase2_cmd_path is not None:
# write the command only (must precede the ASan/llc commands below)
# write the commands only (must precede the ASan/llc commands below)
_phase2_write_cmd(cmd_mo)
_phase2_write_cmd(cmd_mt)
if cmd_link is not None: _phase2_write_cmd(cmd_link)
if cmd_inline is not None: _phase2_write_cmd(cmd_inline)
else:
mo_rc: int = _run_cmd_wait(cmd_mo)
if mo_rc != 0:
@@ -394,7 +476,18 @@ def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: s
if mt_rc != 0:
VLogger.error(f"mlir-translate failed (module={module_name}, exit={mt_rc}, cmd={cmd_mt})", "MLIR")
return 1
compile_input = ll_local_path
if cmd_link is not None:
lk_rc: int = _run_cmd_wait(cmd_link)
if lk_rc != 0:
VLogger.error(f"llvm-link failed for raw LLVM IR (module={module_name}, exit={lk_rc}, cmd={cmd_link})", "RAWIR")
return 1
if cmd_inline is not None:
inl_rc: int = _run_cmd_wait(cmd_inline)
if inl_rc != 0:
VLogger.error(f"opt always-inline failed for raw IR (module={module_name}, exit={inl_rc}, cmd={cmd_inline})", "RAWIR")
return 1
if need_inline != 0: compile_input = ll_inline_path
else: compile_input = ll_local_path
# --phase2command mode: do not run opt/llc; write the opt (ASan) + llc commands to the bat
if _phase2_cmd_path is not None:
@@ -456,6 +549,8 @@ define LLC_STAGE_MLIR_OPT = 1
define LLC_STAGE_MLIR_TRANSLATE = 2
define LLC_STAGE_ASAN_OPT = 3
define LLC_STAGE_LLC = 4
define LLC_STAGE_RAW_LINK = 5
define LLC_STAGE_RAW_INLINE = 6
class LlcTask:
@@ -472,6 +567,9 @@ class LlcTask:
MlirTranslateCmd: bytes # mlir-translate lowering command
# failure stage (LLC_STAGE_*, see the constants above); appended at the end.
FailStage: int # worker-written: which step failed
# Raw-IR support (__llvmir__/__mlir__); appended at the end, None when the module has none.
RawLinkCmd: bytes # llvm-link merge of <sha1>.raw.ll into the lowered module
RawInlineCmd: bytes # opt -passes=always-inline,globaldce (raw body -> inline)
def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int:
@@ -490,19 +588,45 @@ def init_llc_task(task: LlcTask*, ir_path: str, output_dir: str, module_name: st
# compile_ll_to_obj path (else llc ignores the instrumentation and ASan is half-blind).
compile_input: str = ir_path
# MLIR lowering (fixed): the worker runs mlir-opt (validate) -> mlir-translate first, so
# the later ASan OptCmd / LlcCmd input must be <...>.ll.local.
# the later ASan OptCmd / LlcCmd input must be the final LLVM IR file.
ll_local_path: str = _ir_sibling_path(ir_path, ".ll.local")
mlir_opt_out: str = _ir_sibling_path(ir_path, ".mlir.opt")
if ll_local_path is None or mlir_opt_out is None: return 1
raw_path: str = _ir_sibling_path(ir_path, ".raw.ll")
inline_flag: str = _ir_sibling_path(ir_path, ".inline")
ll_pre_path: str = _ir_sibling_path(ir_path, ".ll.pre")
ll_link_path: str = _ir_sibling_path(ir_path, ".ll.link")
ll_inline_path: str = _ir_sibling_path(ir_path, ".ll.inline")
if ll_local_path is None or mlir_opt_out is None or raw_path is None or inline_flag is None \
or ll_pre_path is None or ll_link_path is None or ll_inline_path is None: return 1
has_raw: int = _file_exists(raw_path)
translate_out: str = ll_local_path
if has_raw != 0: translate_out = ll_pre_path
mo_len: sizet = len(MLIR_OPT_CMD) + len(ir_path) + len(mlir_opt_out) + 16
task.MlirOptCmd = stdlib.malloc(mo_len)
if task.MlirOptCmd is None: return 1
viperlib.snprintf(task.MlirOptCmd, mo_len, "%s %s -o %s", MLIR_OPT_CMD, ir_path, mlir_opt_out)
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(ll_local_path) + 40
mt_len: sizet = len(MLIR_TRANSLATE_CMD) + len(mlir_opt_out) + len(translate_out) + 40
task.MlirTranslateCmd = stdlib.malloc(mt_len)
if task.MlirTranslateCmd is None: return 1
viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, ll_local_path)
compile_input = ll_local_path
viperlib.snprintf(task.MlirTranslateCmd, mt_len, "%s --mlir-to-llvmir %s -o %s", MLIR_TRANSLATE_CMD, mlir_opt_out, translate_out)
compile_input = translate_out
# Raw LLVM functions (__llvmir__): merge the sidecar into the lowered module.
if has_raw != 0:
lk_len: sizet = len(LLVM_LINK_CMD) + len(ll_pre_path) + len(raw_path) + len(ll_link_path) + 32
task.RawLinkCmd = stdlib.malloc(lk_len)
if task.RawLinkCmd is None: return 1
viperlib.snprintf(task.RawLinkCmd, lk_len, "%s %s %s -S -o %s", LLVM_LINK_CMD, ll_pre_path, raw_path, ll_link_path)
compile_input = ll_link_path
# Always-inline pass: restores inline semantics for raw functions/blocks.
need_inline: int = 0
if has_raw != 0: need_inline = 1
elif _file_exists(inline_flag) != 0: need_inline = 1
if need_inline != 0:
in_len: sizet = len("opt -passes=") + len(INLINE_PASSES) + len(compile_input) + len(ll_inline_path) + 32
task.RawInlineCmd = stdlib.malloc(in_len)
if task.RawInlineCmd is None: return 1
viperlib.snprintf(task.RawInlineCmd, in_len, "opt -passes=%s -S %s -o %s", INLINE_PASSES, compile_input, ll_inline_path)
compile_input = ll_inline_path
# ASan mode: opt instrumentation command (the worker runs MLIR lowering/opt before llc,
# so LlcCmd's input must point at asan_path; failure sets Result).
if _asan_mode != 0:
@@ -537,6 +661,12 @@ def free_llc_task(task: LlcTask*):
if task.MlirTranslateCmd is not None:
stdlib.free(task.MlirTranslateCmd)
task.MlirTranslateCmd = None
if task.RawLinkCmd is not None:
stdlib.free(task.RawLinkCmd)
task.RawLinkCmd = None
if task.RawInlineCmd is not None:
stdlib.free(task.RawInlineCmd)
task.RawInlineCmd = None
if task.ModuleSha1 is not None:
stdlib.free(task.ModuleSha1)
task.ModuleSha1 = None
@@ -617,6 +747,20 @@ def _llc_task_worker(arg: rawptr) -> rawptr:
task.FailStage = LLC_STAGE_MLIR_TRANSLATE
task.Result = 1
return None
if task.RawLinkCmd is not None:
rl_rc: int = _run_cmd_wait(task.RawLinkCmd)
if rl_rc != 0:
task.ExitCode = rl_rc
task.FailStage = LLC_STAGE_RAW_LINK
task.Result = 1
return None
if task.RawInlineCmd is not None:
ri_rc: int = _run_cmd_wait(task.RawInlineCmd)
if ri_rc != 0:
task.ExitCode = ri_rc
task.FailStage = LLC_STAGE_RAW_INLINE
task.Result = 1
return None
if task.OptCmd is not None:
opt_rc: int = _run_cmd_wait(task.OptCmd)
if opt_rc != 0:
@@ -645,6 +789,8 @@ def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int:
if tsk_p is None: continue
if tsk_p.MlirOptCmd is not None: _phase2_write_cmd(tsk_p.MlirOptCmd)
if tsk_p.MlirTranslateCmd is not None: _phase2_write_cmd(tsk_p.MlirTranslateCmd)
if tsk_p.RawLinkCmd is not None: _phase2_write_cmd(tsk_p.RawLinkCmd)
if tsk_p.RawInlineCmd is not None: _phase2_write_cmd(tsk_p.RawInlineCmd)
if tsk_p.OptCmd is not None: _phase2_write_cmd(tsk_p.OptCmd)
if tsk_p.LlcCmd is not None: _phase2_write_cmd(tsk_p.LlcCmd)
VLogger.info(f"phase2command mode: {task_count} llc commands written to {_phase2_cmd_path}", "phase2cmd")
@@ -690,6 +836,12 @@ def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int:
elif st == LLC_STAGE_MLIR_TRANSLATE:
stage_name = "mlir-translate lowering"
stage_cmd = tsk2.MlirTranslateCmd
elif st == LLC_STAGE_RAW_LINK:
stage_name = "llvm-link raw IR merge"
stage_cmd = tsk2.RawLinkCmd
elif st == LLC_STAGE_RAW_INLINE:
stage_name = "opt always-inline (raw IR)"
stage_cmd = tsk2.RawInlineCmd
elif st == LLC_STAGE_ASAN_OPT:
stage_name = "opt ASan instrumentation"
stage_cmd = tsk2.OptCmd
+236 -57
View File
@@ -846,31 +846,33 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase):
return None
def translate_c_llvmir(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Inline raw LLVM IR text into the current basic block; returns None."""
if cl is None or builder is None or pool is None or trans is None: return None
def _raw_ir_template(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator*, in_vals: sizet*, out_vals: sizet*, counts: int*) -> str:
"""Expand a raw-IR template; irinp -> %in<N>, irout -> %vpc_out<N>."""
cargs: list[ast.AST*]* = cl.args
if cargs is None or len(cargs) < 1: self.Gen.TypeGenErr.fatal_error(cl, "c.LLVMIR missing template argument", "TypeError")
# LOut writeback records (alloca pointer + register name), up to LLVMIR_MAX_OPS
out_alloca: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS)
out_regname: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS)
if out_alloca is None or out_regname is None: return None
string.memset(out_alloca, 0, 8 * LLVMIR_MAX_OPS)
string.memset(out_regname, 0, 8 * LLVMIR_MAX_OPS)
out_count: int = 0
# Template text buffer
text: str = pool.alloc(LLVMIR_TEMPLATE_BUF)
if text is None: return None
text[0] = '\0'
# 1. Parse the IR template (JoinedStr or Constant), replacing LInp/LOut/exprs with value text
if cargs is None or len(cargs) < 1:
self.Gen.TypeGenErr.fatal_error(cl, "raw IR missing the template argument", "TypeError")
return None
first_arg: ast.AST* = cargs.get(0)
if first_arg is None: return None
# Capacity: literal fragments plus one parameter name per formatted value.
cap: sizet = 64
tk: int = first_arg.kind()
if tk == ast.ASTKind.JoinedStr:
js_cap: ast.JoinedStr* = ast.JoinedStr*(first_arg)
if js_cap.values is not None:
cap += len(js_cap.values) * 64
for vi_cap in range(int(len(js_cap.values))):
part_cap: ast.AST* = js_cap.values.get(vi_cap)
if part_cap is not None and part_cap.kind() == ast.ASTKind.Constant:
cn_cap: ast.Constant* = ast.Constant*(part_cap)
if cn_cap.str_val is not None: cap += string.strlen(cn_cap.str_val)
elif tk == ast.ASTKind.Constant:
cn_only: ast.Constant* = ast.Constant*(first_arg)
if cn_only.str_val is not None: cap += string.strlen(cn_only.str_val)
text: str = pool.alloc(cap)
if text is None: return None
text[0] = '\0'
if tk == ast.ASTKind.JoinedStr:
js: ast.JoinedStr* = ast.JoinedStr*(first_arg)
@@ -883,15 +885,13 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase):
pk: int = part.kind()
if pk == ast.ASTKind.Constant:
# String literal fragment
cn: ast.Constant* = ast.Constant*(part)
if cn.str_val is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn.str_val, "c.LLVMIR")
if cn.str_val is not None: self._asm_append_cstr(text, cap, cn.str_val, "raw IR")
elif pk == ast.ASTKind.FormattedValue:
fv: ast.FormattedValue* = ast.FormattedValue*(part)
fexpr: ast.AST* = fv.value
if fexpr is None: continue
# Detect c.LInp / c.LOut
if fexpr.kind() == ast.ASTKind.Call:
fcall: ast.Call* = ast.Call*(fexpr)
attr_nm: str = self._asm_is_c_call(fcall)
@@ -901,55 +901,234 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase):
val_node: ast.AST* = fargs.get(0)
if string.strcmp(attr_nm, "LInp") == 0:
# Input operand: translate to an LLVM value reference text (%x or constant)
inp_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, None, 0, trans)
if inp_val is not None and inp_val.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, inp_val.Name, "c.LLVMIR")
if inp_val is None or inp_val.Ty is None:
self.Gen.TypeGenErr.fatal_error(fcall, "raw IR input operand cannot be translated", "TypeError")
return None
if counts[0] >= LLVMIR_MAX_OPS:
self.Gen.TypeGenErr.fatal_error(fcall, f"raw IR has more than {LLVMIR_MAX_OPS} input operands (compiler limit); split the block", "TypeError")
return None
in_vals[counts[0]] = sizet(inp_val)
pname: str = pool.alloc(32)
if pname is not None:
viperlib.snprintf(pname, 32, "%%in%d", counts[0])
self._asm_append_cstr(text, cap, pname, "raw IR")
counts[0] += 1
continue
elif string.strcmp(attr_nm, "LOut") == 0:
# Output operand: allocate a unique SSA register
# %llvmirN and record the writeback to the alloca.
# Output operand: the variable's alloca is passed as a pointer
# parameter; the raw body defines %vpc_out<N> and the generated
# epilogue stores it back into that alloca.
if val_node is not None and val_node.kind() == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(val_node)
if nm.id is not None:
alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if alloca is None or out_count >= LLVMIR_MAX_OPS: self.Gen.TypeGenErr.fatal_error(fcall, f"c.LOut variable '{nm.id}' is undefined or the output count exceeds {LLVMIR_MAX_OPS}", "TypeError")
self.Gen.CallGenBase.LLVMIRRegSeq += 1
reg_buf: str = pool.alloc(32)
if reg_buf is not None:
viperlib.snprintf(reg_buf, 32, "%%llvmir_%d", self.Gen.CallGenBase.LLVMIRRegSeq)
self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, reg_buf, "c.LLVMIR")
out_alloca[out_count] = sizet(alloca)
out_regname[out_count] = sizet(reg_buf)
out_count += 1
if alloca is None or alloca.Ty is None or alloca.Ty.Pointee == 0:
self.Gen.TypeGenErr.fatal_error(fcall, f"raw IR output variable '{nm.id}' is not a local variable", "TypeError")
return None
if counts[1] >= LLVMIR_MAX_OPS:
self.Gen.TypeGenErr.fatal_error(fcall, f"raw IR has more than {LLVMIR_MAX_OPS} output operands (compiler limit); split the block", "TypeError")
return None
out_vals[counts[1]] = sizet(alloca)
rname: str = pool.alloc(32)
if rname is not None:
viperlib.snprintf(rname, 32, "%%vpc_out%d", counts[1])
self._asm_append_cstr(text, cap, rname, "raw IR")
counts[1] += 1
continue
continue
# Non-LInp/LOut f-string expression: translate and embed the value text
# Ordinary expression: embed its value reference text.
ival: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, fexpr, None, 0, trans)
if ival is not None and ival.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, ival.Name, "c.LLVMIR")
if ival is not None and ival.Name is not None: self._asm_append_cstr(text, cap, ival.Name, "raw IR")
elif tk == ast.ASTKind.Constant:
cn2: ast.Constant* = ast.Constant*(first_arg)
if cn2.str_val is not None:
# A bare strcpy is unbounded and could overflow the pool block, so
# use the same bounded append as the JoinedStr branch.
self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn2.str_val, "c.LLVMIR")
if cn2.str_val is not None: self._asm_append_cstr(text, cap, cn2.str_val, "raw IR")
# 2. Embed the template text directly (no instruction parsing) + LOut writeback
mlirlite.block_append_text(pool, builder.CurBlock, text)
return text
# LOut writeback: store <ty> %llvmirN, <ty>* %var
oi: int
for oi in range(int(out_count)):
oa: mlirlite.Value* = mlirlite.Value*(out_alloca[oi])
rn: str = str(out_regname[oi])
if oa is None or oa.Ty is None or oa.Ty.Pointee == 0 or rn is None: continue
reg_val: mlirlite.Value* = mlirlite.new_value(pool)
if reg_val is None: continue
reg_val.Ty = oa.Ty.Pointee
reg_val.Name = rn
reg_val.IsConst = 0
mlirlite.build_store(builder, reg_val, oa)
def _raw_ir_emit_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, name: str, in_vals: sizet*, in_count: int, out_vals: sizet*, out_count: int) -> mlirlite.Value*:
"""Emit the call to the generated raw function (inputs, then output pointers)."""
total: int = in_count + out_count
head: mlirlite.Value* = None
prev: mlirlite.Value* = None
for k in range(total):
v: mlirlite.Value* = None
if k < in_count: v = mlirlite.Value*(in_vals[k])
else: v = mlirlite.Value*(out_vals[k - in_count])
if v is None: return None
v.Next = None
if head is None: head = v
else: prev.Next = v
prev = v
void_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
return mlirlite.build_call(builder, name, head, total, void_ty, 0)
return None
def _raw_ir_func_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, tag: str) -> str:
"""Unique symbol name for one generated raw function."""
seq: int = self.Gen.CallGenBase.LLVMIRRegSeq + 1
self.Gen.CallGenBase.LLVMIRRegSeq = seq
sha1: str = trans.ModuleSha1
if sha1 is None: sha1 = "vpc"
cap: sizet = string.strlen(sha1) + string.strlen(tag) + 48
name: str = pool.alloc(cap)
if name is None: return None
viperlib.snprintf(name, cap, "%s.%s_%d", sha1, tag, seq)
return name
def translate_c_llvmir(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Inline raw LLVM IR: emit a sidecar function whose body is the user text, call it.
The body is arbitrary LLVM IR parsed by LLVM itself (not MLIR), so every legal
instruction/metadata form works. Inputs become parameters (%in<N>), output variables
become pointer parameters; the generated epilogue stores %vpc_out<N> back.
"""
if cl is None or builder is None or pool is None or trans is None: return None
in_vals: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS)
out_vals: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS)
counts: int* = pool.alloc(8 * 2)
if in_vals is None or out_vals is None or counts is None: return None
string.memset(in_vals, 0, 8 * LLVMIR_MAX_OPS)
string.memset(out_vals, 0, 8 * LLVMIR_MAX_OPS)
counts[0] = 0
counts[1] = 0
body: str = self._raw_ir_template(pool, builder, mod, cl, trans, in_vals, out_vals, counts)
if body is None: return None
in_count: int = counts[0]
out_count: int = counts[1]
name: str = self._raw_ir_func_name(pool, trans, "__vpc_llvmir")
if name is None: return None
# Sidecar definition: linkonce_odr + alwaysinline so llvm-link merges it with the
# declaration and opt -passes=always-inline restores inline semantics.
cap: sizet = string.strlen(body) + 256 + sizet(in_count + out_count) * 4352
fn_buf: str = pool.alloc(cap)
ty_buf: str = pool.alloc(4096)
if fn_buf is None or ty_buf is None: return None
fn_buf[0] = '\0'
viperlib.snprintf(fn_buf, cap, "define linkonce_odr void @\"%s\"(", name)
pos: sizet = string.strlen(fn_buf)
first: int = 1
for k in range(in_count):
v: mlirlite.Value* = mlirlite.Value*(in_vals[k])
if v is None or v.Ty is None: return None
ty_buf[0] = '\0'
mlirlite.LlvmTypePrint(ty_buf, 4096, v.Ty, pool)
if first == 0:
viperlib.snprintf(fn_buf + pos, cap - pos, ", ")
pos = string.strlen(fn_buf)
viperlib.snprintf(fn_buf + pos, cap - pos, "%s %%in%d", ty_buf, k)
pos = string.strlen(fn_buf)
first = 0
for k in range(out_count):
a: mlirlite.Value* = mlirlite.Value*(out_vals[k])
if a is None or a.Ty is None or a.Ty.Pointee == 0: return None
ty_buf[0] = '\0'
mlirlite.LlvmTypePrint(ty_buf, 4096, a.Ty, pool)
if first == 0:
viperlib.snprintf(fn_buf + pos, cap - pos, ", ")
pos = string.strlen(fn_buf)
viperlib.snprintf(fn_buf + pos, cap - pos, "%s %%out%d", ty_buf, k)
pos = string.strlen(fn_buf)
first = 0
viperlib.snprintf(fn_buf + pos, cap - pos, ") alwaysinline {\n")
pos = string.strlen(fn_buf)
viperlib.snprintf(fn_buf + pos, cap - pos, "%s", body)
pos = string.strlen(fn_buf)
# Stores land in the body's last block: LLVM has no implicit fallthrough, so a separate
# epilogue block would need an explicit branch (impossible when the user text already
# ends with a terminator). Appending in-block matches the old inline semantics.
viperlib.snprintf(fn_buf + pos, cap - pos, "\n")
pos = string.strlen(fn_buf)
for k in range(out_count):
a2: mlirlite.Value* = mlirlite.Value*(out_vals[k])
ty_buf[0] = '\0'
mlirlite.LlvmTypePrint(ty_buf, 4096, a2.Ty.Pointee, pool)
viperlib.snprintf(fn_buf + pos, cap - pos, " store %s %%vpc_out%d, ptr %%out%d\n", ty_buf, k, k)
pos = string.strlen(fn_buf)
viperlib.snprintf(fn_buf + pos, cap - pos, " ret void\n}\n")
if mlirlite.module_append_raw_text(pool, mod, fn_buf) != 0:
self.Gen.TypeGenErr.fatal_error(cl, "raw LLVM IR: failed to record the generated function text", "MemoryError")
return None
fn: mlirlite.Function* = mlirlite.create_declare(pool, mod, name, mlirlite.Void(pool))
if fn is None: return None
for k in range(in_count):
v2: mlirlite.Value* = mlirlite.Value*(in_vals[k])
mlirlite.add_param(pool, fn, v2.Ty, None)
for k in range(out_count):
a3: mlirlite.Value* = mlirlite.Value*(out_vals[k])
mlirlite.add_param(pool, fn, a3.Ty, None)
return self._raw_ir_emit_call(pool, builder, name, in_vals, in_count, out_vals, out_count)
def translate_c_mlir(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, cl: ast.Call*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Inline raw MLIR: emit a raw-body llvm.func and call it.
The body is spliced verbatim into the LLVM dialect module, so it may use any registered
dialect/op that mlir-opt and mlir-translate understand. irinp operands become block
arguments (%in<N>), irout variables become pointer arguments (%out<N>) whose stores are
appended after the body.
"""
if cl is None or builder is None or pool is None or trans is None: return None
in_vals: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS)
out_vals: sizet* = pool.alloc(8 * LLVMIR_MAX_OPS)
counts: int* = pool.alloc(8 * 2)
if in_vals is None or out_vals is None or counts is None: return None
string.memset(in_vals, 0, 8 * LLVMIR_MAX_OPS)
string.memset(out_vals, 0, 8 * LLVMIR_MAX_OPS)
counts[0] = 0
counts[1] = 0
body: str = self._raw_ir_template(pool, builder, mod, cl, trans, in_vals, out_vals, counts)
if body is None: return None
in_count: int = counts[0]
out_count: int = counts[1]
name: str = self._raw_ir_func_name(pool, trans, "__vpc_mlir")
if name is None: return None
fn: mlirlite.Function* = mlirlite.create_function(pool, mod, name, mlirlite.Void(pool))
if fn is None: return None
pn: str = pool.alloc(32)
for k in range(in_count):
v: mlirlite.Value* = mlirlite.Value*(in_vals[k])
if v is None or v.Ty is None: return None
if pn is not None:
viperlib.snprintf(pn, 32, "in%d", k)
mlirlite.add_param(pool, fn, v.Ty, pn)
for k in range(out_count):
a: mlirlite.Value* = mlirlite.Value*(out_vals[k])
if a is None or a.Ty is None: return None
if pn is not None:
viperlib.snprintf(pn, 32, "out%d", k)
mlirlite.add_param(pool, fn, a.Ty, pn)
fn.Attrs = "alwaysinline"
cap: sizet = string.strlen(body) + 128 + sizet(out_count) * 8448
raw: str = pool.alloc(cap)
ty_buf: str = pool.alloc(4096)
ptr_buf: str = pool.alloc(4096)
if raw is None or ty_buf is None or ptr_buf is None: return None
string.strcpy(raw, body)
pos: sizet = string.strlen(raw)
if pos > 0 and raw[pos - 1] != '\n':
viperlib.snprintf(raw + pos, cap - pos, "\n")
pos = string.strlen(raw)
for k in range(out_count):
a2: mlirlite.Value* = mlirlite.Value*(out_vals[k])
if a2 is None or a2.Ty is None or a2.Ty.Pointee == 0: return None
ty_buf[0] = '\0'
ptr_buf[0] = '\0'
mlirlite.TypePrint(ty_buf, 4096, a2.Ty.Pointee, pool)
mlirlite.TypePrint(ptr_buf, 4096, a2.Ty, pool)
viperlib.snprintf(raw + pos, cap - pos, " llvm.store %%vpc_out%d, %%out%d : %s, %s\n", k, k, ty_buf, ptr_buf)
pos = string.strlen(raw)
viperlib.snprintf(raw + pos, cap - pos, " llvm.return\n")
fn.RawMlirBody = raw
return self._raw_ir_emit_call(pool, builder, name, in_vals, in_count, out_vals, out_count)
# _apply_struct_defaults - apply default values to struct fields, after
# zeroinitializer and before explicit args (which later overwrite defaults).
+6 -2
View File
@@ -255,11 +255,15 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
if hit_box is not None: hit_box[0] = 1
return self.Gen.CallGenCBuiltin.translate_c_asm(pool, builder, mod, cl, trans)
if bare_nm.id is not None and string.strcmp(bare_nm.id, "__llvmir__") == 0:
# c.LLVMIR always returns void (None): set the hit flag
# __llvmir__ always returns void (None): set the hit flag
if hit_box is not None: hit_box[0] = 1
return self.Gen.CallGenCBuiltin.translate_c_llvmir(pool, builder, mod, cl, trans)
if bare_nm.id is not None and string.strcmp(bare_nm.id, "__mlir__") == 0:
# Raw MLIR block: spliced into the LLVM-dialect module as a raw-body function.
if hit_box is not None: hit_box[0] = 1
return self.Gen.CallGenCBuiltin.translate_c_mlir(pool, builder, mod, cl, trans)
if bare_nm.id is not None and (string.strcmp(bare_nm.id, "irinp") == 0 or string.strcmp(bare_nm.id, "irout") == 0):
# irinp/irout standalone calls return 0 (meaningful only inside __llvmir__ f-strings)
# irinp/irout standalone calls return 0 (meaningful only inside __llvmir__/__mlir__ templates)
return mlirlite.ConstInt(pool, mlirlite.Int32(pool), 0, "0")
# Detect coro.* coroutine interception (inline translation, VPC-M1).