Files
Viper_VRTE/mlirlite/__function.vp
T
TermiNexus 1eb46faa41 fix(mlirlite): T2「store→load 转发」改为类型感知(跨类型 load 不再被按名替换)
现场(位拷贝早期实现写成 `load i64, ptr %xv`):
  697:16: error: use of value '%x' expects different type than prior uses: 'i64' vs 'f64'
    llvm.store %x, %0 : i64, !llvm.ptr
—— T2 转发是纯**按名字**替换的:把 `store double %x, ptr %xv` 的 %x 顶进了那条 i64 行。

- 扫描时同时记录 store 存值的**类型文本**(括号配平读)与每个 load 的**声明类型文本**
  (同样括号配平,读到第一个顶层 ',' 或空格);
- 仅当**所有 load 的声明类型都与存值类型一致**时才转发,否则整槽放弃(保守,IR 始终合法);
- 同类型槽(标量/指针/聚合的常见情形)转发不受影响:聚合两侧读法同口径,不会因文本截断白丢。
2026-10-15 20:18:59 +08:00

4097 lines
163 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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); rendered as MLIR passthrough = [...]
AttrGroup: str # string attr with a value, e.g. '"section"=".text.startup"'; rendered inline as `section = "..."` (None=none)
AttrGroupId: int # llvmlite #N group id (unused by MLIR printing; the attr is inlined instead)
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.
# => VPC'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
# - ViperC 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
# store 存值的**类型文本**:T2 只做"同类型转发"(见下方 Criteria 前的类型检查)
st_val_ty: str = None
st_ok: int = 0
st_count: int = 0
st_line_order: int = -1
load_ltoks: str = pool.alloc(8 * 64)
# 每个 load 的**声明类型文本**(与 load_ltoks 同索引的并行槽)
load_types: 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:
# store 行是 `store <val_ty> <val>, <ptr_ty> <ptr>`。值类型可能是聚合
# (如 `!llvm.struct<"n", (i32, i32)>`),其括号内含 ", ";直接 strstr(", ")
# 会在类型内部切断,把类型前缀当值转发出去(畸形 IR:`store <t> !llvm.struct<"n" , …`)。
# 先按括号配平跳过值类型,再取真正的值记号。
vp: str = sb + 6
if string.strncmp(vp, "volatile ", 9) == 0: vp = vp + 9
vty0: str = vp
vdepth: int = 0
while vp[0] != '\0':
vch: int = vp[0]
if vch == '<' or vch == '(' or vch == '[' or vch == '{':
vdepth += 1
elif vch == '>' or vch == ')' or vch == ']' or vch == '}':
if vdepth > 0: vdepth -= 1
elif vdepth == 0 and vch == ' ':
break
vp = vp + 1
# 存值类型文本 = [vty0, 该空格):T2 的类型检查基准
vty_len: sizet = sizet(uint64(vp) - uint64(vty0))
vty_buf: str = pool.alloc(vty_len + 1)
if vty_buf is not None:
string.strncpy(vty_buf, vty0, vty_len)
vty_buf[vty_len] = '\0'
st_val_ty = vty_buf
else: unsafe = 1
while vp[0] == ' ': vp = vp + 1
vcomma: str = string.strstr(vp, ", ")
if vcomma is not None:
vlen: sizet = sizet(uint64(vcomma) - uint64(vp))
if vlen > 0:
vbuf: str = pool.alloc(vlen + 1)
if vbuf is not None:
string.strncpy(vbuf, vp, 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 的**声明类型文本**(括号配平读到第一个顶层 ','),
# 与 load_ltoks 同索引:T2 只做同类型转发(类型不一致 ⇒ 放弃)。
# 配平口径与 store 侧读值类型一致,聚合类型两端文本才会相等。
lty0: str = string.strstr(sb, " = load ") + 8
lty_p2: str = lty0
lty_depth: int = 0
while lty_p2[0] != '\0':
lch: int = lty_p2[0]
if lch == '<' or lch == '(' or lch == '[' or lch == '{':
lty_depth += 1
elif lch == '>' or lch == ')' or lch == ']' or lch == '}':
if lty_depth > 0: lty_depth -= 1
elif lty_depth == 0 and (lch == ',' or lch == ' '):
break
lty_p2 = lty_p2 + 1
lty_len: sizet = sizet(uint64(lty_p2) - uint64(lty0))
lty_buf: str = pool.alloc(lty_len + 1)
if lty_buf is not None:
string.strncpy(lty_buf, lty0, lty_len)
lty_buf[lty_len] = '\0'
sloty: str* = (str*)(uint64(load_types) + load_cnt * 8)
sloty[0] = lty_buf
else: unsafe = 1
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
# T2 类型感知:仅当**每个 load 的声明类型都与 store 存值的类型一致**时才转发。
# 缺这条时,跨类型 load(例如位拷贝曾写成 `load i64, ptr %xv`)会被按名字替换成
# f64 的存值,产出非法 IR:
# use of value '%x' expects different type than prior uses: 'i64' vs 'f64'
# 类型不一致 ⇒ 整个槽放弃转发(保守,IR 始终合法;只是少做一次转发优化)。
if unsafe == 0:
if st_val_ty is None or load_types is None: unsafe = 1
else:
lty_i: int = 0
while lty_i < load_cnt:
lty_p: str* = (str*)(uint64(load_types) + lty_i * 8)
lty_s: str = lty_p[0]
if lty_s is None or string.strcmp(lty_s, st_val_ty) != 0: unsafe = 1
lty_i += 1
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
_SC_Attr: 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, _SC_Attr, _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)
_SC_Attr = pool.alloc(128)
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:
# 报文带上 (MLIR 类型, 常量文本, 实际位数):仅说"宽度不匹配"无法定位是哪条常量
# (此前现场只能靠二分源码找);与 _MlirConstFail 同风格。
vlen: sizet = string.strlen(val)
tlen: sizet = string.strlen(mt)
dbg: str = _MLIR_Pool.alloc(vlen + tlen + 128)
if dbg is not None:
viperlib.snprintf(dbg, vlen + tlen + 128, "MLIR render: float bit-pattern width mismatches type (type '%s' needs %d hex digits, value '%s' has %d)", mt, want, val, nd)
MlirFail(dbg)
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 "...", "..."()`
VPC 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: VPC'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 (VPC'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: VPC'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: VPC'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)
# Function attributes: MLIR carries them as an `attributes { ... }` clause right after the
# signature (see _MlirFuncAttrs). Placed here so both define and declare lines get them.
_MlirFuncAttrs(buf, size, func)
# ============================================================
# Function attribute rendering
# ============================================================
# MLIR (llvm dialect) expresses LLVM function attributes as an `attributes { ... }` clause on
# `llvm.func`. Measured against mlir-opt 22.1.8 + mlir-translate --mlir-to-llvmir:
# - keyword / quoted-keyword attrs (noinline / "stackrealign") -> `passthrough = ["<name>", ...]`
# (a token wrapped in LLVM quotes is a string attr; the surrounding quotes are stripped once)
# - a string attr with a value (section) -> `<name> = "<value>"`
# e.g. `llvm.func @f() attributes {section = ".text.startup", passthrough = ["noinline", "stackrealign"]}`
# lowers to `define ... #0 section ".text.startup"` + `attributes #0 = { noinline "stackrealign" }`.
def _MlirFuncAttrs(buf: str, size: sizet, func: Function*):
"""Append ` attributes { ... }` for func's function attrs, or nothing when none are set."""
has_attrs: int = 0
if func.Attrs is not None and func.Attrs[0] != '\0': has_attrs = 1
has_section: int = 0
if func.AttrGroup is not None: has_section = 1
if has_attrs == 0 and has_section == 0: return
_append_cstr(buf, size, " attributes {")
wrote: int = 0
if has_section != 0:
wrote = _MlirSectionAttr(buf, size, func.AttrGroup)
if has_attrs != 0:
if wrote != 0: _append_cstr(buf, size, ", ")
_append_cstr(buf, size, "passthrough = [")
_MlirPassthroughList(buf, size, func.Attrs)
_append_cstr(buf, size, "]")
_append_cstr(buf, size, "}")
def _MlirSectionAttr(buf: str, size: sizet, group: str) -> int:
"""Render the string attr group `"section"=".text.startup"` as MLIR `section = ".text.startup"`.
Returns 1 when an entry was written, 0 on an unrecognized/oversized group (caller skips it).
"""
if group is None: return 0
sep: str = string.strstr(group, "\"=\"")
if sep is None: return 0
# name = text between the leading quote and the separator's quote (e.g. section)
name_len: sizet = uint64(sep) - uint64(group) - 1
if name_len == 0 or name_len >= 120: return 0
name_buf: str = _SC_Attr
if name_buf is None: return 0
string.strncpy(name_buf, group + 1, name_len)
name_buf[name_len] = '\0'
_append_cstr(buf, size, name_buf)
_append_cstr(buf, size, " = ")
# value already carries its surrounding quotes: sep points at the '"' of '"="', so sep+2 is '"value"'
_append_cstr(buf, size, sep + 2)
return 1
def _MlirPassthroughList(buf: str, size: sizet, attrs: str):
"""Emit MLIR passthrough entries `"a", "b"` from a space-separated list of LLVM attr tokens."""
if attrs is None: return
n: sizet = string.strlen(attrs)
i: sizet = 0
first: int = 1
tok: str = _SC_Attr
if tok is None: return
while i < n:
while i < n and attrs[i] == ' ': i += 1
if i >= n: break
start: sizet = i
while i < n and attrs[i] != ' ': i += 1
end: sizet = i
# LLVM string attr: strip one surrounding pair of double quotes ("stackrealign" -> stackrealign)
if end - start >= 2 and attrs[start] == '"' and attrs[end - 1] == '"':
start += 1
end -= 1
k: sizet = 0
while start < end and k < 120:
tok[k] = attrs[start]
k += 1
start += 1
tok[k] = '\0'
if first == 0: _append_cstr(buf, size, ", ")
_append_cstr(buf, size, "\"")
_append_cstr(buf, size, tok)
_append_cstr(buf, size, "\"")
first = 0
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 ViperC 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 ViperC'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