Files
Viper_VRTE/mlirlite/__function.vp
T
TermiNexus bca23ffa06 fix(mlirlite): MLIR 间接调用点也过 alias 解析,callee 不再悬空
no-op bitcast(含函数指针 bitcast)被 alias 机制省略后,参数用法会改写成别名目标,
但 llvm.call 的 callee 位置原样写出 → 悬空 %N,mlir-opt 报 use of undeclared SSA value /
definition of SSA value has type 'i8' but previously used here with type '!llvm.ptr'。

- _MlirCall:callee 走 _MlirResolve,并用解析后的名字判断直接/间接调用
  (别名解析可能把 callee 指到 @符号,那就成了直接调用,不能再写 !llvm.ptr)
2026-10-12 13:18:51 +08:00

3935 lines
157 KiB
Plaintext

from stdint import *
import string
import viperlib
import memhub
import stdio
import stdlib
from linkedlist import GSListNode, GSList
from .__types import (
LLVMType, ParamNode, TypePrint,
MlirTypeText, MlirTypeTextIsPointer, MlirFail, MlirSetContextLine,
)
# dump sentinel: IR list traversal step cap (cycle detection, fail-fast, aligned with __module.vp).
# A broken Next cycle would hang traversal; over the cap it prints the list name and exit(1).
define IR_DUMP_CHAIN_LIMIT = 100000
# Peephole master switch (T2 forwarding/ret zeroing, T3 no-op bitcast removal).
# --no-llvmpeephole zeroes it so FunctionPrint skips local peepholes for IR diffing (RenumberSSA still runs).
G_PeepEnable: bool = True
# Function-text truncation flag: _append_cstr sets it to 1 when output hits the buffer cap.
# Checked before FunctionPrint returns and MlirFail'd: truncated MLIR must fail loudly, never silently.
_MlirOutTrunc: int = 0
# ============================================================
# Line: instruction text line
#
# One LLVM IR instruction serializes to one text line, held in a linked list.
# Extends GSListNode[Line] for typed Next: Line|CPtr
#
# NOTE: must carry novirtual, else TransPyC adds a vtable ptr (T*) but drops
# the parent GSListNode[T] Next field, misaligning the struct so field reads return garbage.
# ============================================================
novirtual class Line(GSListNode[Line]):
Buf: str # line text (NUL-terminated)
Len: sizet # text length (excluding NUL)
# ============================================================
# BasicBlock: basic block
#
# Holds name + instruction-line list + terminator flag.
# Extends GSListNode[BasicBlock] for typed Next: BasicBlock|CPtr
# Lines is a GSList[Line] pointer providing O(1) append.
#
# NOTE: must carry novirtual (same reason as Line).
# ============================================================
novirtual class BasicBlock(GSListNode[BasicBlock]):
Name: str # block name (e.g. "entry"/"if.then")
Lines: GSList[Line]* # instruction-line list container (Head/Tail/Count)
IsTerminated: int # 1=terminator present (br/ret)
# ============================================================
# Param: function parameter
#
# Extends GSListNode[Param] for typed Next: Param|CPtr
# Attrs holds parameter attribute text (e.g. "noundef"/"nocapture"/"readonly"), None=none.
#
# NOTE: must carry novirtual (same reason as Line).
# ============================================================
novirtual class Param(GSListNode[Param]):
Ty: LLVMType* # parameter type
Name: str # parameter name (e.g. "%0"/"%a")
Attrs: str # parameter attribute text (None=none)
# ============================================================
# Function: function
#
# Holds name + return type + parameter list + basic-block list.
# Extends GSListNode[Function] for typed Next: Function|CPtr
# Params/Blocks are GSList pointers providing O(1) append.
# Attrs holds function-level attribute text (e.g. "nounwind"/"noinline"), None=none.
#
# NOTE: must carry novirtual (same reason as Line).
# ============================================================
novirtual class Function(GSListNode[Function]):
Name: str # function name
RetTy: LLVMType* # return type
Params: GSList[Param]* # parameter list container
Blocks: GSList[BasicBlock]* # basic-block list container
IsDeclared: int # 1=declaration only (declare), 0=definition (define)
IsVarArg: int # 1=variadic function (...)
Attrs: str # function attribute text (None=none)
AttrGroup: str # string attribute group content (e.g. '"section"=".text.startup"', None=none)
AttrGroupId: int # attribute group id (>0 emits #N on the function line and attributes #N at module end)
NameLen: sizet # function name length (computed once at creation, always == strlen(Name); appended last so existing field offsets stay)
DbgRef: str # DISubprogram ref (kept as-is; ignored by MLIR printing, e.g. "!3")
PeepDone: int # 1=local peephole already applied to this body (runs once; see FunctionPrint)
# ============================================================
# Factory functions
# ============================================================
# Line hardcoded size: Next(8) + Buf(8) + Len(8) = 24 bytes
define LINE_SIZE = 24
def new_line(pool: memhub.MemBuddy*, text: str) -> Line*:
"""Create an instruction line, copying text into a newly allocated buffer"""
ptr: Line* = pool.alloc(LINE_SIZE)
if ptr is None: return None
string.memset(ptr, 0, LINE_SIZE)
if text is not None:
slen: sizet = string.strlen(text)
buf: str = pool.alloc(slen + 1)
if buf is not None:
string.strcpy(buf, text)
ptr.Buf = buf
ptr.Len = slen
return ptr
def _new_gslist(pool: memhub.MemBuddy*, list_size: sizet) -> rawptr:
"""Allocate and zero-init a GSList container from pool (Head/Tail/Count all zero)"""
gsl: rawptr = pool.alloc(list_size)
if gsl is None: return None
string.memset(gsl, 0, list_size)
return gsl
# BasicBlock hardcoded size: Next(8) + Name(8) + Lines(8) + IsTerminated(4) = 32 bytes
define BLOCK_SIZE = 32
def new_basic_block(pool: memhub.MemBuddy*, name: str) -> BasicBlock*:
"""Create a basic block"""
ptr: BasicBlock* = pool.alloc(BLOCK_SIZE)
if ptr is None: return None
string.memset(ptr, 0, BLOCK_SIZE)
ptr.Name = name
ptr.Lines = _new_gslist(pool, 24) # GSList: Head+Tail+Count
ptr.IsTerminated = 0
return ptr
# Param hardcoded size: Next(8) + Ty(8) + Name(8) + Attrs(8) = 32 bytes
define PARAM_SIZE = 32
def new_param(pool: memhub.MemBuddy*, ty: LLVMType*,
name: str) -> Param*:
"""Create a function parameter"""
ptr: Param* = pool.alloc(PARAM_SIZE)
if ptr is None: return None
string.memset(ptr, 0, PARAM_SIZE)
ptr.Ty = ty
ptr.Name = name
ptr.Attrs = None
return ptr
# Function hardcoded size: Next(8) + Name(8) + RetTy(8) + Params(8) + Blocks(8) + IsDeclared(4) + IsVarArg(4) + Attrs(8) + AttrGroup(8) + AttrGroupId(8) + NameLen(8) + DbgRef(8) + PeepDone(8) = 96 bytes (same layout as llvmlite)
define FUNC_SIZE = 96
def new_function(pool: memhub.MemBuddy*, name: str,
ret_ty: LLVMType*) -> Function*:
"""Create a function"""
ptr: Function* = pool.alloc(FUNC_SIZE)
if ptr is None: return None
string.memset(ptr, 0, FUNC_SIZE)
ptr.Name = name
ptr.RetTy = ret_ty
ptr.Params = _new_gslist(pool, 24) # GSList: Head+Tail+Count
ptr.Blocks = _new_gslist(pool, 24)
ptr.IsDeclared = 0
ptr.Attrs = None
ptr.AttrGroup = None
ptr.AttrGroupId = 0
ptr.PeepDone = 0
# NameLen computed once: Name never changes, so NameLen == strlen(Name) (avoids repeated strlen).
# name=None sets 0, matching new_line's null convention (explicit for fixed semantics, not allocator behavior).
if name is not None:
ptr.NameLen = string.strlen(name)
else:
ptr.NameLen = 0
return ptr
# ============================================================
# List operations (delegating to GSList.append)
# ============================================================
def function_add_param(func: Function*, param: Param*):
"""Append a parameter to the function's parameter list"""
if func is None or param is None: return
# NOTE: use a local variable + type parameter (see function_move_block_to_end): chaining
# func.Params.append() mis-resolves to LinkedNode.append and corrupts memory 48 bytes before the container.
params: GSList[Param]* = func.Params
if params is None: return
params.append(param)
def function_add_block(func: Function*, block: BasicBlock*):
"""Append a basic block to the function's block list"""
if func is None or block is None: return
blocks: GSList[BasicBlock]* = func.Blocks
if blocks is None: return
blocks.append(block)
def function_move_block_to_end(func: Function*, block: BasicBlock*):
"""Move block to the end of the function's block list (BB text order == control-flow order).
Fixes non-monotonic SSA numbering (BBs are created in order but instructions are emitted in
control-flow order) that makes llc error; called at position_at_end so BB order matches position order.
NOTE: must use the typed GSList[BasicBlock] parameter, else TransPyC builds an incomplete struct
type and blocks.Head/Tail/Count field reads return garbage.
"""
if func is None or block is None: return
blocks: GSList[BasicBlock]* = func.Blocks
if blocks is None: return
# Already Tail; nothing to move
if blocks.Tail is block:
return
# Remove block from the list
if blocks.Head is block:
# block is Head; advance Head directly
blocks.Head = block.Next
else:
# Walk to find the predecessor
prev: BasicBlock* = blocks.Head
while prev is not None and prev.Next is not block:
prev = prev.Next
if prev is None:
# block not in list; append directly
blocks.append(block)
return
prev.Next = block.Next
# block.Next handled by the removal logic; reset to None
block.Next = None
# Append at the end (no blocks.append call to avoid double-incrementing Count)
if blocks.Tail is not None:
blocks.Tail.Next = block
blocks.Tail = block
def block_append_line(block: BasicBlock*, line: Line*):
"""Append an instruction line to the basic block"""
if block is None or line is None: return
lines: GSList[Line]* = block.Lines
if lines is None: return
lines.append(line)
def block_set_terminated(block: BasicBlock*, val: int):
"""Set the block terminator flag (cross-module call to avoid opaque chained field access)"""
if block is None: return
block.IsTerminated = val
def block_get_terminated(block: BasicBlock*) -> int:
"""Get the block terminator flag (cross-module call to avoid opaque chained field access)"""
if block is None:
return 1
return block.IsTerminated
def block_append_text(pool: memhub.MemBuddy*, block: BasicBlock*,
text: str):
"""Create an instruction line and append it to the basic block"""
if block is None or text is None: return
line: Line* = new_line(pool, text)
if line is not None:
block_append_line(block, line)
def block_insert_text_before_terminator(pool: memhub.MemBuddy*,
block: BasicBlock*,
text: str):
"""Create an instruction line and insert it before the block terminator.
If the block is not terminated, append at the end; if terminated (br/ret), insert before
the terminator so alloca and similar instructions do not land after it.
"""
if block is None or text is None: return
if block.Lines is None: return
line: Line* = new_line(pool, text)
if line is None: return
# Block not terminated: append directly
if block.IsTerminated == 0:
block_append_line(block, line)
return
# Block terminated: insert before Tail (the terminator)
lines: GSList[Line]* = block.Lines
if lines.Head is None:
block_append_line(block, line)
return
# Single node: insert before Head
if lines.Head is lines.Tail:
line.Next = lines.Head
lines.Head = line
lines.Count += 1
return
# Find the predecessor of Tail
prev: Line* = lines.Head
while prev is not None and prev.Next is not lines.Tail:
prev = prev.Next
if prev is None:
# No predecessor found; fall back to append
block_append_line(block, line)
return
# Insert line between prev and Tail
line.Next = lines.Tail
prev.Next = line
lines.Count += 1
# ============================================================
# Attribute setters
# ============================================================
def function_set_attrs(func: Function*, attrs: str):
"""Set function-level attribute text (e.g. "nounwind"/"noinline")"""
if func is None: return
func.Attrs = attrs
_g_attr_group_counter: int = 0
def function_set_attr_group(func: Function*, group: str):
"""Set function string attribute group (e.g. '"section"=".text.startup"') and assign #N.
LLVM string attributes (with a value, e.g. section) cannot be inlined on the signature
line; they must use a group: the function line emits #N and the group is emitted at the end.
"""
global _g_attr_group_counter
if func is None or group is None:
return
func.AttrGroup = group
_g_attr_group_counter += 1
func.AttrGroupId = _g_attr_group_counter
def param_set_attrs(param: Param*, attrs: str):
"""Set parameter attribute text (e.g. "noundef"/"nocapture"/"readonly")"""
if param is None: return
param.Attrs = attrs
def function_set_dbg(func: Function*, ref: str):
"""Set the function-level debug ref (DISubprogram node ref, e.g. "!3").
Kept as-is: MLIR printing ignores it (FunctionPrint emits no !dbg), so this only
records the model without changing output. ref=None clears it.
"""
if func is None: return
func.DbgRef = ref
# ============================================================
# _ll_name_needs_quote - whether a symbol name needs @"..." quoting in MLIR
#
# WARNING: the rule differs from LLVM (measured on mlir-opt 22.1.8):
# - A bare identifier's first char must be a letter or '_': leading digit, '.', '$' must be quoted.
# => TPV's {sha1}.{name} all start with a hex digit, so all need quotes;
# => the string-constant global @.str starts with '.', so it needs quotes too.
# - Later chars allow only [A-Za-z0-9_$] and '.'.
# - '-' is illegal in a bare MLIR name (LLVM allows it); measured error "expected '('".
# - '['/']' (generic specialization names) are likewise illegal.
# This predicate takes the conservative direction: when unsure, quote (@"..." is legal for any name).
# ============================================================
def _ll_name_needs_quote(name: str) -> int:
"""MLIR bare-name predicate: returns 1 (needs quotes) unless it matches [A-Za-z_][A-Za-z0-9_$.]*"""
if name is None:
return 0
name_len: sizet = string.strlen(name)
if name_len == 0:
return 1
c0: char = name[0]
if not ((c0 >= 'a' and c0 <= 'z') or (c0 >= 'A' and c0 <= 'Z') or c0 == '_'):
return 1
i: sizet = 1
while i < name_len:
ch: char = name[i]
if not ((ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or
(ch >= '0' and ch <= '9') or ch == '_' or ch == '$' or ch == '.'):
return 1
i += 1
return 0
# ============================================================
# _MlirSymRefAppend - emit a symbol reference (unified quoting rule)
#
# name does NOT include a leading '@'; an already-quoted form (first char '"') is emitted as-is.
# Used where a symbol appears as an operand (e.g. llvm.mlir.addressof), sharing the same
# predicate with _MlirFuncSig / call target / global name.
# ============================================================
def _MlirSymRefAppend(out: str, outsz: sizet, name: str):
if name is None or name[0] == '\0':
MlirFail("MLIR render: empty symbol name")
if name[0] == '"':
_append_cstr(out, outsz, "@")
_append_cstr(out, outsz, name)
return
if _ll_name_needs_quote(name) != 0:
_append_cstr(out, outsz, "@\"")
_append_cstr(out, outsz, name)
_append_cstr(out, outsz, "\"")
return
_append_cstr(out, outsz, "@")
_append_cstr(out, outsz, name)
# ============================================================
# RenumberSSA: renumber numeric SSA names within a function
#
# Fixes the non-monotonic SSA names in text output caused by build_alloca_at_entry
# inserting allocas before the entry block terminator (llc error:
# "instruction expected to be numbered '%N' or greater").
#
# Example: entry originally %0-%6 + br; alloca_at_entry inserts %28/%41 before br,
# so text order is entry(%0..%6, %28, %41) -> if.then(%7..%27),
# placing %28 before %7, which llc rejects.
#
# Algorithm:
# 1. Scan all BB instruction lines, collect numeric SSA names %N in text-output order
# 2. Build an old->new map by first-occurrence order (incrementing from 0)
# 3. Replace %N in every instruction line text with the new number
#
# NOTE:
# - Only pure numeric SSA names (%0, %1, ...) are renumbered
# - Named SSA names (%entry, %.tmp, %self), block labels and global symbols @N are not
# - Only called for define (with a body); declare has no body to process
# - TransPyV params are mostly named (%self/%env/%n), so no numeric param renumbering needed
# ============================================================
# Whether ch is a decimal digit
def _is_digit_ch(ch: char) -> int:
"""Return 1 if ch is '0'-'9'.
ch is a value type (char = i8); TPC has fixed is-None handling for value types
(always false, generating an i1 0), so no null deref occurs.
"""
if ch is None:
return 0
if ch >= '0' and ch <= '9':
return 1
return 0
# ============================================================
# Safe i32 array access (explicit byte offset)
#
# TransPyC's ptr[i] pointer indexing may be treated as byte indexing (i8*) instead of i32*
# indexing, corrupting memory. Must compute the byte offset explicitly: addr = base + idx * 4,
# then cast to i32*. See HandlesTranslator.py:358's uint64(ptr) + offset * sizeof usage.
# ============================================================
def _map_get_i32(map_arr: str, idx: int) -> int:
"""Read i32 array map_arr[idx] (explicit byte offset idx*4).
Casts the byte offset to i32* via int*(uint64), then dereferences with defef.
TPC has fixed type-union casts (_HandleTypeUnionCastLlvm fallback), so this form works.
"""
if map_arr is None:
return -1
byte_off: uint64 = uint64(map_arr) + idx * 4
elem_ptr: int* = int*(byte_off)
return *elem_ptr
def _map_set_i32(map_arr: str, idx: int, val: int):
"""Write map_arr[idx] = val (i32 array, explicit byte offset idx*4).
Casts via int*(uint64) and assigns through defefas.
TPC has fixed type-union casts, so this form works.
"""
if map_arr is None:
return
byte_off: uint64 = uint64(map_arr) + idx * 4
elem_ptr: int* = int*(byte_off)
*elem_ptr = val
def _scan_max_ssa_in_text(text: str, cur_max: int) -> int:
"""Scan text for the largest numeric %N and return max(cur_max, found).
Only matches '%' immediately followed by a digit; skips named SSA names (%entry etc.).
"""
result: int = cur_max
p: str = text
while p is not None and p[0] != '\0':
if p[0] == '%':
nxt: str = p + 1
if nxt is not None and _is_digit_ch(nxt[0]) != 0:
num: int = string.atoi(nxt)
if num > result:
result = num
p = p + 1
return result
def _find_max_ssa_number(func: Function*) -> int:
"""Walk all BB instruction lines for the largest %N; returns -1 if no numeric SSA names"""
max_n: int = -1
if func is None or func.Blocks is None:
return max_n
blocks: GSList[BasicBlock]* = func.Blocks
blk: BasicBlock* = blocks.Head
blk_steps: sizet = 0
while blk is not None:
blk_steps += 1
if blk_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa scan aborted)\n", func.Name)
stdlib.exit(1)
if blk.Lines is not None:
lines: GSList[Line]* = blk.Lines
line: Line* = lines.Head
line_steps: sizet = 0
while line is not None:
line_steps += 1
if line_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa scan aborted)\n",
func.Name, blk.Name)
stdlib.exit(1)
if line.Buf is not None:
max_n = _scan_max_ssa_in_text(line.Buf, max_n)
line = line.Next
blk = blk.Next
return max_n
def _assign_new_numbers(text: str,
map_arr: str,
max_n: int, next_new: int) -> int:
"""Scan text; for each numeric %N, assign next_new if map_arr[N]==-1.
Returns the updated next_new. Mapping is built in first-occurrence order.
"""
result: int = next_new
p: str = text
while p is not None and p[0] != '\0':
if p[0] == '%':
nxt: str = p + 1
if nxt is not None and _is_digit_ch(nxt[0]) != 0:
num: int = string.atoi(nxt)
if num >= 0 and num <= max_n:
if _map_get_i32(map_arr, num) == -1:
_map_set_i32(map_arr, num, result)
result += 1
p = p + 1
return result
def _replace_ssa_numbers(text: str,
pool: memhub.MemBuddy*,
map_arr: str,
max_n: int) -> str:
"""Build new text with each %N replaced by its mapped new number.
Char-by-char scan: a numeric %N is looked up and substituted, others copied as-is.
Returns a newly allocated buffer (caller need not free the old Buf; pool manages it).
"""
src_len: sizet = string.strlen(text)
# New buffer: worst case each number grows in digits; allocate src_len*2 + 64
new_size: sizet = src_len * 2 + 64
new_buf: str = pool.alloc(new_size)
if new_buf is None:
return None
# Allocate the digit buffer once (reused in the loop)
num_buf: str = pool.alloc(16)
if num_buf is None:
new_buf[0] = '\0'
return new_buf
dst_idx: sizet = 0
src_p: str = text
while src_p is not None and src_p[0] != '\0':
if dst_idx + 16 >= new_size:
# Buffer too small; truncate
break
if src_p[0] == '%':
nxt: str = src_p + 1
if nxt is not None and _is_digit_ch(nxt[0]) != 0:
num: int = string.atoi(nxt)
# Range-check before reading map_arr: when num > max_n the array holds only
# (max_n+1)*4 bytes, so a read-before-check would read out of bounds.
if num >= 0 and num <= max_n:
new_num: int = _map_get_i32(map_arr, num)
if new_num != -1:
# Write '%'
new_buf[dst_idx] = '%'
dst_idx += 1
# Write the new number
num_buf[0] = '\0'
viperlib.snprintf(num_buf, 16, "%d", new_num)
k: sizet = 0
while num_buf[k] != '\0' and dst_idx + 1 < new_size:
new_buf[dst_idx] = num_buf[k]
dst_idx += 1
k += 1
# Skip the old digit chars
src_p = src_p + 1
while src_p is not None and _is_digit_ch(src_p[0]) != 0:
src_p = src_p + 1
continue
# Ordinary char: copy directly
new_buf[dst_idx] = src_p[0]
dst_idx += 1
src_p = src_p + 1
new_buf[dst_idx] = '\0'
return new_buf
def RenumberSSA(func: Function*, pool: memhub.MemBuddy*):
"""Renumber all numeric SSA names in the function so text-output order is monotonic.
Called before FunctionPrint serializes the body; only applies to define (IsDeclared==0)
functions with non-empty Blocks.
"""
if func is None or pool is None:
return
if func.IsDeclared == 1:
return # declare has no body
if func.Blocks is None:
return
# 1. First pass: find the largest SSA number
max_n: int = _find_max_ssa_number(func)
if max_n < 0:
return # no numeric SSA names
# 2. Allocate and init map_arr[0..max_n] = -1; stored as i8* (pool.alloc), accessed via
# _map_get_i32/_map_set_i32; memset to 0xFF (-1 per 4-byte i32) to avoid ptr[i] indexing issues.
map_bytes: sizet = (max_n + 1) * 4 # int = 4 bytes
map_arr: str = pool.alloc(map_bytes)
if map_arr is None:
return
string.memset(map_arr, 0xFF, map_bytes) # 0xFF * 4 = 0xFFFFFFFF = -1
# 3. Second pass: assign new numbers in text order
next_new: int = 0
blocks: GSList[BasicBlock]* = func.Blocks
blk: BasicBlock* = blocks.Head
blk_steps: sizet = 0
while blk is not None:
blk_steps += 1
if blk_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa renumber aborted)\n", func.Name)
stdlib.exit(1)
if blk.Lines is not None:
lines: GSList[Line]* = blk.Lines
line: Line* = lines.Head
line_steps: sizet = 0
while line is not None:
line_steps += 1
if line_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa renumber aborted)\n",
func.Name, blk.Name)
stdlib.exit(1)
if line.Buf is not None:
next_new = _assign_new_numbers(line.Buf, map_arr, max_n, next_new)
line = line.Next
blk = blk.Next
# 4. Check whether any renumbering is needed (old != new)
need_renumber: int = 0
i: int = 0
while i <= max_n:
v: int = _map_get_i32(map_arr, int(i))
if v != -1 and v != i:
need_renumber = 1
break
i += 1
if need_renumber == 0:
return # already monotonic; no replacement needed
# 5. Third pass: replace every instruction line text
blk = blocks.Head
blk_steps = 0
while blk is not None:
blk_steps += 1
if blk_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa replace aborted)\n", func.Name)
stdlib.exit(1)
if blk.Lines is not None:
lines: GSList[Line]* = blk.Lines
line: Line* = lines.Head
line_steps = 0
while line is not None:
line_steps += 1
if line_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa replace aborted)\n",
func.Name, blk.Name)
stdlib.exit(1)
if line.Buf is not None:
new_buf: str = _replace_ssa_numbers(
line.Buf, pool, map_arr, max_n)
if new_buf is not None:
line.Buf = new_buf
line.Len = string.strlen(new_buf)
line = line.Next
blk = blk.Next
# ============================================================
# Local peephole optimization (text layer, before output, semantics-preserving)
# T1: ret <iN> zeroinitializer -> ret <iN> 0; ret <ptr*> zeroinitializer -> null
# T2: when an entry alloca slot has exactly one store with a side-effect-free value
# (param/%SSA/int/float literal), forward store->load to that value and drop
# alloca/store/load ("virtual register optimization").
# Safety: only when the alloca and its single store are both in entry, loads anywhere,
# and no other address-taking/aliasing use.
# ============================================================
def _peep_is_startwith(s: str, pfx: str) -> int:
if s is None or pfx is None:
return 0
pl: sizet = string.strlen(pfx)
if pl == 0:
return 1
if string.strlen(s) < pl:
return 0
return 1 if string.strncmp(s, pfx, pl) == 0 else 0
def _peep_replace_token(pool: memhub.MemBuddy*,
line_buf: str, tok: str, repl: str,
tlen: sizet, rlen: sizet) -> str:
"""Replace the standalone token %<tok> in line_buf with repl; returns a new buffer (original if unchanged).
tlen/rlen = strlen(tok)/strlen(repl): computed by the caller outside the line loop
(the same ltok/safe_val applies to all lines here) to avoid recomputing lengths per line.
"""
if line_buf is None or tok is None or repl is None:
return line_buf
if tlen == 0:
return line_buf
# Pre-scan for a hit: no strlen, single NUL-bounded pass (most calls return with no hit,
# avoiding per-line strlen). Per-input equivalent: tok has no NUL, so strncmp equality
# implies the token fits; dropping the old i+tlen<=ln bound does not change hit detection.
hit: int = 0
i: sizet = 0
while line_buf[i] != '\0':
if line_buf[i] == '%' and line_buf[i + 1] == tok[0] \
and string.strncmp(line_buf + i + 1, tok, tlen) == 0:
nxt: int = 0
if line_buf[i + 1 + tlen] != '\0':
cc: int = int(line_buf[i + 1 + tlen])
if (cc >= 48 and cc <= 57) or (cc >= 65 and cc <= 90) or (cc >= 97 and cc <= 122) or cc == 95:
nxt = 1
if nxt == 0:
hit = 1
break
i += 1
if hit == 0:
return line_buf
ln: sizet = string.strlen(line_buf)
# Buffer size bound: at most floor(ln/2) replacements per line, each removing >=2 bytes
# (%+name) and inserting rlen; worst output <= ln + (ln/2)*rlen + NUL. ln + 1 + rlen*(ln+1)
# always covers it, preventing overflow (random memory corruption) when rlen is larger.
out: str = pool.alloc(ln + 1 + rlen * (ln + 1))
if out is None:
return line_buf
op: sizet = 0
oq: sizet = 0
i = 0
while i < ln:
if line_buf[i] == '%' and i + 1 + tlen <= ln \
and line_buf[i + 1] == tok[0] \
and string.strncmp(line_buf + i + 1, tok, tlen) == 0:
nxt2: int = 0
if i + 1 + tlen < ln:
cc2: int = int(line_buf[i + 1 + tlen])
if (cc2 >= 48 and cc2 <= 57) or (cc2 >= 65 and cc2 <= 90) or (cc2 >= 97 and cc2 <= 122) or cc2 == 95:
nxt2 = 1
if nxt2 == 0:
for j in range(int(rlen)):
out[oq] = repl[j]
oq += 1
i += 1 + tlen
continue
out[oq] = line_buf[i]
oq += 1
i += 1
out[oq] = '\0'
return out
def _peep_last_ref_eq(sb: str, a_name: str, nl: sizet) -> int:
"""Whether the line's last '%name' token equals a_name (store/load pointer operand is at line end).
nl = strlen(a_name): computed by the caller outside the alloca-slot scan (a_name is
constant within one slot scan) to avoid repeated strlen per line.
"""
if sb is None or a_name is None:
return 0
# Single pass to locate the last '%' (avoids repeated strchr calls, one per '%').
r: str = None
probe: str = sb
while probe[0] != '\0':
if probe[0] == '%':
r = probe
probe += 1
if r is None:
return 0
# Compare the first char first (cheap reject); only strncmp when it matches
if r[1] == a_name[0] and string.strncmp(r + 1, a_name, nl) == 0:
nxtc: int = int(r[1 + nl])
if nxtc == 0 or nxtc == 32 or nxtc == 44 or nxtc == 41:
return 1
return 0
def _peep_alloc_name(eb: str, pool: memhub.MemBuddy*) -> str:
"""Extract name (without %) from '%name = alloca ...'; returns None on failure"""
if eb is None or eb[0] != '%':
return None
eqpos: str = string.strstr(eb, " = alloca ")
if eqpos is None:
return None
nlen: sizet = sizet(uint64(eqpos) - uint64(eb)) - 1
if nlen == 0:
return None
nm: str = pool.alloc(nlen + 1)
if nm is None:
return None
string.strncpy(nm, eb + 1, nlen)
nm[nlen] = '\0'
return nm
def _peep_is_alloc_of(eb: str, a_name: str, nl: sizet) -> int:
"""Whether eb is a_name's alloca definition line; equivalent to strcmp(_peep_alloc_name(eb), a_name)==0.
Compares the name first (strncmp, reject on mismatch), then locates " = alloca " only when
bytes match, reducing strstr count from O(slots*lines) to only hit lines per slot.
nl = strlen(a_name); nlen==0 (empty name) returned None before, so it is rejected here too.
Equivalence: _peep_alloc_name takes the bytes before the first " = alloca " and after '%',
so equal-length equal name <=> strncmp match and the substring starts at eb+1+nl.
"""
if eb is None or a_name is None or nl == 0 or eb[0] != '%':
return 0
# First-char prefilter: nl>=1, so strncmp equality implies equal first chars; a mismatch
# cheaply rejects, keeping most lines out of strncmp. Per-input equivalent: this branch
# rejects iff the old strncmp would too. eb[0]=='%' is non-NUL, so eb[1] is in-bounds.
if eb[1] != a_name[0]:
return 0
if string.strncmp(eb + 1, a_name, nl) != 0:
return 0
eqpos: str = string.strstr(eb, " = alloca ")
if eqpos is not None and uint64(eqpos) == uint64(eb) + 1 + nl:
return 1
return 0
def _peephole_opt(func: Function*, pool: memhub.MemBuddy*):
"""Text-layer local peephole: T1 ret zeroinitializer; T2 entry-slot store->load forwarding"""
if func is None or func.Blocks is None or pool is None:
return
# NOTE: TPC silently drops assignments for a two-level "var = ptr.field.member" auto-deref
# chain (the IR reads an uninitialized stack var -> random stack errors). Split into one level:
# fetch the field into a local rawptr, then null-check before .Head (same pattern as FunctionPrint).
func_blocks: GSList[BasicBlock]* = func.Blocks
blk: BasicBlock* = None
if func_blocks is not None:
blk = func_blocks.Head
# ---------- T1: ret <ty> zeroinitializer ----------
blk_t1 = blk
while blk_t1 is not None:
line1: Line* = None
if blk_t1.Lines is not None:
t1_lines: GSList[Line]* = blk_t1.Lines
line1 = t1_lines.Head
while line1 is not None:
if line1.Buf is not None:
zi: str = string.strstr(line1.Buf, " zeroinitializer")
if zi is not None and _peep_is_startwith(line1.Buf, "ret ") != 0:
ty_len: sizet = sizet(uint64(zi) - uint64(line1.Buf)) - 4
if ty_len > 0:
ty_buf: str = pool.alloc(ty_len + 1)
if ty_buf is not None:
string.strncpy(ty_buf, line1.Buf + 4, ty_len)
ty_buf[ty_len] = '\0'
lit: str = None
if ty_buf[0] == 'i':
lit = "0"
elif string.strchr(ty_buf, 42) is not None: # '*'
lit = "null"
if lit is not None:
nb: str = pool.alloc(ty_len + 16)
if nb is not None:
nb[0] = '\0'
string.strcat(nb, "ret ")
string.strcat(nb, ty_buf)
string.strcat(nb, " ")
string.strcat(nb, lit)
line1.Buf = nb
line1 = line1.Next
blk_t1 = blk_t1.Next
# ---------- T2: entry alloca slot forwarding ----------
entry_blk: BasicBlock* = blk
if entry_blk is None:
return
entry_lines: GSList[Line]* = entry_blk.Lines
if entry_lines is None:
return
e_line: Line* = entry_lines.Head
while e_line is not None:
eb: str = e_line.Buf
if eb is not None:
a_name: str = _peep_alloc_name(eb, pool)
if a_name is not None:
# a_name is constant across the slot's full line scan; strlen computed once (not per line)
a_name_len: sizet = string.strlen(a_name)
st_val: str = None
st_ok: int = 0
st_count: int = 0
st_line_order: int = -1
load_ltoks: str = pool.alloc(8 * 64)
load_cnt: int = 0
load_entry_order: int64 = 0
alloca_order: int = -1
order: int = 0
unsafe: int = 0
# First pass: count this alloca's store/load/other refs and line order
blk_s = blk
while blk_s is not None:
l_s: Line* = None
if blk_s.Lines is not None:
scan_lines: GSList[Line]* = blk_s.Lines
l_s = scan_lines.Head
while l_s is not None:
sb: str = l_s.Buf
if sb is not None:
is_alloc_def: int = 0
if sb[0] == '%':
if _peep_is_alloc_of(sb, a_name, a_name_len) != 0:
is_alloc_def = 1
if is_alloc_def == 0 and _peep_last_ref_eq(sb, a_name, a_name_len) != 0:
if _peep_is_startwith(sb, "store ") != 0:
st_count += 1
if st_count == 1:
vcomma: str = string.strstr(sb + 6, ", ")
if vcomma is not None:
vlen: sizet = sizet(uint64(vcomma) - uint64(sb + 6))
if vlen > 0:
vbuf: str = pool.alloc(vlen + 1)
if vbuf is not None:
string.strncpy(vbuf, sb + 6, vlen)
vbuf[vlen] = '\0'
st_val = vbuf
st_ok = 1
else:
unsafe = 1
if blk_s == entry_blk:
st_line_order = order
elif string.strstr(sb, " = load ") is not None:
ls: str = string.strstr(sb, "%")
if ls is not None and load_cnt < 64:
lname_len: sizet = sizet(
uint64(string.strstr(sb, " = load ")) - uint64(ls))
lname: str = pool.alloc(lname_len + 1)
if lname is not None:
string.strncpy(lname, ls + 1, lname_len - 1)
lname[lname_len - 1] = '\0'
slotp: str* = (str*)(uint64(load_ltoks) + load_cnt * 8)
slotp[0] = lname
load_cnt += 1
if blk_s == entry_blk and st_line_order < 0:
load_entry_order += 1
else:
# lname alloc failed: this load won't enter load_ltoks, but the
# delete phase still removes its def line by slot name, leaving a
# dangling use. Conservatively abandon T2 forwarding for this slot.
unsafe = 1
else:
# Loads exceed record capacity (load_ltoks=8*64, cap 64) or the line has
# no '%' to parse a def name: unrecorded loads would still have their
# " = load " def lines deleted by slot name, leaving dangling uses
# (llc: "use of undefined value", e.g. strcmp short-circuit >64 branch).
# Only a fully recorded load set allows deleting all defs, so
# conservatively abandon the whole T2 forwarding for this slot (IR stays valid).
unsafe = 1
else:
unsafe = 1
if sb is not None:
order += 1
l_s = l_s.Next
blk_s = blk_s.Next
# Criteria
safe_val: str = None
if unsafe == 0 and st_count == 1 and st_ok != 0 and st_line_order >= 0 \
and load_cnt > 0 and load_entry_order == 0 and st_val is not None:
pct: str = string.strchr(st_val, '%')
if pct is None:
sp1: str = string.strchr(st_val, ' ')
if sp1 is not None and sp1[1] != 0:
safe_val = sp1 + 1
else:
safe_val = st_val
else:
if string.strchr(pct, ' ') is None and string.strchr(pct, ',') is None:
safe_val = pct
if safe_val is not None:
# Second pass: replace load-name references
# safe_val is constant during this slot's replacement; strlen computed once per slot
safe_len: sizet = string.strlen(safe_val)
for li2 in range(int(load_cnt)):
slotsp: str* = (str*)(uint64(load_ltoks) + li2 * 8)
ltok: str = slotsp[0]
if ltok is not None:
# ltok is constant across the load name's function-wide scan; strlen computed once
ltok_len: sizet = string.strlen(ltok)
blk_r = blk
while blk_r is not None:
lr: Line* = None
if blk_r.Lines is not None:
repl_lines: GSList[Line]* = blk_r.Lines
lr = repl_lines.Head
while lr is not None:
if lr.Buf is not None:
nb2: str = _peep_replace_token(pool, lr.Buf, ltok, safe_val,
ltok_len, safe_len)
if nb2 is not None and nb2 is not lr.Buf:
lr.Buf = nb2
lr = lr.Next
blk_r = blk_r.Next
# Delete: alloca def / the single store / all load lines
blk_d = blk
while blk_d is not None:
ld: Line* = None
if blk_d.Lines is not None:
kill_lines: GSList[Line]* = blk_d.Lines
ld = kill_lines.Head
while ld is not None:
db: str = ld.Buf
if db is not None:
kill: int = 0
if db[0] == '%' and db[1] == a_name[0]:
if _peep_is_alloc_of(db, a_name, a_name_len) != 0:
kill = 1
if kill == 0 and _peep_last_ref_eq(db, a_name, a_name_len) != 0:
if _peep_is_startwith(db, "store ") != 0 \
or string.strstr(db, " = load ") is not None:
kill = 1
if kill != 0:
ld.Buf = None
ld = ld.Next
blk_d = blk_d.Next
e_line = e_line.Next
# ---------- T3: no-op bitcast removal ----------
_peep_noop_bitcast(func, pool)
def _peep_noop_bitcast(func: Function*, pool: memhub.MemBuddy*):
"""T3: remove no-op bitcast (bitcast <T> %x to <T>, same type on both sides).
The emitter (virtual calls / union width casts) produces same-type identity bitcasts
(e.g. `%6 = bitcast i8* %ok_msg to i8*`), purely redundant. Replace all uses of %x with
the source operand (%SSA/@global/constant token), then delete the line.
Conservative limit: both type and operand must be simple whitespace-free tokens.
"""
if func is None or pool is None or func.Blocks is None:
return
# Single forward pass: on a valid candidate, replace refs function-wide + delete the line,
# resuming from the next line; stop at the first non-matching bitcast. Equivalent deletion
# result to the old rescan-from-block-head approach (replacement only touches operand tokens),
# but removes the O(K*C) quadratic rescan.
blk: BasicBlock* = func.Blocks.Head
while blk is not None:
ln: Line* = None
if blk.Lines is not None:
ln = blk.Lines.Head
while ln is not None:
nxt: Line* = ln.Next
if ln.Buf is not None:
eqpos: str = string.strstr(ln.Buf, " = bitcast ")
if eqpos is not None:
nlen: sizet = sizet(uint64(eqpos) - uint64(ln.Buf)) - 1
nm_buf: str = None
src_buf: str = None
if nlen > 0:
after: str = eqpos + 11 # skip " = bitcast "
sp1: str = string.strchr(after, ' ')
tpos: str = string.strstr(after, " to ")
if sp1 is not None and tpos is not None \
and uint64(sp1) < uint64(tpos):
tyA_len: sizet = sizet(uint64(sp1) - uint64(after))
src_st: str = sp1 + 1
src_len: sizet = sizet(uint64(tpos) - uint64(src_st))
tyB: str = tpos + 4
tyB_len: sizet = string.strlen(tyB)
if src_len > 0 and string.strchr(tyB, ' ') is None \
and uint64(string.strchr(src_st, ' ')) == uint64(tpos) \
and tyA_len == tyB_len \
and string.strncmp(after, tyB, tyA_len) == 0:
nm_buf = pool.alloc(nlen + 1)
if nm_buf is not None:
string.strncpy(nm_buf, ln.Buf + 1, nlen)
nm_buf[nlen] = '\0'
src_buf = pool.alloc(src_len + 1)
if src_buf is not None:
string.strncpy(src_buf, src_st, src_len)
src_buf[src_len] = '\0'
if nm_buf is None or src_buf is None:
# First non-matching bitcast: stop (same semantics as before)
return
# Replace %nm references with the source operand function-wide
# nm_buf/src_buf are constant across the scan; strlen computed once (not per line)
nm_buf_len: sizet = string.strlen(nm_buf)
src_buf_len: sizet = string.strlen(src_buf)
blk_r: BasicBlock* = func.Blocks.Head
while blk_r is not None:
if blk_r.Lines is not None:
ln2: Line* = blk_r.Lines.Head
while ln2 is not None:
if ln2.Buf is not None:
rb: str = _peep_replace_token(pool, ln2.Buf, nm_buf, src_buf,
nm_buf_len, src_buf_len)
if rb is not None and rb is not ln2.Buf:
ln2.Buf = rb
ln2.Len = string.strlen(rb)
ln2 = ln2.Next
blk_r = blk_r.Next
ln.Buf = None
ln = nxt
blk = blk.Next
# ============================================================
# MLIR (llvm dialect) render stage
#
# Located inside FunctionPrint. Builders stay unchanged and keep writing LLVM-syntax
# instruction text into Line; this stage re-parses that text and handles the three
# MLIR-specific differences:
# 1) constant materialization -- MLIR operands cannot be literals: non-SSA operands
# (int/float/null/zeroinitializer/@symbol) become llvm.mlir.constant / llvm.mlir.zero /
# llvm.mlir.addressof ops at the entry block head, assigned %cN (once per (type,text) per function)
# 2) phi -> block parameters -- leading phis become block params ^blk(%0: i32); phi lines are
# not emitted; br/cond_br into that block get arguments matched by source block
# 3) opaque pointers -- all pointers unify to !llvm.ptr (handled by TypePrint/MlirTypeText)
#
# Two passes: pass1 only registers (const table / bitcast no-op alias table) + shape checks;
# pass2 writes text (const table is complete, so entry-head const ops can be emitted first).
# ============================================================
# Table capacity
define MLIR_CONST_CAP = 512
define MLIR_ALIAS_CAP = 512
# Render pass: 1=scan(register) 2=emit(write text)
_MLIR_Pass: int = 0
_MLIR_Pool: memhub.MemBuddy* = None
_MLIR_Func: Function* = None
_MLIR_SrcBlk: BasicBlock* = None
# Constant table: (MLIR type text, constant text) -> materialized SSA name
_MLIR_ConstTy: rawptr = None
_MLIR_ConstVal: rawptr = None
_MLIR_ConstName: rawptr = None
_MLIR_ConstCount: int = 0
# Next display number for auto constant SSA names (%c<seq>): decoupled from the table index
# _MLIR_ConstCount so the number can skip user SSA names literally named c0/c1/... (see
# _MlirConstGet / _MlirCollectUsedCNames), while identical constants still share one name.
_MLIR_ConstNameSeq: int = 0
# Used-name table (i32[MLIR_CONST_CAP]): slot d = 1 iff the current function already uses an SSA
# name c<d> (param or local), so the constant namer must skip d.
_MLIR_UsedC: rawptr = None
# bitcast no-op alias table + cmpxchg result binding table: result name -> source operand / extractvalue result name
# (cmpxchg's extractvalue result name is deterministically derived from the result name, not a counter; see _MlirCxResName)
_MLIR_AliasFrom: rawptr = None
_MLIR_AliasTo: rawptr = None
_MLIR_AliasCount: int = 0
# The intermediate instruction text line currently being rendered (set at _MlirRenderLine entry).
# Supplies line context to type-text conversion failures: MlirFail terminates immediately with no
# stack, so the line text is stashed in __types.vp (via MlirSetContextLine) and printed with the error.
_MLIR_CurLine: str = None
# Parse-time scratch buffers. Allocated once per print window and reused across every FunctionPrint
# in it (the tables/scratch are ~150 KiB with buddy rounding, so allocating them per function filled
# the 1 GB main pool for function-heavy modules; see _MlirTablesInit / MlirTablesReset). Text that
# must outlive a line is dup'd on table/alias insertion, see _mlir_dup).
_SC_Op: str = None
_SC_Tb: str = None
_SC_Ty: str = None
_SC_Ty2: str = None
_SC_V1: str = None
_SC_V2: str = None
_SC_V3: str = None
_SC_Callee: str = None
_SC_Args: str = None
_SC_ArgTs: str = None
_SC_IdxV: str = None
_SC_IdxT: str = None
_SC_Mt: str = None
_SC_Tmp: str = None
_SC_Tmp2: str = None
_SC_P1: str = None
_SC_P2: str = None
_SC_B1: str = None
_SC_B2: str = None
_SC_Buf: str = None
# Reuse guard: 1 once the tables/scratch above are live for _MlirTablesPool. _MlirTablesInit
# allocates only when the guard is clear; every FunctionPrint in the same print window then reuses
# the buffers (logical state is reset per function). MlirTablesReset clears the guard at PrintModule
# entry so a window whose pool was reset_to'd reallocates from the current pool (a cached pointer
# into a reclaimed window would dangle).
_MlirTablesValid: int = 0
_MlirTablesPool: memhub.MemBuddy* = None
# Live capacities of the constant/alias tables: start at the define defaults and grow on demand
# (_MlirAliasGrow / _MlirConstGrow), instead of hard-failing the whole compile when a big function
# needs more entries than the fixed caps allow. _MLIR_ConstCap also bounds the auto constant name
# space (%c<seq>) and the used-name table, which used to be nailed to MLIR_CONST_CAP.
_MLIR_AliasCap: int = MLIR_ALIAS_CAP
_MLIR_ConstCap: int = MLIR_CONST_CAP
def MlirTablesReset():
"""Invalidate the parse-time tables/scratch so the next FunctionPrint reallocates from the pool.
Mirrors the global-region scratch pattern (MlirGvResetScratch): a print window may run after the
pool was reset_to'd, so cached pointers must not be reused across windows. Called at PrintModule
entry; FunctionPrint called standalone keeps its buffers (its caller owns the pool lifetime).
"""
global _MlirTablesValid, _SC_Op
_MlirTablesValid = 0
_SC_Op = None
def _mlir_slotp(buf: rawptr, idx: int) -> str*:
"""Table slot address (buf + idx*8)"""
return str*(uint64(buf) + idx * 8)
def _mlir_dup(s: str) -> str:
"""Copy text into the pool (for table entries that must outlive a line)"""
if s is None:
return None
n: sizet = string.strlen(s)
b: str = _MLIR_Pool.alloc(n + 1)
if b is None:
MlirFail("MLIR render: text dup allocation failed")
string.strncpy(b, s, n)
b[n] = '\0'
return b
def _MlirTablesInit(pool: memhub.MemBuddy*):
"""Initialize (or reuse) the constant/alias tables and the parse scratch buffers.
The buffers live for the whole print window: the first FunctionPrint allocates them and every
later one reuses them, resetting only the logical state (counts + used-name table). Re-allocating
the ~150 KiB set per function (once per function, per dump mode) exhausted the 1 GB main pool on
function-heavy modules; the equivalent global-region scratch (MlirGvEnsureScratch) already reuses
this way. Callers that may reset the pool between prints must call MlirTablesReset first.
"""
global _MLIR_Pool, _MLIR_Pass, _MLIR_ConstTy, _MLIR_ConstVal, _MLIR_ConstName, _MLIR_ConstCount, _MLIR_ConstNameSeq, _MLIR_UsedC, _MLIR_ConstCap, _MLIR_AliasFrom, _MLIR_AliasTo, _MLIR_AliasCount, _MLIR_AliasCap, _SC_Op, _SC_Tb, _SC_Ty, _SC_Ty2, _SC_V1, _SC_V2, _SC_V3, _SC_Callee, _SC_Args, _SC_ArgTs, _SC_IdxV, _SC_IdxT, _SC_Mt, _SC_Tmp, _SC_Tmp2, _SC_P1, _SC_P2, _SC_B1, _SC_B2, _SC_Buf, _MlirTablesValid, _MlirTablesPool
if pool is None:
MlirFail("MLIR render: memory pool is null")
if _MlirTablesValid != 0 and _MlirTablesPool == pool:
# Reuse the live buffers: reset the per-function logical state. Entries past the reset counts
# are unreachable (all lookups scan [0, count)), so no table body needs clearing; only the
# used-name bitmap must be zeroed because _MlirCollectUsedCNames only ever sets bits.
_MLIR_Pool = pool
_MLIR_Pass = 0
_MLIR_ConstCount = 0
_MLIR_ConstNameSeq = 0
_MLIR_AliasCount = 0
string.memset(_MLIR_UsedC, 0, _MLIR_ConstCap * 4)
return
_MLIR_Pool = pool
_MLIR_Pass = 0
_MLIR_ConstCount = 0
_MLIR_ConstNameSeq = 0
_MLIR_AliasCount = 0
_MLIR_ConstCap = MLIR_CONST_CAP
_MLIR_ConstTy = pool.alloc(_MLIR_ConstCap * 8)
_MLIR_ConstVal = pool.alloc(_MLIR_ConstCap * 8)
_MLIR_ConstName = pool.alloc(_MLIR_ConstCap * 8)
_MLIR_UsedC = pool.alloc(_MLIR_ConstCap * 4)
_MLIR_AliasCap = MLIR_ALIAS_CAP
_MLIR_AliasFrom = pool.alloc(_MLIR_AliasCap * 8)
_MLIR_AliasTo = pool.alloc(_MLIR_AliasCap * 8)
if _MLIR_ConstTy is None or _MLIR_ConstVal is None or _MLIR_ConstName is None \
or _MLIR_UsedC is None or _MLIR_AliasFrom is None or _MLIR_AliasTo is None:
MlirFail("MLIR render: constant/alias table allocation failed")
string.memset(_MLIR_ConstTy, 0, _MLIR_ConstCap * 8)
string.memset(_MLIR_ConstVal, 0, _MLIR_ConstCap * 8)
string.memset(_MLIR_ConstName, 0, _MLIR_ConstCap * 8)
string.memset(_MLIR_UsedC, 0, _MLIR_ConstCap * 4)
string.memset(_MLIR_AliasFrom, 0, _MLIR_AliasCap * 8)
string.memset(_MLIR_AliasTo, 0, _MLIR_AliasCap * 8)
_SC_Op = pool.alloc(64)
_SC_Tb = pool.alloc(512)
_SC_Ty = pool.alloc(4096)
_SC_Ty2 = pool.alloc(4096)
_SC_V1 = pool.alloc(512)
_SC_V2 = pool.alloc(512)
_SC_V3 = pool.alloc(512)
_SC_Callee = pool.alloc(512)
_SC_Args = pool.alloc(2048)
_SC_ArgTs = pool.alloc(8192)
_SC_IdxV = pool.alloc(1024)
_SC_IdxT = pool.alloc(4096)
_SC_Mt = pool.alloc(4096)
_SC_Tmp = pool.alloc(8192)
_SC_Tmp2 = pool.alloc(4096)
_SC_P1 = pool.alloc(512)
_SC_P2 = pool.alloc(512)
_SC_B1 = pool.alloc(4096)
_SC_B2 = pool.alloc(4096)
_SC_Buf = pool.alloc(4096)
if _SC_Op is None or _SC_Ty is None or _SC_Mt is None or _SC_Tmp is None:
MlirFail("MLIR render: parse scratch allocation failed")
_SC_Op[0] = '\0'
_SC_Ty[0] = '\0'
_SC_Mt[0] = '\0'
_SC_Tmp[0] = '\0'
_MlirTablesPool = pool
_MlirTablesValid = 1
# ============================================================
# Constant table / alias table
# ============================================================
def _MlirAliasLookup(name: str) -> str:
"""Look up the alias table; returns None on miss"""
if name is None:
return None
for i in range(int(_MLIR_AliasCount)):
sp: str* = _mlir_slotp(_MLIR_AliasFrom, int(i))
if sp[0] is not None and string.strcmp(sp[0], name) == 0:
st: str* = _mlir_slotp(_MLIR_AliasTo, int(i))
return st[0]
return None
def _MlirAliasGrow():
"""扩容 alias 表:池内分配新块并拷贝旧内容,容量翻倍(旧块留在池里,不回收)。
原实现把上限写死为 MLIR_ALIAS_CAP(512),超出直接 MlirFail 中断整次编译——
这是资源限制,不该让编译失败。大函数里 no-op bitcast(别名)条数会超过它。
"""
global _MLIR_AliasFrom, _MLIR_AliasTo, _MLIR_AliasCap
old_cap: int = _MLIR_AliasCap
nb_from: bytes = _MLIR_Pool.alloc(old_cap * 16)
nb_to: bytes = _MLIR_Pool.alloc(old_cap * 16)
if nb_from is None or nb_to is None:
MlirFail("MLIR render: alias table growth allocation failed")
string.memcpy(nb_from, _MLIR_AliasFrom, old_cap * 8)
string.memcpy(nb_to, _MLIR_AliasTo, old_cap * 8)
string.memset(nb_from + old_cap * 8, 0, old_cap * 8)
string.memset(nb_to + old_cap * 8, 0, old_cap * 8)
_MLIR_AliasFrom = nb_from
_MLIR_AliasTo = nb_to
_MLIR_AliasCap = old_cap * 2
def _MlirConstGrow():
"""扩容常量表 + 自动常量名空间(%c<seq>)+ 已用名表:池内分配翻倍新块并拷贝旧内容。
原实现三处都钉死在 MLIR_CONST_CAP(512):常量表满 → MlirFail("constant table overflow")、
%c0..%c511 用尽 → MlirFail、以及 _MlirMarkUsedCName 里"更大的后缀不可能冲突"的假设。
这些只是资源限制,不该中断编译,故统一按需扩容。
"""
global _MLIR_ConstTy, _MLIR_ConstVal, _MLIR_ConstName, _MLIR_UsedC, _MLIR_ConstCap
old_cap: int = _MLIR_ConstCap
nb_ty: bytes = _MLIR_Pool.alloc(old_cap * 16)
nb_val: bytes = _MLIR_Pool.alloc(old_cap * 16)
nb_nm: bytes = _MLIR_Pool.alloc(old_cap * 16)
nb_used: bytes = _MLIR_Pool.alloc(old_cap * 8)
if nb_ty is None or nb_val is None or nb_nm is None or nb_used is None:
MlirFail("MLIR render: constant table growth allocation failed")
string.memcpy(nb_ty, _MLIR_ConstTy, old_cap * 8)
string.memcpy(nb_val, _MLIR_ConstVal, old_cap * 8)
string.memcpy(nb_nm, _MLIR_ConstName, old_cap * 8)
string.memcpy(nb_used, _MLIR_UsedC, old_cap * 4)
string.memset(nb_ty + old_cap * 8, 0, old_cap * 8)
string.memset(nb_val + old_cap * 8, 0, old_cap * 8)
string.memset(nb_nm + old_cap * 8, 0, old_cap * 8)
string.memset(nb_used + old_cap * 4, 0, old_cap * 4)
_MLIR_ConstTy = nb_ty
_MLIR_ConstVal = nb_val
_MLIR_ConstName = nb_nm
_MLIR_UsedC = nb_used
_MLIR_ConstCap = old_cap * 2
def _MlirAliasAdd(from_name: str, to_name: str):
"""Register a no-op bitcast alias (from is replaced by to)"""
global _MLIR_AliasCount
if from_name is None or to_name is None:
return
if _MLIR_AliasCount >= _MLIR_AliasCap:
_MlirAliasGrow()
sp: str* = _mlir_slotp(_MLIR_AliasFrom, _MLIR_AliasCount)
st: str* = _mlir_slotp(_MLIR_AliasTo, _MLIR_AliasCount)
sp[0] = _mlir_dup(from_name)
st[0] = _mlir_dup(to_name)
_MLIR_AliasCount += 1
def _MlirResolve(name: str) -> str:
"""Resolve the alias chain (bitcast no-op result name -> real operand)"""
if name is None:
return None
cur: str = name
for g in range(64):
nx: str = _MlirAliasLookup(cur)
if nx is None:
return cur
cur = nx
return cur
def _MlirIsSsaNameChar(ch: char) -> int:
"""Return 1 if ch may appear in a bare SSA/symbol name [A-Za-z0-9_$.] (mirrors _ll_name_needs_quote)."""
if ch >= 'a' and ch <= 'z': return 1
if ch >= 'A' and ch <= 'Z': return 1
if ch >= '0' and ch <= '9': return 1
if ch == '_' or ch == '$' or ch == '.': return 1
return 0
def _usedc_set(idx: int, val: int):
"""Write the used-name table slot (_MLIR_UsedC is an i32[MLIR_CONST_CAP] via explicit byte offset)."""
if _MLIR_UsedC is None:
return
elem_ptr: int* = int*(uint64(_MLIR_UsedC) + idx * 4)
*elem_ptr = val
def _usedc_get(idx: int) -> int:
"""Read the used-name table slot (explicit byte offset, same convention as _map_get_i32)."""
if _MLIR_UsedC is None:
return 0
elem_ptr: int* = int*(uint64(_MLIR_UsedC) + idx * 4)
return *elem_ptr
def _MlirMarkUsedCName(name: str):
"""Mark name (an SSA name, with or without a leading '%') as used iff it is exactly c<digits>.
自动常量名 %c<seq> 的编号空间会按需扩容(见 _MlirConstGrow),所以这里记录**任意** c<digits>
后缀:越界的编号先扩容已用名表再置位,否则扩出来的编号可能与该函数自带的 c<N> 撞名。
"""
if name is None:
return
p: str = name
if p[0] == '%':
p = p + 1
if p[0] != 'c':
return
q: str = p + 1
d: int = 0
nd: int = 0
while _is_digit_ch(q[0]) != 0:
if d < MLIR_CONST_CAP:
d = d * 10 + (int(q[0]) - 48)
nd += 1
q = q + 1
if nd == 0:
return
if _MlirIsSsaNameChar(q[0]) != 0:
return
if d < MLIR_CONST_CAP:
_usedc_set(d, 1)
def _MlirScanUsedCText(text: str):
"""Mark every user SSA name of the form %c<digits> occurring in one intermediate line."""
if text is None:
return
p: str = text
while p[0] != '\0':
if p[0] == '%':
cs: str = p + 1
if cs[0] == 'c':
e: str = p + 2
d: int = 0
nd: int = 0
while _is_digit_ch(e[0]) != 0:
# 上限只用于防溢出:记录任何 c<digits>(名字空间会按需扩容,
# 不能再假设"更大的后缀不会与自动名冲突")
if d < 100000000:
d = d * 10 + (int(e[0]) - 48)
nd += 1
e = e + 1
if nd > 0 and _MlirIsSsaNameChar(e[0]) == 0:
if d >= _MLIR_ConstCap:
_MlirConstGrow()
_usedc_set(d, 1)
p = e
continue
p = p + 1
def _MlirCollectUsedCNames(func: Function*):
"""Mark all user SSA names of the form c<digits> in func (params + every intermediate line).
The constant materializer auto-names constants %c<seq>; a param or local literally named c1/c2
(legal Viper identifiers) would otherwise collide with it and mlir-opt reports
"redefinition of SSA value '%c1'". This pass records the exact used names so _MlirConstGet skips
those numbers; user names are never rewritten and there is no silent fallback (fail-fast below).
"""
if func is None:
return
params: GSList[Param]* = func.Params
if params is not None:
cur: Param* = params.Head
steps: int = 0
while cur is not None:
steps += 1
if steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: parameter chain may be cyclic")
_MlirMarkUsedCName(cur.Name)
cur = cur.Next
blocks: GSList[BasicBlock]* = func.Blocks
if blocks is None:
return
blk: BasicBlock* = blocks.Head
blk_steps: int = 0
while blk is not None:
blk_steps += 1
if blk_steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: function block chain may be cyclic")
if blk.Lines is not None:
lines: GSList[Line]* = blk.Lines
ln: Line* = lines.Head
ln_steps: int = 0
while ln is not None:
ln_steps += 1
if ln_steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: block instruction chain may be cyclic")
_MlirScanUsedCText(ln.Buf)
ln = ln.Next
blk = blk.Next
def _MlirConstGet(tytext: str, valtext: str) -> str:
"""Get (or create) the constant's materialized name %cN"""
global _MLIR_ConstCount, _MLIR_ConstNameSeq
for i in range(int(_MLIR_ConstCount)):
sp: str* = _mlir_slotp(_MLIR_ConstTy, int(i))
if sp[0] is not None and string.strcmp(sp[0], tytext) == 0:
sv: str* = _mlir_slotp(_MLIR_ConstVal, int(i))
if sv[0] is not None and string.strcmp(sv[0], valtext) == 0:
sn: str* = _mlir_slotp(_MLIR_ConstName, int(i))
return sn[0]
if _MLIR_Pass == 2:
# pass1 already scanned all instruction lines; no new constant should appear here
MlirFail("MLIR render: unmaterialized constant in emit pass (scan missed it)")
if _MLIR_ConstCount >= _MLIR_ConstCap:
_MlirConstGrow()
nm: str = _MLIR_Pool.alloc(32)
if nm is None:
MlirFail("MLIR render: constant name allocation failed")
# Auto name %c<seq>: skip numbers already used by the function's own SSA names (c0/c1/...).
# seq is strictly increasing, so names stay unique; it is decoupled from the table index, and the
# (type,value) cache above still makes identical constants reuse the same name.
seq: int = _MLIR_ConstNameSeq
while seq < _MLIR_ConstCap and _usedc_get(seq) != 0:
seq += 1
if seq >= _MLIR_ConstCap:
# 名字空间同样按需扩容:原来钉死在 %c0..%c511,用尽就 MlirFail
_MlirConstGrow()
while seq < _MLIR_ConstCap and _usedc_get(seq) != 0:
seq += 1
_MLIR_ConstNameSeq = seq + 1
viperlib.snprintf(nm, 32, "%%c%d", seq)
sp2: str* = _mlir_slotp(_MLIR_ConstTy, _MLIR_ConstCount)
sv2: str* = _mlir_slotp(_MLIR_ConstVal, _MLIR_ConstCount)
sn2: str* = _mlir_slotp(_MLIR_ConstName, _MLIR_ConstCount)
sp2[0] = _mlir_dup(tytext)
sv2[0] = _mlir_dup(valtext)
sn2[0] = nm
_MLIR_ConstCount += 1
return nm
def _MlirCxResName(resname: str, suffix: str) -> str:
"""cmpxchg's extractvalue result name: deterministically derived from the struct result name.
A bare MLIR SSA name's first char must be a letter or `_` (mlir-opt 22.1.8 measured: `%0_v`
errors "expected '=' after SSA name"), so a `_` is prefixed to the result name body:
`%0` -> `%_0_v` (old value [0]) / `%_0_ok` (success bit [1])
`%a` -> `%_a_v` / `%_a_ok`
Names map one-to-one to result names with no counter, so pass1 (alias registration) and
pass2 (emission) produce identical names and references never dangle.
"""
nm: str = _MLIR_Pool.alloc(64)
if nm is None:
MlirFail("MLIR render: cmpxchg result name allocation failed")
nm[0] = '\0'
_append_cstr(nm, 4096, "%_")
_append_cstr(nm, 4096, resname + 1)
_append_cstr(nm, 4096, suffix)
return nm
def _MlirFloatLiteral(val: str, mt: str) -> str:
"""Float constant text -> MLIR float literal text.
MLIR float literals have two forms (Builtin Dialect FloatAttr syntax:
`float-literal (':' float-type)?` | `hexadecimal-literal ':' float-type`):
- decimal float literal (e.g. `3.14 : f64`);
- hexadecimal bit pattern (e.g. `0x40091EB851EB851F : f64`, interpreted bitwise).
So the `0x...` bit pattern from ConstFloat passes through as-is (no precision loss);
converting it to `%e` (7 significant digits) would truncate and defeat the bit-pattern intent.
The pattern width must match the type (f16/f32/f64/f128 <-> 4/8/16/32 hex digits), else a loud
error, since MLIR interprets the hex value by the type width.
"""
if val[0] == '0' and (val[1] == 'x' or val[1] == 'X'):
want: int = 0
if string.strcmp(mt, "f16") == 0:
want = 4
elif string.strcmp(mt, "f32") == 0:
want = 8
elif string.strcmp(mt, "f64") == 0:
want = 16
elif string.strcmp(mt, "f128") == 0:
want = 32
if want == 0:
MlirFail("MLIR render: hex float literal type is none of f16/f32/f64/f128")
nd: int = 0
p: str = val + 2
while (p[0] >= '0' and p[0] <= '9') \
or (p[0] >= 'a' and p[0] <= 'f') \
or (p[0] >= 'A' and p[0] <= 'F'):
nd += 1
p = p + 1
if nd != want:
MlirFail("MLIR render: float bit-pattern width mismatches type (f16/f32/f64/f128 need 4/8/16/32 hex digits respectively)")
return val
# Already decimal text: use as-is
return val
def _MlirConstFail(tytext: str, valtext: str):
"""Unsupported constant form -> loud failure including the type and value.
MlirFail gives no backtrace, so print (MLIR 类型, 常量文本) — otherwise an
"unsupported constant operand form" is unattributable (which pointer/aggregate constant,
and from which operand shape). Supports i/f literals, true/false, null/zeroinitializer and
`@sym`; anything else (e.g. a pointer/aggregate-typed literal or a constant expression)
lands here and must be lowered explicitly.
"""
tlen: sizet = 0
vlen: sizet = 0
if tytext is not None: tlen = string.strlen(tytext)
if valtext is not None: vlen = string.strlen(valtext)
bsize: sizet = tlen + vlen + 160
buf: str = _MLIR_Pool.alloc(bsize)
if buf is None:
MlirFail("MLIR render: unsupported constant operand form")
viperlib.snprintf(buf, bsize, "MLIR render: unsupported constant operand form (type '%s', value '%s')", tytext, valtext)
MlirFail(buf)
def _MlirEmitOneConst(out: str, outsz: sizet, mt: str, val: str, nm: str):
"""Emit one constant-materialization op"""
_append_cstr(out, outsz, " ")
_append_cstr(out, outsz, nm)
if val[0] == '@':
# Symbol reference (global/function address): use the unified quoting rule
# (LLVM and MLIR bare-name rules differ: MLIR disallows leading digit/'.' and '-')
_append_cstr(out, outsz, " = llvm.mlir.addressof ")
_MlirSymRefAppend(out, outsz, val + 1)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, mt)
elif string.strcmp(val, "null") == 0 or string.strcmp(val, "zeroinitializer") == 0:
_append_cstr(out, outsz, " = llvm.mlir.zero : ")
_append_cstr(out, outsz, mt)
else:
lit: str = val
if mt[0] == 'f':
lit = _MlirFloatLiteral(val, mt)
elif mt[0] != 'i':
_MlirConstFail(mt, val)
_append_cstr(out, outsz, " = llvm.mlir.constant(")
_append_cstr(out, outsz, lit)
# i1's `true`/`false` are MLIR bare boolean keywords, no type suffix allowed:
# `llvm.mlir.constant(false : i1)` makes mlir-opt error "expected ')'".
# Other scalars must be written `value : type` (otherwise treated as an untyped attr, mismatching the result type).
if string.strcmp(lit, "true") != 0 and string.strcmp(lit, "false") != 0:
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, mt)
_append_cstr(out, outsz, ") : ")
_append_cstr(out, outsz, mt)
_append_cstr(out, outsz, "\n")
def _MlirEmitConsts(out: str, outsz: sizet):
"""Emit all constant-materialization ops at the entry block head in registration order"""
for i in range(int(_MLIR_ConstCount)):
st: str* = _mlir_slotp(_MLIR_ConstTy, int(i))
sv: str* = _mlir_slotp(_MLIR_ConstVal, int(i))
sn: str* = _mlir_slotp(_MLIR_ConstName, int(i))
_MlirEmitOneConst(out, outsz, st[0], sv[0], sn[0])
# ============================================================
# Parsing primitives for intermediate instruction text
# ============================================================
def _mlir_ws(p: str) -> str:
"""Skip whitespace (space/tab/newline)"""
if p is None:
return None
while p[0] == ' ' or p[0] == '\t' or p[0] == '\n' or p[0] == '\r':
p = p + 1
return p
def _mlir_delim(ch: int) -> int:
"""Whether ch ends a token"""
if ch == 0 or ch == 32 or ch == 9 or ch == 10 or ch == 13:
return 1
if ch == ',' or ch == '(' or ch == ')' or ch == '[' or ch == ']' \
or ch == ':' or ch == '=':
return 1
return 0
def _mlir_read_value(p: str, out: str, outsz: sizet) -> str:
"""Read a value token (%name / @"name" / %"name" / literal); returns the new cursor"""
p = _mlir_ws(p)
n: sizet = 0
if p[0] == '\0':
out[0] = '\0'
return p
if p[0] == '%' or p[0] == '@':
if n + 1 < outsz:
out[n] = p[0]
n += 1
p = p + 1
if p[0] == '"':
if n + 1 < outsz:
out[n] = '"'
n += 1
p = p + 1
while p[0] != '\0' and p[0] != '"':
if n + 1 < outsz:
out[n] = p[0]
n += 1
p = p + 1
if p[0] == '"':
if n + 1 < outsz:
out[n] = '"'
n += 1
p = p + 1
out[n] = '\0'
return p
while p[0] != '\0' and _mlir_delim(p[0]) == 0:
if n + 1 < outsz:
out[n] = p[0]
n += 1
p = p + 1
out[n] = '\0'
return p
def _mlir_read_type(p: str, out: str, outsz: sizet, nofunc: int = 0) -> str:
"""Read a type token (MLIR form !llvm... or scalar iN/fN, also LLVM aggregate forms).
Bracket balancing covers `[] {} () <>`: MLIR's !llvm.array<...>/!llvm.struct<(...)> contain
spaces and commas and must be read whole by depth counting; bracketless types (!llvm.ptr)
end at a top-level delimiter.
With nofunc=1, does NOT absorb the following ` (...)` function-type suffix: in a call's return
type position, `(...)` (LLVM syntax) is the callee's parameter type list, not part of the return
type, so it must be left to _MlirCall's variadic branch (see the comment there).
"""
p = _mlir_ws(p)
n: sizet = 0
if p[0] == '\0':
out[0] = '\0'
return p
c0: int = p[0]
if c0 == '!' or c0 == '[' or c0 == '{' or c0 == '(':
d: int = 0
while p[0] != '\0':
ch: int = p[0]
if ch == '[' or ch == '{' or ch == '(' or ch == '<':
d += 1
elif ch == ']' or ch == '}' or ch == ')' or ch == '>':
if n + 1 < outsz:
out[n] = char(ch)
n += 1
p = p + 1
d -= 1
if d <= 0:
break
continue
elif d == 0 and (ch == ' ' or ch == ',' or ch == '\t'
or ch == '\n' or ch == '\r'):
# Top-level delimiter: bracketless types (e.g. !llvm.ptr) end here
break
if n + 1 < outsz:
out[n] = char(ch)
n += 1
p = p + 1
while p[0] == '*':
if n + 1 < outsz:
out[n] = '*'
n += 1
p = p + 1
out[n] = '\0'
return p
if c0 == '%':
p = _mlir_read_value(p, out, outsz)
n = string.strlen(out)
while p[0] == '*':
if n + 1 < outsz:
out[n] = '*'
n += 1
p = p + 1
out[n] = '\0'
return p
# Ordinary word (i32 / i8* / void / double ...)
while p[0] != '\0' and _mlir_delim(p[0]) == 0:
if n + 1 < outsz:
out[n] = p[0]
n += 1
p = p + 1
# Function-type suffix " ( ... )" (not absorbed when nofunc=1, see the docstring)
q: str = _mlir_ws(p)
if nofunc == 0 and q[0] == '(':
if n + 1 < outsz:
out[n] = ' '
n += 1
p = q
d2: int = 0
while p[0] != '\0':
ch2: int = p[0]
if ch2 == '(':
d2 += 1
elif ch2 == ')':
if n + 1 < outsz:
out[n] = char(ch2)
n += 1
p = p + 1
d2 -= 1
if d2 <= 0:
break
continue
if n + 1 < outsz:
out[n] = char(ch2)
n += 1
p = p + 1
while p[0] == '*':
if n + 1 < outsz:
out[n] = '*'
n += 1
p = p + 1
out[n] = '\0'
return p
def _mlir_in_set(ob: str, setstr: str) -> int:
"""Whether opcode belongs to a "|a|b|c|"-form set"""
tb: str = _SC_Tb
tb[0] = '\0'
string.strcat(tb, "|")
string.strcat(tb, ob)
string.strcat(tb, "|")
if string.strstr(setstr, tb) is not None:
return 1
return 0
def _MlirBlkRef(t: str) -> str:
"""Block reference text: strip the leading '%' (MLIR blocks use ^name)"""
if t is None:
return ""
if t[0] == '%':
return t + 1
return t
def _MlirFindBlock(func: Function*, name: str) -> BasicBlock*:
"""Find a basic block by name (without %) within the function"""
if func is None or func.Blocks is None or name is None:
return None
blocks: GSList[BasicBlock]* = func.Blocks
blk: BasicBlock* = blocks.Head
steps: int = 0
while blk is not None:
steps += 1
if steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: block chain may be cyclic")
if blk.Name is not None and string.strcmp(blk.Name, name) == 0:
return blk
blk = blk.Next
return None
def _MlirIsPhiLine(text: str) -> int:
"""Whether the line is a phi line"""
if text is None:
return 0
if string.strstr(text, " = phi ") is not None:
return 1
return 0
# ============================================================
# Operand rendering (with constant materialization)
# ============================================================
def _MlirOperand(val: str, tyllvm: str, out: str, outsz: sizet) -> str:
"""Render one operand and return its final text.
SSA value -> %name after alias resolution;
non-SSA (literal/symbol ref) -> materialized %cN (registered during the scan pass).
"""
if val is None or val[0] == '\0':
MlirFail("MLIR render: empty operand")
if val[0] == '%':
r: str = _MlirResolve(val)
_append_cstr(out, outsz, r)
return r
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, tyllvm, _MLIR_Pool)
nm: str = _MlirConstGet(mt, val)
_append_cstr(out, outsz, nm)
return nm
# ============================================================
# phi parsing
# ============================================================
def _MlirPhiScan(text: str):
"""Scan a phi line: register all incoming constants (scan pass only)"""
p: str = _mlir_ws(text)
tb: str = _SC_Buf
p = _mlir_read_value(p, tb, 512)
p = _mlir_ws(p)
if p[0] == '=':
p = _mlir_ws(p + 1)
ob: str = _SC_Op
p = _mlir_read_value(p, ob, 64)
if string.strcmp(ob, "phi") != 0:
return
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
g: int = 0
while p[0] != '\0':
g += 1
if g > 512:
MlirFail("MLIR render: too many phi incoming edges")
p = _mlir_ws(p)
if p[0] != '[':
break
p = _mlir_ws(p + 1)
vb: str = _SC_P1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
bb: str = _SC_P2
p = _mlir_read_value(p, bb, 512)
p = _mlir_ws(p)
if p[0] == ']':
p = p + 1
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
# Register this incoming value (constants/symbol refs need materialization)
_MlirOperand(vb, tyb, None, 0)
def _MlirPhiParam(text: str) -> int:
"""Take a phi's result name and type as block params; on success writes _SC_B1(name)/_SC_B2(type) and returns 1"""
p: str = _mlir_ws(text)
nm: str = _SC_B1
p = _mlir_read_value(p, nm, 512)
p = _mlir_ws(p)
if p[0] != '=':
return 0
p = _mlir_ws(p + 1)
ob: str = _SC_Op
p = _mlir_read_value(p, ob, 64)
if string.strcmp(ob, "phi") != 0:
return 0
tyb: str = _SC_B2
_mlir_read_type(p, tyb, 512)
return 1
def _MlirPhiFindIncoming(text: str, src: BasicBlock*) -> int:
"""Find the phi incoming edge matching the source block; on success writes _SC_B1(value)/_SC_B2(type) and returns 1"""
p: str = _mlir_ws(text)
tb: str = _SC_Buf
p = _mlir_read_value(p, tb, 512)
p = _mlir_ws(p)
if p[0] == '=':
p = _mlir_ws(p + 1)
ob: str = _SC_Op
p = _mlir_read_value(p, ob, 64)
if string.strcmp(ob, "phi") != 0:
return 0
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
srcn: str = None
if src is not None:
srcn = src.Name
g: int = 0
while p[0] != '\0':
g += 1
if g > 512:
MlirFail("MLIR render: too many phi incoming edges")
p = _mlir_ws(p)
if p[0] != '[':
break
p = _mlir_ws(p + 1)
vb: str = _SC_P1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
bb: str = _SC_P2
p = _mlir_read_value(p, bb, 512)
p = _mlir_ws(p)
if p[0] == ']':
p = p + 1
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
if srcn is not None:
bn: str = bb
if bn[0] == '%':
bn = bn + 1
if string.strcmp(bn, srcn) == 0:
string.strcpy(_SC_B1, vb)
string.strcpy(_SC_B2, tyb)
return 1
return 0
# ============================================================
# Block parameters / block arguments
# ============================================================
def _MlirBlockHasPhi(blk: BasicBlock*) -> int:
"""Whether the block head has a phi (i.e. the block carries block params)"""
if blk is None or blk.Lines is None:
return 0
lines: GSList[Line]* = blk.Lines
ln: Line* = lines.Head
while ln is not None:
if ln.Buf is not None:
if _MlirIsPhiLine(ln.Buf) != 0:
return 1
return 0
ln = ln.Next
return 0
def _MlirBlockOperands(out: str, outsz: sizet,
target: BasicBlock*, src: BasicBlock*):
"""Emit target's block arguments when branching to it: (%v : ty, ...).
Incoming edges are matched to phis by source block; no match -> loud error (never silently drop args).
"""
if target is None or src is None:
return
if _MlirBlockHasPhi(target) == 0:
return
tmp: str = _SC_Tmp
tmp[0] = '\0'
lines: GSList[Line]* = target.Lines
ln: Line* = lines.Head
cnt: int = 0
while ln is not None:
if ln.Buf is None:
ln = ln.Next
continue
if _MlirIsPhiLine(ln.Buf) == 0:
break
if _MlirPhiFindIncoming(ln.Buf, src) == 0:
MlirFail("MLIR render: target block has phi but no incoming edge matches the source block")
if cnt > 0:
_append_cstr(tmp, 8192, ", ")
_MlirOperand(_SC_B1, _SC_B2, tmp, 4096)
_append_cstr(tmp, 8192, " : ")
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, _SC_B2, _MLIR_Pool)
_append_cstr(tmp, 8192, mt)
cnt += 1
ln = ln.Next
if cnt == 0:
MlirFail("MLIR render: target block has phi but no block arguments were generated")
_append_cstr(out, outsz, "(")
_append_cstr(out, outsz, tmp)
_append_cstr(out, outsz, ")")
def _MlirEmitBlockHeader(out: str, outsz: sizet, blk: BasicBlock*):
"""Emit a non-entry block header: ^name(%p: ty, ...):"""
_append_cstr(out, outsz, "^")
if blk.Name is not None:
_append_cstr(out, outsz, blk.Name)
if _MlirBlockHasPhi(blk) != 0:
tmp: str = _SC_Tmp
tmp[0] = '\0'
lines: GSList[Line]* = blk.Lines
ln: Line* = lines.Head
cnt: int = 0
while ln is not None:
if ln.Buf is None:
ln = ln.Next
continue
if _MlirIsPhiLine(ln.Buf) == 0:
break
if _MlirPhiParam(ln.Buf) != 0:
if cnt > 0:
_append_cstr(tmp, 8192, ", ")
_append_cstr(tmp, 8192, _SC_B1)
_append_cstr(tmp, 8192, ": ")
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, _SC_B2, _MLIR_Pool)
_append_cstr(tmp, 8192, mt)
cnt += 1
ln = ln.Next
if cnt == 0:
MlirFail("MLIR render: failed to parse leading phi")
_append_cstr(out, outsz, "(")
_append_cstr(out, outsz, tmp)
_append_cstr(out, outsz, ")")
_append_cstr(out, outsz, ":\n")
def _MlirEmitBlockLines(out: str, outsz: sizet, blk: BasicBlock*):
"""Emit in-block instructions (skip leading phis, which became block params)"""
if blk is None or blk.Lines is None:
return
lines: GSList[Line]* = blk.Lines
ln: Line* = lines.Head
skipping: int = 1
steps: int = 0
while ln is not None:
steps += 1
if steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: block instruction chain may be cyclic")
if ln.Buf is not None:
if skipping != 0 and _MlirIsPhiLine(ln.Buf) != 0:
ln = ln.Next
continue
skipping = 0
_MlirRenderLine(ln.Buf, out, outsz)
ln = ln.Next
def _MlirCheckEntryNoPhi(blk: BasicBlock*):
"""entry block must not have phis (no predecessor, cannot become block params)"""
if _MlirBlockHasPhi(blk) != 0:
MlirFail("MLIR render: entry block must not have phi (MLIR entry block takes no block arguments)")
# ============================================================
# Per-instruction MLIR rendering
# ============================================================
def _MlirEmitRes(out: str, outsz: sizet, resname: str):
"""Emit the " [%N = ]" prefix"""
_append_cstr(out, outsz, " ")
if resname is not None:
_append_cstr(out, outsz, resname)
_append_cstr(out, outsz, " = ")
def _MlirBinop(p: str, resname: str, ob: str, out: str, outsz: sizet):
"""Integer/float binary op: llvm.<op> %a, %b : ty"""
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
lb: str = _SC_V1
p = _mlir_read_value(p, lb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
rb: str = _SC_V2
_mlir_read_value(p, rb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.")
_append_cstr(out, outsz, ob)
_append_cstr(out, outsz, " ")
_MlirOperand(lb, tyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(rb, tyb, out, outsz)
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, tyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, mt)
_append_cstr(out, outsz, "\n")
def _MlirFneg(p: str, resname: str, out: str, outsz: sizet):
"""fneg: llvm.fneg %a : ty"""
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
vb: str = _SC_V1
_mlir_read_value(p, vb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.fneg ")
_MlirOperand(vb, tyb, out, outsz)
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, tyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, mt)
_append_cstr(out, outsz, "\n")
def _MlirCmp(p: str, resname: str, ob: str, out: str, outsz: sizet):
"""icmp/fcmp: llvm.icmp "sgt" %a, %b : ty
The intermediate text is LLVM syntax `icmp <pred> <type> <lhs>, <rhs>` -- predicate before type,
so the read order must be predicate -> type -> operands.
Do not read the type first: the predicate (e.g. `sgt`) would be passed as type text, triggering
"cannot convert to MLIR type text" and aborting.
"""
pb: str = _SC_P1
p = _mlir_read_value(p, pb, 512)
p = _mlir_ws(p)
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
lb: str = _SC_V1
p = _mlir_read_value(p, lb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
rb: str = _SC_V2
_mlir_read_value(p, rb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.")
_append_cstr(out, outsz, ob)
_append_cstr(out, outsz, " \"")
_append_cstr(out, outsz, pb)
_append_cstr(out, outsz, "\" ")
_MlirOperand(lb, tyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(rb, tyb, out, outsz)
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, tyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, mt)
_append_cstr(out, outsz, "\n")
def _MlirAlloca(p: str, resname: str, out: str, outsz: sizet):
"""alloca: llvm.alloca %cN x ty {alignment = N : i64} : (i32) -> !llvm.ptr
The MLIR form of the alignment attribute was determined by measuring mlir-opt 22.1.8
(do NOT rely on memory): `{alignment = 16 : i64}` -> exit 0 (adopted);
`align(16)` / `, align 16` (LLVM syntax) -> exit 1 (expected ':'); `{alignment = 16 : i32}`
-> exit 1 (requires i64). So the intermediate `, align N` must be rewritten to `{alignment = N : i64}`.
Alignment accepts only a decimal constant that is a power of two: mlir-opt does not check
power-of-two (`{alignment = 3 : i64}` exits 0), but non-powers are illegal in LLVM semantics, so this
layer gates it: non-constant/non-power/out-of-range (> 2^30) always errors loudly.
"""
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
p = _mlir_ws(p)
aligntxt: str = _SC_P2
aligntxt[0] = '\0'
if p[0] == ',':
# ", align N" (the intermediate-text form from build_alloca_aligned)
p = _mlir_ws(p + 1)
if string.strncmp(p, "align", 5) != 0:
MlirFail("MLIR render: alloca only supports the align attribute (unknown attribute)")
p = _mlir_ws(p + 5)
nd: int = 0
while p[0] >= '0' and p[0] <= '9':
if nd + 1 >= 512:
MlirFail("MLIR render: alloca alignment value too long (out of range)")
aligntxt[nd] = p[0]
nd += 1
p = p + 1
aligntxt[nd] = '\0'
p = _mlir_ws(p)
if p[0] != '\0':
MlirFail("MLIR render: extra token after alloca align")
if nd == 0:
MlirFail("MLIR render: alloca alignment must be a constant (decimal constant, not SSA)")
if nd > 10:
MlirFail("MLIR render: alloca alignment out of range (must be a power of 2 and <= 2^30)")
av: int = string.atoi(aligntxt)
if av <= 0 or av > 1073741824:
MlirFail("MLIR render: alloca alignment out of range (must be a power of 2 and <= 2^30)")
if (av & (av - 1)) != 0:
MlirFail("MLIR render: alloca alignment must be a power of 2")
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, tyb, _MLIR_Pool)
cnt: str = _MlirConstGet("i32", "1")
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.alloca ")
_append_cstr(out, outsz, cnt)
_append_cstr(out, outsz, " x ")
_append_cstr(out, outsz, mt)
if aligntxt[0] != '\0':
_append_cstr(out, outsz, " {alignment = ")
_append_cstr(out, outsz, aligntxt)
_append_cstr(out, outsz, " : i64}")
_append_cstr(out, outsz, " : (i32) -> !llvm.ptr\n")
def _MlirLoad(p: str, resname: str, out: str, outsz: sizet):
"""load: llvm.load %p : !llvm.ptr -> ty"""
p = _mlir_ws(p)
vol: int = 0
if string.strncmp(p, "volatile ", 9) == 0:
vol = 1
p = _mlir_ws(p + 9)
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ptyb: str = _SC_Ty2
p = _mlir_read_type(p, ptyb, 512)
p = _mlir_ws(p)
pb: str = _SC_V1
_mlir_read_value(p, pb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.load ")
if vol != 0:
_append_cstr(out, outsz, "volatile ")
_MlirOperand(pb, ptyb, out, outsz)
tmt: str = _SC_Mt
tmt[0] = '\0'
MlirTypeText(tmt, 4096, tyb, _MLIR_Pool)
pmt: str = _SC_Tmp2
pmt[0] = '\0'
MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, pmt)
_append_cstr(out, outsz, " -> ")
_append_cstr(out, outsz, tmt)
_append_cstr(out, outsz, "\n")
def _MlirStore(p: str, out: str, outsz: sizet):
"""store: llvm.store %v, %p : ty, !llvm.ptr"""
p = _mlir_ws(p)
vol: int = 0
if string.strncmp(p, "volatile ", 9) == 0:
vol = 1
p = _mlir_ws(p + 9)
vtyb: str = _SC_Ty
p = _mlir_read_type(p, vtyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ptyb: str = _SC_Ty2
p = _mlir_read_type(p, ptyb, 512)
p = _mlir_ws(p)
pb: str = _SC_V2
_mlir_read_value(p, pb, 512)
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.store ")
if vol != 0:
_append_cstr(out, outsz, "volatile ")
_MlirOperand(vb, vtyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(pb, ptyb, out, outsz)
vmt: str = _SC_Mt
vmt[0] = '\0'
MlirTypeText(vmt, 1024, vtyb, _MLIR_Pool)
pmt: str = _SC_Tmp2
pmt[0] = '\0'
MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, vmt)
_append_cstr(out, outsz, ", ")
_append_cstr(out, outsz, pmt)
_append_cstr(out, outsz, "\n")
def _MlirBr(p: str, out: str, outsz: sizet):
"""br: in LLVM syntax the conditional branch opcode is also `br`, dispatched by whether the first token is `label`.
br label %x -> llvm.br ^x
br i1 %c, label %a, label %b -> llvm.cond_br %c, ^a, ^b
Note: build_cond_br emits intermediate text `br i1 ...` (NOT `cond_br`), so the conditional
form must be forwarded here to _MlirCondBr; otherwise it is taken as unconditional and the
type `i1` is mistaken for a block name (producing `llvm.br ^i1` and dropping the condition).
"""
p = _mlir_ws(p)
if string.strncmp(p, "label ", 6) != 0:
# Conditional branch: `<type> <cond>, label %a, label %b`
_MlirCondBr(p, out, outsz)
return
p = _mlir_ws(p + 6)
tb: str = _SC_V1
_mlir_read_value(p, tb, 512)
br: str = _MlirBlkRef(tb)
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.br ^")
_append_cstr(out, outsz, br)
_MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, br), _MLIR_SrcBlk)
_append_cstr(out, outsz, "\n")
def _MlirCondBr(p: str, out: str, outsz: sizet):
"""cond_br: llvm.cond_br %c, ^then(%v : ty), ^else(%v : ty)"""
ctyb: str = _SC_Ty
p = _mlir_read_type(p, ctyb, 512)
p = _mlir_ws(p)
cb: str = _SC_V1
p = _mlir_read_value(p, cb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
if string.strncmp(p, "label ", 6) == 0:
p = _mlir_ws(p + 6)
tb: str = _SC_V2
p = _mlir_read_value(p, tb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
if string.strncmp(p, "label ", 6) == 0:
p = _mlir_ws(p + 6)
eb: str = _SC_V3
_mlir_read_value(p, eb, 512)
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.cond_br ")
_MlirOperand(cb, ctyb, out, outsz)
tr: str = _MlirBlkRef(tb)
_append_cstr(out, outsz, ", ^")
_append_cstr(out, outsz, tr)
_MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, tr), _MLIR_SrcBlk)
er: str = _MlirBlkRef(eb)
_append_cstr(out, outsz, ", ^")
_append_cstr(out, outsz, er)
_MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, er), _MLIR_SrcBlk)
_append_cstr(out, outsz, "\n")
def _MlirRet(p: str, out: str, outsz: sizet):
"""ret: llvm.return %v : ty (void omits operand and type)"""
p = _mlir_ws(p)
if string.strncmp(p, "void", 4) == 0 and (p[4] == '\0' or p[4] == ' '):
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.return\n")
return
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
vb: str = _SC_V1
_mlir_read_value(p, vb, 512)
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.return ")
_MlirOperand(vb, tyb, out, outsz)
mt: str = _SC_Mt
mt[0] = '\0'
MlirTypeText(mt, 4096, tyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, mt)
_append_cstr(out, outsz, "\n")
def _MlirUnreachable(out: str, outsz: sizet):
"""unreachable: llvm.unreachable"""
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.unreachable\n")
def _MlirCall(p: str, resname: str, out: str, outsz: sizet):
"""call: llvm.call @f(%a) : (i32) -> i32 / indirect llvm.call %fp(%a) : !llvm.ptr, (i32) -> i32
Variadic calls (intermediate text `call <ret> (...) @f(...)`) append `vararg(!llvm.func<...>)`.
"""
p = _mlir_ws(p)
variadic: int = 0
vftys: str = _SC_Tmp
vftys[0] = '\0'
rtyb: str = _SC_Ty
# The return type must be read with nofunc=1: in variadic-call text, the `(...)` after the
# return type is the callee's parameter type list (LLVM syntax), not part of the return type.
# Reading it as a function-type suffix would leave p at `@`/`%`, never entering the variadic
# branch, turning rtyb into `i32 (!llvm.ptr, i32, ...)`, which MlirTypeText splits on commas
# and misreports on the trailing `...`.
p = _mlir_read_type(p, rtyb, 512, 1)
p = _mlir_ws(p)
if string.strncmp(p, "asm", 3) == 0:
# Inline asm: intermediate text is `[%N = ]call <ret_ty> asm [sideeffect]
# [inteldialect] "<asm>", "<constraints>"(<ty> %v, ...)` (LLVM syntax);
# the MLIR form is rewritten by _MlirAsm.
_MlirAsm(p, resname, rtyb, out, outsz)
return
if p[0] == '(':
# Variadic call: intermediate text is `call <ret> (<arg types...>, ...) @f(<args>)`
# (builder's build_call is_variadic=1 form).
# MLIR form (mlir-opt 22.1.8 measured exit 0):
# %0 = llvm.call @printf(%p, %n) vararg(!llvm.func<i32 (!llvm.ptr, i32, ...)>)
# : (!llvm.ptr, i32) -> i32
# `vararg(...)` holds the callee's full MLIR function type (with `...`);
# for void returns that return type is `void` (writing `none` errors
# "expected var_callee_type to return void").
# Note: buffers like vftys/calb avoid _SC_Ty (rtyb holds it) and _SC_Tmp2 (used by the arg loop).
variadic = 1
p = p + 1
g0: int = 0
while p[0] != '\0':
g0 += 1
if g0 > 256:
MlirFail("MLIR render: too many function type params in variadic call")
p = _mlir_ws(p)
if p[0] == ')':
p = p + 1
break
if vftys[0] != '\0':
_append_cstr(vftys, 8192, ", ")
if p[0] == '.':
# `...` variadic token passes through (not a type; MlirTypeText rejects it)
_append_cstr(vftys, 8192, "...")
while p[0] == '.':
p = p + 1
else:
ftb: str = _SC_V2
ftb[0] = '\0'
p = _mlir_read_type(p, ftb, 512)
fmt: str = _SC_IdxT
fmt[0] = '\0'
MlirTypeText(fmt, 4096, ftb, _MLIR_Pool)
_append_cstr(vftys, 8192, fmt)
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
calb: str = _SC_Callee
p = _mlir_read_value(p, calb, 512)
p = _mlir_ws(p)
if p[0] != '(':
MlirFail("MLIR render: call missing argument list")
p = p + 1
args: str = _SC_Args
argts: str = _SC_ArgTs
args[0] = '\0'
argts[0] = '\0'
nargs: int = 0
g: int = 0
while p[0] != '\0':
g += 1
if g > 256:
MlirFail("MLIR render: too many call arguments")
p = _mlir_ws(p)
if p[0] == ')':
break
atyb: str = _SC_Ty2
p = _mlir_read_type(p, atyb, 512)
p = _mlir_ws(p)
avb: str = _SC_V1
p = _mlir_read_value(p, avb, 512)
if nargs > 0:
_append_cstr(args, 2048, ", ")
_append_cstr(argts, 8192, ", ")
_MlirOperand(avb, atyb, args, 2048)
amt: str = _SC_Tmp2
amt[0] = '\0'
MlirTypeText(amt, 4096, atyb, _MLIR_Pool)
_append_cstr(argts, 8192, amt)
nargs += 1
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
rmt: str = _SC_Mt
rmt[0] = '\0'
MlirTypeText(rmt, 1024, rtyb, _MLIR_Pool)
# LLVM intrinsics (bare symbol @llvm.xxx, not llvm.mlir.*) must use the generic op
# `llvm.call_intrinsic "llvm.xxx"(...) : (...) -> ...`: mlir-translate mangles the intrinsic
# name by operand types (e.g. llvm.va_start -> llvm.va_start.p0). A plain llvm.call would emit
# an unmangled @llvm.va_start and LLVM verification errors
# "Intrinsic name not mangled correctly for type arguments!".
if calb[0] == '@' and string.strncmp(calb + 1, "llvm.", 5) == 0 \
and string.strncmp(calb + 1, "llvm.mlir.", 10) != 0:
if string.strcmp(rmt, "none") == 0:
_MlirEmitRes(out, outsz, None)
else:
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.call_intrinsic \"")
_append_cstr(out, outsz, calb + 1)
_append_cstr(out, outsz, "\"(")
_append_cstr(out, outsz, args)
_append_cstr(out, outsz, ") : (")
_append_cstr(out, outsz, argts)
_append_cstr(out, outsz, ") -> ")
if string.strcmp(rmt, "none") == 0:
_append_cstr(out, outsz, "()")
else:
_append_cstr(out, outsz, rmt)
_append_cstr(out, outsz, "\n")
return
if string.strcmp(rmt, "none") == 0:
_MlirEmitRes(out, outsz, None)
else:
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.call ")
# callee 也必须过 alias 解析:no-op bitcast(含函数指针 bitcast)被省略后,间接调用点
# 必须改写成别名目标,否则 %N 悬空 → mlir-opt 报 use of undeclared SSA value
# / definition has type 'i8' but previously used here with type '!llvm.ptr'。
calb_r: str = _MlirResolve(calb)
if calb_r is None: calb_r = calb
_append_cstr(out, outsz, calb_r)
_append_cstr(out, outsz, "(")
_append_cstr(out, outsz, args)
_append_cstr(out, outsz, ")")
if variadic != 0:
# Variadic call: vararg(the callee's full MLIR function type) (includes `...`)
_append_cstr(out, outsz, " vararg(!llvm.func<")
if string.strcmp(rmt, "none") == 0:
# Void-returning variadic function type: write `void`, not `none`
_append_cstr(out, outsz, "void")
else:
_append_cstr(out, outsz, rmt)
_append_cstr(out, outsz, " (")
_append_cstr(out, outsz, vftys)
# Close three levels in order: the param list `)`, `!llvm.func<`'s `>`, `vararg(`'s `)`
# (omitting vararg's `)` made mlir-opt 22.1.8 report "expected ')'" verbatim)
_append_cstr(out, outsz, ")>)")
_append_cstr(out, outsz, " : ")
if calb_r[0] != '@':
# Indirect call: the callee is a !llvm.ptr operand
# (用解析后的名字判断:别名解析可能把 callee 指到一个 @符号,那就成了直接调用)
_append_cstr(out, outsz, "!llvm.ptr, ")
_append_cstr(out, outsz, "(")
_append_cstr(out, outsz, argts)
_append_cstr(out, outsz, ") -> ")
if string.strcmp(rmt, "none") == 0:
_append_cstr(out, outsz, "()")
else:
_append_cstr(out, outsz, rmt)
_append_cstr(out, outsz, "\n")
def _MlirAsm(p: str, resname: str, rtyb: str, out: str, outsz: sizet):
"""Inline asm: llvm.inline_asm [has_side_effects] [asm_dialect = intel] "...", "..." %a, %b : (i32, i32) -> i32
Intermediate text (LLVM syntax) has two sources:
builders' build_inline_asm -> `call <ret> asm sideeffect "...", "..."()`
TPV CallGenCBuiltin injected -> `call <ret> asm sideeffect inteldialect "...", "..."(<ty> %v, ...)`
The MLIR (llvm dialect) form was determined by measuring mlir-opt 22.1.8 (do NOT rely on memory):
* The dialect keyword is `asm_dialect = intel`: LLVM's bare `inteldialect` errors
"expected attribute value"; `asm_dialect = inteldialect` errors with the enum list [att, intel].
* Input operands are comma-separated bare operands without parens: `"...", "=r,r"(%x) : (i32) -> i32`
errors "expected ':'" (that is LLVM syntax and must be rewritten).
* No return => `: (<operand types...>) -> ()`; with return => `-> ty`.
* Multi-result inline asm is a single struct-typed result in MLIR: TPV's
`%N = call {i32, i32} asm sideeffect "...", "=r,r"()` renders as
`%N = llvm.inline_asm has_side_effects "...", "=r,r" : () -> !llvm.struct<(i32, i32)>`
(mlir-opt / mlir-translate 22.1.8 measured exit 0), then fields are extracted with
`llvm.extractvalue` (see _MlirExtractValue). No special branch is needed here: the return type
text goes through MlirTypeText (`{...}` -> `!llvm.struct<(...)>`), nested structs included.
* Measured passing combinations: has_side_effects x asm_dialect = intel x 0/1/2 operands x return/no-return.
Illegal forms always error loudly: unknown tokens besides `sideeffect`/`inteldialect`, missing
constraint string, non-string literal, unpaired args, or a return value without a result name.
"""
p = _mlir_ws(p + 3)
side: int = 0
intel: int = 0
g: int = 0
while g < 8:
g += 1
if string.strncmp(p, "sideeffect", 10) == 0 and (p[10] == ' ' or p[10] == '\t'):
side = 1
p = _mlir_ws(p + 10)
continue
if string.strncmp(p, "inteldialect", 12) == 0 and (p[12] == ' ' or p[12] == '\t'):
intel = 1
p = _mlir_ws(p + 12)
continue
break
# Asm text can reach several KB (TPV's ASM_LINE_BUF_SIZE=4096); use a 4096 buffer, not a
# small one, to avoid truncation; escape pairs (\22/\5C/\0A) pass through as MLIR accepts them.
asmb: str = _SC_Tmp
p = _mlir_read_value(p, asmb, 4096)
if asmb[0] != '"':
MlirFail("MLIR render: inline_asm missing asm text (or contains an unknown keyword)")
p = _mlir_ws(p)
if p[0] != ',':
MlirFail("MLIR render: inline_asm missing constraint string (must be \"asm\", \"constraints\")")
p = _mlir_ws(p + 1)
conb: str = _SC_Args
p = _mlir_read_value(p, conb, 2048)
if conb[0] != '"':
MlirFail("MLIR render: inline_asm constraint string must be a string literal")
p = _mlir_ws(p)
if p[0] != '(':
MlirFail("MLIR render: inline_asm missing argument list '(...)'")
p = p + 1
ops: str = _SC_ArgTs
ops[0] = '\0'
tys: str = _SC_IdxV
tys[0] = '\0'
nops: int = 0
g = 0
while p[0] != '\0':
g += 1
if g > 256:
MlirFail("MLIR render: too many inline_asm arguments")
p = _mlir_ws(p)
if p[0] == ')':
break
atyb: str = _SC_Ty2
p = _mlir_read_type(p, atyb, 512)
p = _mlir_ws(p)
avb: str = _SC_V1
p = _mlir_read_value(p, avb, 512)
if nops > 0:
_append_cstr(ops, 8192, ", ")
_append_cstr(tys, 1024, ", ")
_MlirOperand(avb, atyb, ops, 2048)
amt: str = _SC_Mt
amt[0] = '\0'
MlirTypeText(amt, 4096, atyb, _MLIR_Pool)
_append_cstr(tys, 1024, amt)
nops += 1
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
if p[0] == ')':
p = p + 1
p = _mlir_ws(p)
if p[0] != '\0':
MlirFail("MLIR render: extra token after inline_asm argument list")
rmt: str = _SC_Buf
rmt[0] = '\0'
MlirTypeText(rmt, 1024, rtyb, _MLIR_Pool)
if string.strcmp(rmt, "none") == 0:
_MlirEmitRes(out, outsz, None)
else:
if resname is None:
MlirFail("MLIR render: inline_asm has a return value but no result name (%N = call ...)")
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.inline_asm")
if side != 0:
_append_cstr(out, outsz, " has_side_effects")
if intel != 0:
_append_cstr(out, outsz, " asm_dialect = intel")
_append_cstr(out, outsz, " ")
_append_cstr(out, outsz, asmb)
_append_cstr(out, outsz, ", ")
_append_cstr(out, outsz, conb)
if nops > 0:
_append_cstr(out, outsz, " ")
_append_cstr(out, outsz, ops)
_append_cstr(out, outsz, " : (")
_append_cstr(out, outsz, tys)
_append_cstr(out, outsz, ") -> ")
if string.strcmp(rmt, "none") == 0:
_append_cstr(out, outsz, "()")
else:
_append_cstr(out, outsz, rmt)
_append_cstr(out, outsz, "\n")
def _MlirCast(p: str, resname: str, ob: str, out: str, outsz: sizet):
"""Cast instruction: llvm.zext %v : i32 to i64"""
ftyb: str = _SC_Ty
p = _mlir_read_type(p, ftyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if string.strncmp(p, "to ", 3) == 0:
p = _mlir_ws(p + 3)
ttyb: str = _SC_Ty2
_mlir_read_type(p, ttyb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.")
_append_cstr(out, outsz, ob)
_append_cstr(out, outsz, " ")
_MlirOperand(vb, ftyb, out, outsz)
fmt: str = _SC_Mt
fmt[0] = '\0'
MlirTypeText(fmt, 4096, ftyb, _MLIR_Pool)
tmt: str = _SC_Tmp2
tmt[0] = '\0'
MlirTypeText(tmt, 4096, ttyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, fmt)
_append_cstr(out, outsz, " to ")
_append_cstr(out, outsz, tmt)
_append_cstr(out, outsz, "\n")
def _MlirBitcast(p: str, resname: str, out: str, outsz: sizet):
"""bitcast: ptr->ptr is treated as a no-op (records an alias); otherwise llvm.bitcast %v : T to U"""
ftyb: str = _SC_Ty
p = _mlir_read_type(p, ftyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if string.strncmp(p, "to ", 3) == 0:
p = _mlir_ws(p + 3)
ttyb: str = _SC_Ty2
_mlir_read_type(p, ttyb, 512)
if MlirTypeTextIsPointer(ftyb) != 0 and MlirTypeTextIsPointer(ttyb) != 0:
# With opaque pointers, a ptr->ptr bitcast is meaningless -> no-op:
# register the result name as an alias of the source operand (later refs auto-rewritten), emit nothing
if resname is None:
MlirFail("MLIR render: no-op bitcast missing result name")
opnd: str = _MlirOperand(vb, ftyb, None, 0)
_MlirAliasAdd(resname, opnd)
return
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.bitcast ")
_MlirOperand(vb, ftyb, out, outsz)
fmt: str = _SC_Mt
fmt[0] = '\0'
MlirTypeText(fmt, 4096, ftyb, _MLIR_Pool)
tmt: str = _SC_Tmp2
tmt[0] = '\0'
MlirTypeText(tmt, 4096, ttyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, fmt)
_append_cstr(out, outsz, " to ")
_append_cstr(out, outsz, tmt)
_append_cstr(out, outsz, "\n")
def _MlirSelect(p: str, resname: str, out: str, outsz: sizet):
"""select: llvm.select %c, %a, %b : i1, i32"""
ctyb: str = _SC_Ty
p = _mlir_read_type(p, ctyb, 512)
p = _mlir_ws(p)
cb: str = _SC_V1
p = _mlir_read_value(p, cb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ttyb: str = _SC_Ty2
p = _mlir_read_type(p, ttyb, 512)
p = _mlir_ws(p)
tb: str = _SC_V2
p = _mlir_read_value(p, tb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ftyb: str = _SC_Buf
p = _mlir_read_type(p, ftyb, 512)
p = _mlir_ws(p)
fb: str = _SC_V3
_mlir_read_value(p, fb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.select ")
_MlirOperand(cb, ctyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(tb, ttyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(fb, ttyb, out, outsz)
cmt: str = _SC_Mt
cmt[0] = '\0'
MlirTypeText(cmt, 4096, ctyb, _MLIR_Pool)
vmt: str = _SC_Tmp2
vmt[0] = '\0'
MlirTypeText(vmt, 1024, ttyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, cmt)
_append_cstr(out, outsz, ", ")
_append_cstr(out, outsz, vmt)
_append_cstr(out, outsz, "\n")
def _MlirGep(p: str, resname: str, out: str, outsz: sizet):
"""getelementptr: llvm.getelementptr %p[i0, i1] : (!llvm.ptr, t0, t1) -> !llvm.ptr, base"""
baseb: str = _SC_Ty2
p = _mlir_read_type(p, baseb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ptyb: str = _SC_Buf
p = _mlir_read_type(p, ptyb, 512)
p = _mlir_ws(p)
pb: str = _SC_V3
p = _mlir_read_value(p, pb, 512)
idxv: str = _SC_IdxV
idxt: str = _SC_IdxT
idxv[0] = '\0'
idxt[0] = '\0'
p = _mlir_ws(p)
nidx: int = 0
tmpv: str = _SC_Tmp
tmpv[0] = '\0'
g: int = 0
while p[0] == ',':
g += 1
if g > 32:
MlirFail("MLIR render: too many GEP indices")
p = _mlir_ws(p + 1)
ityb: str = _SC_Ty
p = _mlir_read_type(p, ityb, 512)
p = _mlir_ws(p)
ivb: str = _SC_V1
p = _mlir_read_value(p, ivb, 512)
# Constant indices must be written as integer literals (e.g. `[0, 1]`), not materialized
# as SSA operands: MLIR's llvm.getelementptr requires struct-field indices to be constant,
# else the verifier errors 'expected index N indexing a struct to be constant'
# (operand form only populates rawConstantIndices' dynamic slot). Only non-constant (SSA,
# starting with '%') indices are materialized and take a type slot; constants do not.
if nidx > 0:
_append_cstr(idxv, 1024, ", ")
if ivb[0] == '%':
_MlirOperand(ivb, ityb, tmpv, 4096)
_append_cstr(idxv, 1024, tmpv)
tmpv[0] = '\0'
imt: str = _SC_Mt
imt[0] = '\0'
MlirTypeText(imt, 4096, ityb, _MLIR_Pool)
if idxt[0] != '\0':
_append_cstr(idxt, 4096, ", ")
_append_cstr(idxt, 4096, imt)
else:
_append_cstr(idxv, 1024, ivb)
nidx += 1
p = _mlir_ws(p)
if nidx == 0:
MlirFail("MLIR render: GEP missing index")
bmt: str = _SC_Tmp2
bmt[0] = '\0'
MlirTypeText(bmt, 4096, baseb, _MLIR_Pool)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.getelementptr ")
_MlirOperand(pb, ptyb, out, outsz)
_append_cstr(out, outsz, "[")
_append_cstr(out, outsz, idxv)
_append_cstr(out, outsz, "] : (!llvm.ptr")
# One type per index
_MlirAppendCommaTypes(out, outsz, idxt)
_append_cstr(out, outsz, ") -> !llvm.ptr, ")
_append_cstr(out, outsz, bmt)
_append_cstr(out, outsz, "\n")
def _MlirAppendCommaTypes(out: str, outsz: sizet, tylist: str):
"""Append ", t0, t1" to out (tylist is a comma-separated type string)"""
if tylist is None or tylist[0] == '\0':
return
_append_cstr(out, outsz, ", ")
_append_cstr(out, outsz, tylist)
def _MlirSwitch(p: str, out: str, outsz: sizet):
"""switch: llvm.switch %v : i32, ^default [0: ^case0, 1: ^case1]
The form was determined by measuring mlir-opt 22.1.8 (the design-doc form
`llvm.switch %v, ^default [0: ^case0] : i32` is REJECTED: "expected ':'"):
`llvm.switch %v : i32, ^default [0: ^c0, 1: ^c1]` -> exit 0 (adopted)
`llvm.switch %v : i32, ^default(%r : i32) [ ... ]` -> exit 0 (default block args)
`llvm.switch %v : i32, ^default [0: ^c0(%r : i32)]` -> exit 0 (case block args)
`llvm.switch %v : i64, ^default [4294967296: ^c0]` -> exit 0 (case value follows value type)
`llvm.switch %v : i32, ^default [-1: ^c0]` -> exit 0
`llvm.switch %v : i32, ^default []` -> exit 0 (empty case table)
i.e. the value type follows the value (`%v : i32,`); block args are written exactly like
br/cond_br, so _MlirBlockOperands' phi->block-arg facility is reused as-is => target blocks may
carry phis (a missing incoming edge errors loudly, never silently dropping args).
Case values must still be constants: in MLIR they are attributes (case_values), not SSA values.
"""
tyb: str = _SC_Ty
p = _mlir_read_type(p, tyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
if string.strncmp(p, "label ", 6) == 0:
p = _mlir_ws(p + 6)
db: str = _SC_V2
p = _mlir_read_value(p, db, 512)
p = _mlir_ws(p)
if p[0] == '[':
p = p + 1
# Materialize the value operand and its MLIR type text first: _MlirBlockOperands inside the
# case loop reuses _SC_Ty/_SC_Buf/_SC_P1/_SC_P2 scratch, so capture into a separate buffer first.
vop: str = _SC_IdxT
vop[0] = '\0'
_MlirOperand(vb, tyb, vop, 1024)
vmt: str = _SC_IdxV
vmt[0] = '\0'
MlirTypeText(vmt, 1024, tyb, _MLIR_Pool)
cases: str = _SC_Tmp2
cases[0] = '\0'
ncase: int = 0
g: int = 0
while p[0] != '\0':
g += 1
if g > 1024:
MlirFail("MLIR render: too many switch branches")
p = _mlir_ws(p)
if p[0] == ']' or p[0] == '\0':
break
ctb: str = _SC_P1
p = _mlir_read_type(p, ctb, 512)
p = _mlir_ws(p)
cvb: str = _SC_P2
p = _mlir_read_value(p, cvb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
if string.strncmp(p, "label ", 6) == 0:
p = _mlir_ws(p + 6)
cb: str = _SC_Ty2
p = _mlir_read_value(p, cb, 512)
if cvb[0] == '%':
MlirFail("MLIR render: switch case value must be a constant (SSA value not supported)")
cbr: str = _MlirBlkRef(cb)
if ncase > 0:
_append_cstr(cases, 4096, ", ")
_append_cstr(cases, 4096, cvb)
_append_cstr(cases, 4096, ": ^")
_append_cstr(cases, 4096, cbr)
# Target block has phis => append block arguments (same facility as br/cond_br)
_MlirBlockOperands(cases, 1024, _MlirFindBlock(_MLIR_Func, cbr), _MLIR_SrcBlk)
ncase += 1
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
dr: str = _MlirBlkRef(db)
_MlirEmitRes(out, outsz, None)
_append_cstr(out, outsz, "llvm.switch ")
_append_cstr(out, outsz, vop)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, vmt)
_append_cstr(out, outsz, ", ^")
_append_cstr(out, outsz, dr)
_MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, dr), _MLIR_SrcBlk)
_append_cstr(out, outsz, " [")
_append_cstr(out, outsz, cases)
_append_cstr(out, outsz, "]\n")
def _MlirAtomicRmw(p: str, resname: str, out: str, outsz: sizet):
"""atomicrmw: llvm.atomicrmw add %p, %v monotonic : !llvm.ptr, i32"""
aop: str = _SC_P1
p = _mlir_read_value(p, aop, 512)
p = _mlir_ws(p)
ptyb: str = _SC_Ty
p = _mlir_read_type(p, ptyb, 512)
p = _mlir_ws(p)
pb: str = _SC_V1
p = _mlir_read_value(p, pb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
vtyb: str = _SC_Ty2
p = _mlir_read_type(p, vtyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V2
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
ordb: str = _SC_P2
_mlir_read_value(p, ordb, 512)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.atomicrmw ")
_append_cstr(out, outsz, aop)
_append_cstr(out, outsz, " ")
_MlirOperand(pb, ptyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(vb, vtyb, out, outsz)
_append_cstr(out, outsz, " ")
_append_cstr(out, outsz, ordb)
pmt: str = _SC_Mt
pmt[0] = '\0'
MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool)
vmt: str = _SC_Tmp2
vmt[0] = '\0'
MlirTypeText(vmt, 1024, vtyb, _MLIR_Pool)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, pmt)
_append_cstr(out, outsz, ", ")
_append_cstr(out, outsz, vmt)
_append_cstr(out, outsz, "\n")
def _MlirCmpXchg(p: str, resname: str, resname2: str,
out: str, outsz: sizet):
"""cmpxchg: MLIR single result struct{T, i1} + two extractvalues for old value / success bit.
Intermediate text (LLVM syntax, two forms: builder single result / two-result prefix)
%N = cmpxchg <ptr_ty> <ptr>, <cmp_ty> <cmp>, <new_ty> <new> <succ> <fail>
%a, %b = cmpxchg ...
MLIR form (mlir-opt 22.1.8 measured exit 0; cmpxchg is a single result in MLIR, yielding
struct{T, i1}, not two SSA values):
%N = llvm.cmpxchg %p, %c, %n acq_rel monotonic : !llvm.ptr, i32
%_N_v = llvm.extractvalue %N[0] : !llvm.struct<(i32, i1)> ; old value (compared value)
%_N_ok = llvm.extractvalue %N[1] : !llvm.struct<(i32, i1)> ; i1 success bit
The two extractvalue result names are deterministically derived from the struct result name
(_MlirCxResName, e.g. `%0` -> `%_0_v`/`%_0_ok`).
Bindings: `%N` (single result) / `%a` (two-result first name) -> old value `[0]`;
two-result second name `%b` -> success bit `[1]`. Bindings use the same alias facility as
bitcast no-op (_MlirAliasAdd); later references are auto-rewritten by _MlirResolve to the
extractvalue result names. This preserves builder-side build_cmpxchg's "T-typed Value" (old value) semantics.
"""
ptyb: str = _SC_Ty
p = _mlir_read_type(p, ptyb, 512)
p = _mlir_ws(p)
pb: str = _SC_V1
p = _mlir_read_value(p, pb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ctyb: str = _SC_Ty2
p = _mlir_read_type(p, ctyb, 512)
p = _mlir_ws(p)
cb: str = _SC_V2
p = _mlir_read_value(p, cb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
ntyb: str = _SC_B1
p = _mlir_read_type(p, ntyb, 512)
p = _mlir_ws(p)
nb: str = _SC_B2
p = _mlir_read_value(p, nb, 512)
p = _mlir_ws(p)
succ: str = _SC_P1
p = _mlir_read_value(p, succ, 512)
p = _mlir_ws(p)
fail: str = _SC_P2
_mlir_read_value(p, fail, 512)
if resname is None:
MlirFail("MLIR render: cmpxchg missing result name")
# The two extractvalue result names derive deterministically from the struct name (identical across both passes)
onm: str = _MlirCxResName(resname, "_v")
snm: str = _MlirCxResName(resname, "_ok")
_MlirAliasAdd(resname, onm)
if resname2 is not None:
_MlirAliasAdd(resname2, snm)
# MLIR type text (use a non-_SC_Mt buffer: _MlirOperand occupies _SC_Mt for literals)
pmt: str = _SC_Tmp2
pmt[0] = '\0'
MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool)
vmt: str = _SC_IdxV
vmt[0] = '\0'
MlirTypeText(vmt, 1024, ctyb, _MLIR_Pool)
stmt: str = _SC_IdxT
stmt[0] = '\0'
_append_cstr(stmt, 4096, "!llvm.struct<(")
_append_cstr(stmt, 4096, vmt)
_append_cstr(stmt, 4096, ", i1)>")
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.cmpxchg ")
_MlirOperand(pb, ptyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(cb, ctyb, out, outsz)
_append_cstr(out, outsz, ", ")
_MlirOperand(nb, ntyb, out, outsz)
_append_cstr(out, outsz, " ")
_append_cstr(out, outsz, succ)
_append_cstr(out, outsz, " ")
_append_cstr(out, outsz, fail)
_append_cstr(out, outsz, " : ")
_append_cstr(out, outsz, pmt)
_append_cstr(out, outsz, ", ")
_append_cstr(out, outsz, vmt)
_append_cstr(out, outsz, "\n")
_MlirEmitRes(out, outsz, onm)
_append_cstr(out, outsz, "llvm.extractvalue ")
_append_cstr(out, outsz, resname)
_append_cstr(out, outsz, "[0] : ")
_append_cstr(out, outsz, stmt)
_append_cstr(out, outsz, "\n")
_MlirEmitRes(out, outsz, snm)
_append_cstr(out, outsz, "llvm.extractvalue ")
_append_cstr(out, outsz, resname)
_append_cstr(out, outsz, "[1] : ")
_append_cstr(out, outsz, stmt)
_append_cstr(out, outsz, "\n")
def _MlirVaArg(p: str, resname: str, out: str, outsz: sizet):
"""va_arg: use the generic op syntax (deliberate choice, see below).
Intermediate text (LLVM syntax): %N = va_arg <va_list_ty> <va_list_ptr>, <arg_ty>
MLIR text: %N = "llvm.va_arg"(%ap) : (!llvm.ptr) -> i32
Why generic form is required (mlir-opt 22.1.8 measured): the custom assembly syntax
`llvm.va_arg %ap, i32` requires an explicit va_list type (in LLVM it is `ptr`), which cannot
be expressed with opaque pointers -- mlir-opt errors "expected builtin.function". So the
generic op form is used, described only by operand/result types. Down-level verification
(mlir-translate --mlir-to-llvmir) yields `%2 = va_arg ptr %0, i32`, isomorphic to LLVM's va_arg.
"""
vtyb: str = _SC_Ty
p = _mlir_read_type(p, vtyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V1
p = _mlir_read_value(p, vb, 512)
p = _mlir_ws(p)
if p[0] == ',':
p = _mlir_ws(p + 1)
atyb: str = _SC_Ty2
_mlir_read_type(p, atyb, 512)
if resname is None:
MlirFail("MLIR render: va_arg missing result name")
# Operand/result types (_MlirOperand only uses _SC_Mt, so take separate buffers here)
omt: str = _SC_Tmp2
omt[0] = '\0'
MlirTypeText(omt, 4096, vtyb, _MLIR_Pool)
rmt: str = _SC_IdxV
rmt[0] = '\0'
MlirTypeText(rmt, 1024, atyb, _MLIR_Pool)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "\"llvm.va_arg\"(")
_MlirOperand(vb, vtyb, out, outsz)
_append_cstr(out, outsz, ") : (")
_append_cstr(out, outsz, omt)
_append_cstr(out, outsz, ") -> ")
_append_cstr(out, outsz, rmt)
_append_cstr(out, outsz, "\n")
def _MlirExtractValue(p: str, resname: str, out: str, outsz: sizet):
"""extractvalue: llvm.extractvalue %v[i0, i1] : <aggregate type>
Intermediate text is LLVM syntax (aggregate type BEFORE the value, indices after commas):
%r = extractvalue {i32, i32} %v, 0
%r = extractvalue {i32, {i32, i32}} %v, 1, 0 ; multi-level index
MLIR form (mlir-opt 22.1.8 measured exit 0):
%r = llvm.extractvalue %v[0] : !llvm.struct<(i32, i32)>
%r = llvm.extractvalue %v[1, 0] : !llvm.struct<(i32, !llvm.struct<(i32, i32)>)>
The index list is written inside `[]` separated by ", " in MLIR (different from LLVM's
`<aggregate>, i0, i1` syntax, so it must be rewritten); the aggregate type text goes through
MlirTypeText (anonymous `{...}` -> !llvm.struct<(...)>, named `%"name"` -> inlined named body,
array `[N x T]` -> !llvm.array<N x T>, all measured exit 0).
Real source: TPV's multi-result inline asm (`%asm_result = call {i32, i32} asm ...`) followed by
per-field extraction (`%asm_out_0 = extractvalue {i32, i32} %asm_result, 0`); multi-level indices
are also supported (measured legal). Indices must be constant literals (LLVM syntax) -- SSA index => loud error.
"""
atyb: str = _SC_Ty
p = _mlir_read_type(p, atyb, 512)
p = _mlir_ws(p)
vb: str = _SC_V1
p = _mlir_read_value(p, vb, 512)
idxs: str = _SC_IdxV
idxs[0] = '\0'
nidx: int = 0
g: int = 0
# Index list: comma-separated in LLVM syntax (`<aggregate>, i0, i1`); read each and rewrite.
# Loop bound matches other parse loops in this file (bounded; exceeding errors loudly).
while g < 64:
p = _mlir_ws(p)
if p[0] != ',':
break
g += 1
if g > 32:
MlirFail("MLIR render: too many extractvalue indices")
p = _mlir_ws(p + 1)
ib: str = _SC_IdxT
p = _mlir_read_value(p, ib, 512)
if ib[0] == '\0':
MlirFail("MLIR render: extractvalue missing index")
if ib[0] == '%':
MlirFail("MLIR render: extractvalue index must be a constant (SSA index not supported)")
if nidx > 0:
_append_cstr(idxs, 1024, ", ")
_append_cstr(idxs, 1024, ib)
nidx += 1
if nidx == 0:
MlirFail("MLIR render: extractvalue missing index")
if p[0] != '\0':
MlirFail("MLIR render: extra token after extractvalue index")
# Aggregate type text goes into a separate buffer first (_MlirOperand only uses _SC_Mt)
amt: str = _SC_Tmp2
amt[0] = '\0'
MlirTypeText(amt, 4096, atyb, _MLIR_Pool)
_MlirEmitRes(out, outsz, resname)
_append_cstr(out, outsz, "llvm.extractvalue ")
_MlirOperand(vb, atyb, out, outsz)
_append_cstr(out, outsz, "[")
_append_cstr(out, outsz, idxs)
_append_cstr(out, outsz, "] : ")
_append_cstr(out, outsz, amt)
_append_cstr(out, outsz, "\n")
# ============================================================
# Instruction line -> MLIR dispatch
# ============================================================
def _MlirRenderLine(text: str, out: str, outsz: sizet):
"""Parse one intermediate instruction text and render it to MLIR (out=None: register/validate only)"""
global _MLIR_Pass, _MLIR_CurLine
if text is None:
return
# Stash the "current line" at entry: printed on type-text conversion failure (both passes
# use the same line, so pass1 failures are also locatable). text points to Line.Buf, live for the whole render.
_MLIR_CurLine = text
MlirSetContextLine(_MLIR_CurLine)
p: str = _mlir_ws(text)
if p[0] == '\0':
return
resname: str = None
resname2: str = None
if p[0] == '%':
tb: str = _SC_Tb
q: str = _mlir_read_value(p, tb, 512)
q2: str = _mlir_ws(q)
if q2[0] == '=':
resname = _mlir_dup(tb)
p = _mlir_ws(q2 + 1)
elif q2[0] == ',':
# Two-result prefix `%a, %b = <op>` (cmpxchg only): MLIR's cmpxchg is a single
# struct{T, i1} result; the two names each bind to one extractvalue (see _MlirCmpXchg).
resname = _mlir_dup(tb)
q3: str = _mlir_ws(q2 + 1)
tb2: str = _SC_V3
q3 = _mlir_read_value(q3, tb2, 512)
q4: str = _mlir_ws(q3)
if q4[0] != '=':
MlirFail("MLIR render: double-result prefix (%a, %b =) missing '='")
resname2 = _mlir_dup(tb2)
p = _mlir_ws(q4 + 1)
ob: str = _SC_Op
p = _mlir_read_value(p, ob, 64)
if ob[0] == '\0':
MlirFail("MLIR render: empty instruction")
if resname2 is not None and string.strcmp(ob, "cmpxchg") != 0:
MlirFail("MLIR render: double-result prefix (%a, %b =) only supports cmpxchg")
if _mlir_in_set(ob, "|add|sub|mul|sdiv|udiv|srem|urem|and|or|xor|shl|lshr|ashr|") != 0:
_MlirBinop(p, resname, ob, out, outsz)
return
if _mlir_in_set(ob, "|fadd|fsub|fmul|fdiv|frem|") != 0:
_MlirBinop(p, resname, ob, out, outsz)
return
if string.strcmp(ob, "fneg") == 0:
_MlirFneg(p, resname, out, outsz)
return
if string.strcmp(ob, "icmp") == 0:
_MlirCmp(p, resname, ob, out, outsz)
return
if string.strcmp(ob, "fcmp") == 0:
_MlirCmp(p, resname, ob, out, outsz)
return
if string.strcmp(ob, "alloca") == 0:
_MlirAlloca(p, resname, out, outsz)
return
if string.strcmp(ob, "load") == 0:
_MlirLoad(p, resname, out, outsz)
return
if string.strcmp(ob, "store") == 0:
_MlirStore(p, out, outsz)
return
if string.strcmp(ob, "br") == 0:
_MlirBr(p, out, outsz)
return
if string.strcmp(ob, "ret") == 0:
_MlirRet(p, out, outsz)
return
if string.strcmp(ob, "unreachable") == 0:
_MlirUnreachable(out, outsz)
return
if string.strcmp(ob, "call") == 0:
_MlirCall(p, resname, out, outsz)
return
if string.strcmp(ob, "select") == 0:
_MlirSelect(p, resname, out, outsz)
return
if string.strcmp(ob, "phi") == 0:
if out is not None:
# Leading phis are skipped in _MlirEmitBlockLines; reaching here means phi is not at block head
MlirFail("MLIR render: phi is not at block head (v1 only supports leading consecutive phis -> block arguments)")
_MlirPhiScan(text)
return
if string.strcmp(ob, "getelementptr") == 0:
_MlirGep(p, resname, out, outsz)
return
if string.strcmp(ob, "bitcast") == 0:
_MlirBitcast(p, resname, out, outsz)
return
if _mlir_in_set(ob, "|sext|trunc|zext|ptrtoint|inttoptr|fpext|fptrunc|fptosi|sitofp|fptoui|uitofp|") != 0:
_MlirCast(p, resname, ob, out, outsz)
return
if string.strcmp(ob, "switch") == 0:
_MlirSwitch(p, out, outsz)
return
if string.strcmp(ob, "atomicrmw") == 0:
_MlirAtomicRmw(p, resname, out, outsz)
return
if string.strcmp(ob, "cmpxchg") == 0:
_MlirCmpXchg(p, resname, resname2, out, outsz)
return
if string.strcmp(ob, "va_arg") == 0:
_MlirVaArg(p, resname, out, outsz)
return
if string.strcmp(ob, "extractvalue") == 0:
# Real source: TPV's multi-result inline-asm result extraction (see _MlirExtractValue).
# Note: other unsupported opcodes like `insertvalue` never occur in the corpus and fall
# through to the loud fallback error ("unknown instruction opcode").
_MlirExtractValue(p, resname, out, outsz)
return
# Note: inline asm's opcode in intermediate text is "call" (call <ret> asm ...); _MlirCall
# recognizes it and forwards to _MlirAsm (llvm.inline_asm).
MlirFail("MLIR render: unknown instruction opcode")
# ============================================================
# Function signature and body
# ============================================================
def _MlirFuncSig(buf: str, size: sizet,
func: Function*, pool: memhub.MemBuddy*):
"""llvm.func @name(%a: i32, %b: i32) -> i32 (void return omits -> ty)"""
_append_cstr(buf, size, "llvm.func @")
if func.Name is not None:
if _ll_name_needs_quote(func.Name) != 0:
_append_cstr(buf, size, "\"")
_append_cstr(buf, size, func.Name)
_append_cstr(buf, size, "\"")
else:
_append_cstr(buf, size, func.Name)
_append_cstr(buf, size, "(")
params: GSList[Param]* = func.Params
cur: Param* = None
if params is not None:
cur = params.Head
first: int = 1
cur_steps: int = 0
while cur is not None:
cur_steps += 1
if cur_steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: parameter chain may be cyclic")
if first == 0:
_append_cstr(buf, size, ", ")
if cur.Name is not None:
if cur.Name[0] != '%':
_append_cstr(buf, size, "%")
_append_cstr(buf, size, cur.Name)
_append_cstr(buf, size, ": ")
ty_buf: str = _SC_Ty
ty_buf[0] = '\0'
TypePrint(ty_buf, 4096, cur.Ty, pool)
_append_cstr(buf, size, ty_buf)
cur = cur.Next
first = 0
# Variadic function: append `...` at the end of the parameter list (mlir-opt 22.1.8 measured exit 0:
# `llvm.func @f(%p: !llvm.ptr, ...) -> i32` / with no fixed params `llvm.func @f(...) -> i32`)
if func.IsVarArg == 1:
if params is not None and params.Head is not None:
_append_cstr(buf, size, ", ...")
else:
_append_cstr(buf, size, "...")
_append_cstr(buf, size, ")")
# Return type (Void omitted)
is_void: int = 0
match func.RetTy:
case LLVMType.Void():
is_void = 1
case _:
is_void = 0
if is_void == 0:
ret_buf: str = _SC_Ty2
ret_buf[0] = '\0'
TypePrint(ret_buf, 4096, func.RetTy, pool)
_append_cstr(buf, size, " -> ")
_append_cstr(buf, size, ret_buf)
def FunctionPrint(buf: str, size: sizet,
func: Function*, pool: memhub.MemBuddy*):
"""Serialize function func as MLIR (llvm dialect) text APPENDED to buf.
NOTE: like the llvmlite version, this appends (so the module printer can concatenate
functions); callers wanting single-function text must set buf[0]='\\0' themselves.
"""
global _MLIR_Pass, _MLIR_Func, _MLIR_SrcBlk, _MLIR_CurLine, _MlirOutTrunc
if func is None or buf is None or size == 0:
return
_MlirOutTrunc = 0
# Local peephole first (ret zeroinitializer->0/null, entry-slot store->load forwarding,
# no-op bitcast removal): peepholes match by name and delete lines by setting Buf=None.
# They act on the builders' intermediate instruction text and are orthogonal to MLIR rendering;
# RenumberSSA internally skips declare functions (IsDeclared==1).
# Run at most ONCE per body: the module is printed several times (STUB/TEXT/FULL, see PrintModule)
# and the T2 forwarding is NOT idempotent — a second pass over the already-optimized text can
# drop an entry alloca whose use was only reachable through a line the first pass had removed,
# leaving dangling SSA names (mlir-opt: "op using value defined outside the region").
if func.IsDeclared == 0 and G_PeepEnable != 0 and func.PeepDone == 0:
_peephole_opt(func, pool)
func.PeepDone = 1
RenumberSSA(func, pool)
_MlirTablesInit(pool)
_MLIR_Func = func
# Record the function's own SSA names of the form c0/c1/... so the constant namer never picks a
# colliding %c<seq> (a user param/local literally named c1 would clash with a materialized const).
_MlirCollectUsedCNames(func)
# Clear the previous function's line context: signature/declare and other non-line-driven
# positions must not carry an unrelated stale line, avoiding misleading diagnostics.
_MLIR_CurLine = None
MlirSetContextLine(None)
if func.IsDeclared == 1:
# LLVM intrinsic declarations are NOT emitted: on the MLIR side intrinsics are expressed
# via llvm.call_intrinsic (see _MlirCall). Emitting `llvm.func @llvm.va_start(!llvm.ptr)`
# would be down-leveled to an unmangled `declare void @llvm.va_start(ptr)`, and LLVM
# verification errors "Intrinsic name not mangled correctly for type arguments!".
# Rule: name starts with "llvm." and not "llvm.mlir.*" (the latter are MLIR-internal op names).
if func.Name is not None and string.strncmp(func.Name, "llvm.", 5) == 0 \
and string.strncmp(func.Name, "llvm.mlir.", 10) != 0:
return
# declare (no body)
_MlirFuncSig(buf, size, func, pool)
_append_cstr(buf, size, "\n")
_MlirOutTruncFail()
return
blocks: GSList[BasicBlock]* = func.Blocks
if blocks is None:
_MlirFuncSig(buf, size, func, pool)
_append_cstr(buf, size, "\n")
_MlirOutTruncFail()
return
# ---- pass1: scan (register constants/aliases + shape checks) ----
_MLIR_Pass = 1
blk: BasicBlock* = blocks.Head
blk_steps: int = 0
while blk is not None:
blk_steps += 1
if blk_steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: function block chain may be cyclic")
_MLIR_SrcBlk = blk
if blk.Lines is not None:
lines: GSList[Line]* = blk.Lines
ln: Line* = lines.Head
ln_steps: int = 0
while ln is not None:
ln_steps += 1
if ln_steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: block instruction chain may be cyclic")
if ln.Buf is not None:
_MlirRenderLine(ln.Buf, None, 0)
ln = ln.Next
blk = blk.Next
# ---- pass2: emit ----
_MLIR_Pass = 2
_MlirFuncSig(buf, size, func, pool)
_append_cstr(buf, size, " {\n")
blk = blocks.Head
first: int = 1
blk_steps = 0
while blk is not None:
blk_steps += 1
if blk_steps > IR_DUMP_CHAIN_LIMIT:
MlirFail("MLIR render: function block chain may be cyclic")
_MLIR_SrcBlk = blk
if first != 0:
# entry block has no label; constant-materialization ops go at the entry block head
# (entry dominates the function, so constants are visible everywhere without breaking the phi-at-head rule)
_MlirCheckEntryNoPhi(blk)
_MlirEmitConsts(buf, size)
else:
_MlirEmitBlockHeader(buf, size, blk)
_MlirEmitBlockLines(buf, size, blk)
blk = blk.Next
first = 0
_append_cstr(buf, size, "}\n")
_MlirOutTruncFail()
# ============================================================
# Internal helpers
# ============================================================
# _append_cstr write-position cache (perf: O(1) when appending into the same buffer,
# avoiding O(n^2) strlen(dst) rescans -- an IR-serialization hot spot).
# Falls back to strlen on buffer switch (different dst) or buffer reset (dst[0]=0 and pos>0).
_g_last_dst: uint64 = 0
_g_last_pos: sizet = 0
def _append_cstr(dst: str, dst_size: sizet, src: str):
"""Append C string src to the end of dst"""
global _g_last_dst, _g_last_pos, _MlirOutTrunc
if dst is None or src is None: return
dlen: sizet = 0
if _g_last_dst == uint64(dst):
dlen = _g_last_pos
# Buffer reset (caller reuses the same address after buf[0]=0) -> restart from head
if dlen > 0 and dst[0] == '\0':
dlen = 0
# Cache-consistency check: the cached end must still be the current text end (NUL at the
# end, non-NUL before it). If several modules write the same buf or pool reuse leaves old
# content, the cached pos goes stale and appends past the first NUL become invisible
# (LLvmLiteTest Test5/9 root cause); on failure fall back to strlen.
if dlen == 0:
if dst[0] != '\0':
dlen = string.strlen(dst)
elif dst[dlen] != '\0' or dst[dlen - 1] == '\0':
dlen = string.strlen(dst)
else:
dlen = string.strlen(dst)
_g_last_dst = uint64(dst)
# Unsigned-underflow guard: if dlen exceeds the buffer size (corrupted/dangling buffer),
# return; otherwise remain = dst_size - dlen underflows to a huge value and writes out of bounds.
if dlen >= dst_size:
_MlirOutTrunc = 1
return
slen: sizet = string.strlen(src)
remain: sizet = dst_size - dlen
if remain <= 0:
_MlirOutTrunc = 1
return
i: sizet = 0
while i < slen and i + 1 < remain:
dst[dlen + i] = src[i]
i += 1
if i < slen:
_MlirOutTrunc = 1
dst[dlen + i] = '\0'
_g_last_pos = dlen + i
def _MlirOutTruncFail():
"""Output-truncation check before printing finishes (truncation errors loudly, never emits partial MLIR silently)"""
global _MlirOutTrunc
if _MlirOutTrunc != 0:
_MlirOutTrunc = 0
MlirFail("MLIR render: output text truncated by buffer overflow (increase the dump_ir/FunctionPrint buffer)")
# ============================================================
# Debug call-site recording (--traceback only; zero overhead when off)
#
# The MLIR backend emits no DWARF (see README 3.3): the final exe has no .debug_line records
# for our .vp functions, so runtime stackmaps cannot map a caller frame's PC (= return address)
# back to a call-site line. Here, at BUILD time, each define (F record) and each DIRECT call
# (C record) is recorded into a text buffer; after each module is translated TransPyV writes it
# to a <sha1>.vpcall side file, and BuildPipeline.WriteStackMap combines it with
# `llvm-objdump -d` for "return address -> (file, line, function)" address normalization.
#
# Record format (fields separated by \t to avoid ambiguity from spaces/backslashes in Windows paths):
# F\t<function symbol>\t<definition line>\t<absolute file path>\n
# C\t<caller symbol>\t<callee symbol>\t<call-site line>\t<absolute file path>\n
#
# Function symbol = Function.Name (no @ prefix), matching objdump labels; the callee symbol comes
# from build_call's callee (also no @); LLVM intrinsics (llvm.*) never become named callq targets and are skipped.
# ============================================================
# Master switch: mirrors TransPyV's --traceback (relayed via DebugInfoGen.SetEnabled)
_g_dbg_on: int = 0
# Text buffer (stdlib-allocated, survives pool reset; cleared after DebugTakeText hands off ownership)
_g_dbg_buf: bytes = None
_g_dbg_len: sizet = 0
_g_dbg_cap: sizet = 0
# Current function context (C records pull caller symbol/file/line from here); nested defines
# (lambdas etc.) use a fixed-size Push/Pop stack.
define DBGC_STACK_MAX = 256
_g_dbg_func: str = None
_g_dbg_file: str = None
_g_dbg_defline: int = 0
_g_dbg_line: int = 0
_g_dbg_depth: int = 0
_g_dbg_stk_func: bytes = None # 8 bytes per slot (char*)
_g_dbg_stk_file: bytes = None # 8 bytes per slot (char*)
_g_dbg_stk_def: bytes = None # 8 bytes per slot (sizet)
_g_dbg_stk_ln: bytes = None # 8 bytes per slot (sizet)
def DebugSetEnabled(flag: bool) -> int:
"""Enable/disable debug call-site recording (relayed from DebugInfoGen.SetEnabled --traceback)."""
global _g_dbg_on
if flag:
_g_dbg_on = 1
else:
_g_dbg_on = 0
return 0
def DebugIsEnabled() -> int:
return _g_dbg_on
def _DbgReserve(extra: sizet) -> int:
"""Ensure the text buffer can hold at least extra more bytes (excluding the trailing NUL)."""
global _g_dbg_buf, _g_dbg_cap
if _g_dbg_buf is None:
cap0: sizet = 262144
b: bytes = stdlib.malloc(cap0)
if b is None:
return 1
b[0] = '\0'
_g_dbg_buf = b
_g_dbg_cap = cap0
need: sizet = _g_dbg_len + extra + 1
if need <= _g_dbg_cap:
return 0
ncap: sizet = _g_dbg_cap
while ncap < need:
ncap = ncap * 2
nb: bytes = stdlib.realloc(_g_dbg_buf, ncap)
if nb is None:
return 1
_g_dbg_buf = nb
_g_dbg_cap = ncap
return 0
def _DbgEnsureStack() -> int:
"""Lazily allocate the Push/Pop context stack (only when enabled and actual nesting occurs)."""
global _g_dbg_stk_func, _g_dbg_stk_file, _g_dbg_stk_def, _g_dbg_stk_ln
if _g_dbg_stk_func is not None:
return 0
b1: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8)
b2: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8)
b3: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8)
b4: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8)
if b1 is None or b2 is None or b3 is None or b4 is None:
return 1
_g_dbg_stk_func = b1
_g_dbg_stk_file = b2
_g_dbg_stk_def = b3
_g_dbg_stk_ln = b4
return 0
def DebugNoteFunc(sym: str, file: str, line: int) -> int:
"""Enter a define: push the outer context, set as current function, and write an F record."""
global _g_dbg_func, _g_dbg_file, _g_dbg_defline, _g_dbg_line, _g_dbg_depth, _g_dbg_len
if _g_dbg_on == 0:
return 0
if _g_dbg_depth >= DBGC_STACK_MAX:
MlirFail("MLIR debug record: function nesting depth exceeds limit (DBGC_STACK_MAX)")
if _DbgEnsureStack() != 0:
MlirFail("MLIR debug record: context stack allocation failed")
fs: str* = (str*)(uint64(_g_dbg_stk_func) + sizet(_g_dbg_depth) * 8)
fs[0] = _g_dbg_func
ff: str* = (str*)(uint64(_g_dbg_stk_file) + sizet(_g_dbg_depth) * 8)
ff[0] = _g_dbg_file
ds: sizet* = (sizet*)(uint64(_g_dbg_stk_def) + sizet(_g_dbg_depth) * 8)
ds[0] = sizet(_g_dbg_defline)
ls: sizet* = (sizet*)(uint64(_g_dbg_stk_ln) + sizet(_g_dbg_depth) * 8)
ls[0] = sizet(_g_dbg_line)
_g_dbg_depth += 1
_g_dbg_func = sym
_g_dbg_file = file
_g_dbg_defline = line
_g_dbg_line = line
if sym is None or file is None:
return 0
need: sizet = string.strlen(sym) + string.strlen(file) + 64
if _DbgReserve(need) != 0:
MlirFail("MLIR debug record: F record buffer allocation failed")
viperlib.snprintf(_g_dbg_buf + _g_dbg_len, _g_dbg_cap - _g_dbg_len,
"F\t%s\t%d\t%s\n", sym, line, file)
_g_dbg_len = _g_dbg_len + string.strlen(_g_dbg_buf + _g_dbg_len)
return 0
def DebugClearFunc() -> int:
"""Leave a define: restore the outer context (clear if none)."""
global _g_dbg_func, _g_dbg_file, _g_dbg_defline, _g_dbg_line, _g_dbg_depth
if _g_dbg_on == 0:
return 0
if _g_dbg_depth <= 0:
_g_dbg_func = None
_g_dbg_file = None
_g_dbg_defline = 0
_g_dbg_line = 0
return 0
_g_dbg_depth -= 1
fs: str* = (str*)(uint64(_g_dbg_stk_func) + sizet(_g_dbg_depth) * 8)
_g_dbg_func = fs[0]
ff: str* = (str*)(uint64(_g_dbg_stk_file) + sizet(_g_dbg_depth) * 8)
_g_dbg_file = ff[0]
ds: sizet* = (sizet*)(uint64(_g_dbg_stk_def) + sizet(_g_dbg_depth) * 8)
_g_dbg_defline = int(ds[0])
ls: sizet* = (sizet*)(uint64(_g_dbg_stk_ln) + sizet(_g_dbg_depth) * 8)
_g_dbg_line = int(ls[0])
return 0
def DebugNoteLine(line: int) -> int:
"""Record the current statement line (used by the following build_call to write a C record)."""
global _g_dbg_line
if _g_dbg_on == 0:
return 0
if line > 0:
_g_dbg_line = line
return 0
def DebugResetFunc() -> int:
"""Clear the current function context (module-start fallback: a mid-translation failure may leave an un-Popped context)."""
global _g_dbg_func, _g_dbg_file, _g_dbg_defline, _g_dbg_line, _g_dbg_depth
if _g_dbg_on == 0:
return 0
_g_dbg_func = None
_g_dbg_file = None
_g_dbg_defline = 0
_g_dbg_line = 0
_g_dbg_depth = 0
return 0
def _DebugNoteCall(callee: str) -> int:
"""Write a C record (caller = current function, line = current statement line)."""
global _g_dbg_len
if _g_dbg_on == 0:
return 0
if _g_dbg_func is None or _g_dbg_file is None or _g_dbg_line <= 0:
return 0
if callee is None or callee[0] == '\0':
return 0
if string.strncmp(callee, "llvm.", 5) == 0:
return 0
need: sizet = string.strlen(_g_dbg_func) + string.strlen(callee) \
+ string.strlen(_g_dbg_file) + 64
if _DbgReserve(need) != 0:
MlirFail("MLIR debug record: C record buffer allocation failed")
viperlib.snprintf(_g_dbg_buf + _g_dbg_len, _g_dbg_cap - _g_dbg_len,
"C\t%s\t%s\t%d\t%s\n",
_g_dbg_func, callee, _g_dbg_line, _g_dbg_file)
_g_dbg_len = _g_dbg_len + string.strlen(_g_dbg_buf + _g_dbg_len)
return 0
def DebugTakeText() -> str:
"""Take and clear the accumulated text (once per module); buffer ownership passes to the caller."""
global _g_dbg_buf, _g_dbg_len, _g_dbg_cap
if _g_dbg_buf is None or _g_dbg_len == 0:
if _g_dbg_buf is not None:
_g_dbg_len = 0
_g_dbg_buf[0] = '\0'
return None
out: str = _g_dbg_buf
_g_dbg_buf = None
_g_dbg_len = 0
_g_dbg_cap = 0
return out
# ============================================================
# Field accessors (let other modules bypass stub type limits)
#
# Cross-module, the user module only has stub types for Function/Param (missing fields, possibly
# wrong types), so TransPyC silently skips field access (no GEP, no error) and fields stay null.
# These accessors run inside this module where the full type definitions exist, so they can access
# all fields correctly; other modules get field values via function calls, bypassing the stub limits.
# ============================================================
def function_get_name(func: Function*) -> str:
"""Get the function name"""
if func is None:
return None
return func.Name
def function_get_name_len(func: Function*) -> sizet:
"""Get the function name length (computed once at creation, always == strlen(Name)).
Mirrors function_get_name's cross-module stub-bypass pattern: the user module only has a stub
Function type (missing fields, possibly wrong types), so reading Name/NameLen directly is
silently skipped. This accessor reads fields inside the full-type module and returns the length
via a call, avoiding repeated strlen on the same name (a program-wide strlen hot spot).
"""
if func is None:
return 0
return func.NameLen
def function_get_ret_ty(func: Function*) -> LLVMType*:
"""Get the function return type"""
if func is None:
return None
return func.RetTy
def function_get_params(func: Function*) -> GSList[Param]*:
"""Get the function parameter list container"""
if func is None:
return None
return func.Params
def function_get_param_head(func: Function*) -> Param*:
"""Get the function parameter list head (one step, avoiding GSList.Head access by callers)"""
if func is None:
return None
params: GSList[Param]* = func.Params
if params is None:
return None
return params.Head
def function_get_next(func: Function*) -> Function*:
"""Get the next function in the list"""
if func is None:
return None
return func.Next
def function_is_declared(func: Function*) -> int:
"""Get whether the function is a declaration (1=declare, 0=define)"""
if func is None:
return 0
return func.IsDeclared
def param_get_name(param: Param*) -> str:
"""Get the parameter name"""
if param is None:
return None
return param.Name
def param_get_ty(param: Param*) -> LLVMType*:
"""Get the parameter type"""
if param is None:
return None
return param.Ty
def param_get_next(param: Param*) -> Param*:
"""Get the next parameter in the list"""
if param is None:
return None
return param.Next