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). """ # Pass 1: size the output from the actual replacements. A renumbered token can grow from # "%0" (2 bytes) to "%4294967295" (11 bytes), so the old src_len*2 guess was too small and # the copy loop stopped mid-line, leaving a truncated instruction behind. out_len: sizet = 0 scan: str = text while scan is not None and scan[0] != '\0': if scan[0] == '%': nxt0: str = scan + 1 if nxt0 is not None and _is_digit_ch(nxt0[0]) != 0: num0: int = string.atoi(nxt0) if num0 >= 0 and num0 <= max_n and _map_get_i32(map_arr, num0) != -1: out_len = out_len + 16 scan = scan + 1 while scan is not None and _is_digit_ch(scan[0]) != 0: scan = scan + 1 continue out_len = out_len + 1 scan = scan + 1 new_size: sizet = out_len + 1 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 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: # Pass 1 reserved 16 bytes for this token. if dst_idx + 16 > new_size: MlirFail("MLIR render: SSA renumbering overflowed its output buffer (pass-1 sizing is inconsistent)") # 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': if dst_idx + 1 >= new_size: MlirFail("MLIR render: SSA renumbering overflowed its output buffer (pass-1 sizing is inconsistent)") 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 if dst_idx + 1 >= new_size: MlirFail("MLIR render: SSA renumbering overflowed its output buffer (pass-1 sizing is inconsistent)") 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 zeroinitializer -> ret 0; ret 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 % 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 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 , `。值类型可能是聚合 # (如 `!llvm.struct<"n", (i32, i32)>`),其括号内含 ", ";直接 strstr(", ") # 会在类型内部切断,把类型前缀当值转发出去(畸形 IR:`store !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 %x to , 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): 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 (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) 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) # Scratch capacities are shared by the parse readers: a 512-byte slot truncated long type # texts (a type can reach _TY_TEXT_CAP = 2048) and long symbol names. All slots below are # 4096 except the opcode slot (opcodes are short), so readers can use one capacity. _SC_Tb = pool.alloc(4096) _SC_Ty = pool.alloc(4096) _SC_Ty2 = pool.alloc(4096) _SC_V1 = pool.alloc(4096) _SC_V2 = pool.alloc(4096) _SC_V3 = pool.alloc(4096) _SC_Callee = pool.alloc(4096) _SC_Args = pool.alloc(4096) _SC_ArgTs = pool.alloc(8192) _SC_IdxV = pool.alloc(4096) _SC_IdxT = pool.alloc(4096) _SC_Mt = pool.alloc(4096) _SC_Tmp = pool.alloc(8192) _SC_Tmp2 = pool.alloc(4096) _SC_P1 = pool.alloc(4096) _SC_P2 = pool.alloc(4096) _SC_B1 = pool.alloc(4096) _SC_B2 = pool.alloc(4096) _SC_Buf = pool.alloc(4096) _SC_Attr = pool.alloc(4096) if _SC_Op is None or _SC_Ty is None or _SC_Mt is None or _SC_Tmp is None: MlirFail("MLIR render: parse scratch allocation failed") _SC_Op[0] = '\0' _SC_Ty[0] = '\0' _SC_Mt[0] = '\0' _SC_Tmp[0] = '\0' _MlirTablesPool = pool _MlirTablesValid = 1 # ============================================================ # Constant table / alias table # ============================================================ def _MlirAliasLookup(name: str) -> str: """Look up the alias table; returns None on miss""" if name is None: return None for i in range(int(_MLIR_AliasCount)): sp: str* = _mlir_slotp(_MLIR_AliasFrom, int(i)) if sp[0] is not None and string.strcmp(sp[0], name) == 0: st: str* = _mlir_slotp(_MLIR_AliasTo, int(i)) return st[0] return None def _MlirAliasGrow(): """扩容 alias 表:池内分配新块并拷贝旧内容,容量翻倍(旧块留在池里,不回收)。 原实现把上限写死为 MLIR_ALIAS_CAP(512),超出直接 MlirFail 中断整次编译—— 这是资源限制,不该让编译失败。大函数里 no-op bitcast(别名)条数会超过它。 """ global _MLIR_AliasFrom, _MLIR_AliasTo, _MLIR_AliasCap old_cap: int = _MLIR_AliasCap nb_from: bytes = _MLIR_Pool.alloc(old_cap * 16) nb_to: bytes = _MLIR_Pool.alloc(old_cap * 16) if nb_from is None or nb_to is None: MlirFail("MLIR render: alias table growth allocation failed") string.memcpy(nb_from, _MLIR_AliasFrom, old_cap * 8) string.memcpy(nb_to, _MLIR_AliasTo, old_cap * 8) string.memset(nb_from + old_cap * 8, 0, old_cap * 8) string.memset(nb_to + old_cap * 8, 0, old_cap * 8) _MLIR_AliasFrom = nb_from _MLIR_AliasTo = nb_to _MLIR_AliasCap = old_cap * 2 def _MlirConstGrow(): """扩容常量表 + 自动常量名空间(%c)+ 已用名表:池内分配翻倍新块并拷贝旧内容。 原实现三处都钉死在 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. 自动常量名 %c 的编号空间会按需扩容(见 _MlirConstGrow),所以这里记录**任意** c 后缀:越界的编号先扩容已用名表再置位,否则扩出来的编号可能与该函数自带的 c 撞名。 """ 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 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(名字空间会按需扩容, # 不能再假设"更大的后缀不会与自动名冲突") 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 in func (params + every intermediate line). The constant materializer auto-names constants %c; 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: 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 _MlirReadOverflow(): """A value/type token did not fit the caller's read buffer: fail instead of truncating. Truncation here stays syntactically plausible (a cut symbol name yields IR that calls a nonexistent symbol; a cut type yields malformed MLIR far from the real cause), so it must be loud. """ MlirFail("MLIR render: a value/type token is longer than its read buffer and would be truncated (raise the scratch buffer capacity for this token)") 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: _MlirReadOverflow() out[n] = p[0] n += 1 p = p + 1 if p[0] == '"': if n + 1 >= outsz: _MlirReadOverflow() out[n] = '"' n += 1 p = p + 1 while p[0] != '\0' and p[0] != '"': if n + 1 >= outsz: _MlirReadOverflow() out[n] = p[0] n += 1 p = p + 1 if p[0] == '"': if n + 1 >= outsz: _MlirReadOverflow() 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: _MlirReadOverflow() 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: _MlirReadOverflow() 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: _MlirReadOverflow() out[n] = char(ch) n += 1 p = p + 1 while p[0] == '*': if n + 1 >= outsz: _MlirReadOverflow() 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: _MlirReadOverflow() 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: _MlirReadOverflow() 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: _MlirReadOverflow() 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: _MlirReadOverflow() out[n] = char(ch2) n += 1 p = p + 1 d2 -= 1 if d2 <= 0: break continue if n + 1 >= outsz: _MlirReadOverflow() out[n] = char(ch2) n += 1 p = p + 1 while p[0] == '*': if n + 1 >= outsz: _MlirReadOverflow() 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, 4096) 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, 4096) 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, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) bb: str = _SC_P2 p = _mlir_read_value(p, bb, 4096) 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, 4096) 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, 4096) 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, 4096) 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, 4096) 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, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) bb: str = _SC_P2 p = _mlir_read_value(p, bb, 4096) 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. %a, %b : ty""" tyb: str = _SC_Ty p = _mlir_read_type(p, tyb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) lb: str = _SC_V1 p = _mlir_read_value(p, lb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) rb: str = _SC_V2 _mlir_read_value(p, rb, 4096) _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, 4096) vb: str = _SC_V1 _mlir_read_value(p, vb, 4096) _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 , ` -- 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, 4096) p = _mlir_ws(p) tyb: str = _SC_Ty p = _mlir_read_type(p, tyb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) lb: str = _SC_V1 p = _mlir_read_value(p, lb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) rb: str = _SC_V2 _mlir_read_value(p, rb, 4096) _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, 4096) 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, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ptyb: str = _SC_Ty2 p = _mlir_read_type(p, ptyb, 4096) p = _mlir_ws(p) pb: str = _SC_V1 _mlir_read_value(p, pb, 4096) _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, 4096) p = _mlir_ws(p) vb: str = _SC_V1 p = _mlir_read_value(p, vb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ptyb: str = _SC_Ty2 p = _mlir_read_type(p, ptyb, 4096) p = _mlir_ws(p) pb: str = _SC_V2 _mlir_read_value(p, pb, 4096) _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: ` , label %a, label %b` _MlirCondBr(p, out, outsz) return p = _mlir_ws(p + 6) tb: str = _SC_V1 _mlir_read_value(p, tb, 4096) 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, 4096) p = _mlir_ws(p) cb: str = _SC_V1 p = _mlir_read_value(p, cb, 4096) 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, 4096) 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, 4096) _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, 4096) vb: str = _SC_V1 _mlir_read_value(p, vb, 4096) _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 (...) @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 asm [sideeffect] # [inteldialect] "", ""( %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 (, ...) @f()` # (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) # : (!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, 4096) 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, 4096) 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, 4096) p = _mlir_ws(p) avb: str = _SC_V1 p = _mlir_read_value(p, avb, 4096) 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 asm sideeffect "...", "..."()` VPC CallGenCBuiltin injected -> `call asm sideeffect inteldialect "...", "..."( %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 => `: () -> ()`; 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, 4096) p = _mlir_ws(p) avb: str = _SC_V1 p = _mlir_read_value(p, avb, 4096) 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, 4096) p = _mlir_ws(p) vb: str = _SC_V1 p = _mlir_read_value(p, vb, 4096) 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, 4096) _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, 4096) p = _mlir_ws(p) vb: str = _SC_V1 p = _mlir_read_value(p, vb, 4096) 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, 4096) 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, 4096) p = _mlir_ws(p) cb: str = _SC_V1 p = _mlir_read_value(p, cb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ttyb: str = _SC_Ty2 p = _mlir_read_type(p, ttyb, 4096) p = _mlir_ws(p) tb: str = _SC_V2 p = _mlir_read_value(p, tb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ftyb: str = _SC_Buf p = _mlir_read_type(p, ftyb, 4096) p = _mlir_ws(p) fb: str = _SC_V3 _mlir_read_value(p, fb, 4096) _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, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ptyb: str = _SC_Buf p = _mlir_read_type(p, ptyb, 4096) p = _mlir_ws(p) pb: str = _SC_V3 p = _mlir_read_value(p, pb, 4096) 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, 4096) p = _mlir_ws(p) ivb: str = _SC_V1 p = _mlir_read_value(p, ivb, 4096) # 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, 4096) p = _mlir_ws(p) vb: str = _SC_V1 p = _mlir_read_value(p, vb, 4096) 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, 4096) 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, 4096) p = _mlir_ws(p) cvb: str = _SC_P2 p = _mlir_read_value(p, cvb, 4096) 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, 4096) 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, 4096) p = _mlir_ws(p) ptyb: str = _SC_Ty p = _mlir_read_type(p, ptyb, 4096) p = _mlir_ws(p) pb: str = _SC_V1 p = _mlir_read_value(p, pb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) vtyb: str = _SC_Ty2 p = _mlir_read_type(p, vtyb, 4096) p = _mlir_ws(p) vb: str = _SC_V2 p = _mlir_read_value(p, vb, 4096) p = _mlir_ws(p) ordb: str = _SC_P2 _mlir_read_value(p, ordb, 4096) _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 , , %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, 4096) p = _mlir_ws(p) pb: str = _SC_V1 p = _mlir_read_value(p, pb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ctyb: str = _SC_Ty2 p = _mlir_read_type(p, ctyb, 4096) p = _mlir_ws(p) cb: str = _SC_V2 p = _mlir_read_value(p, cb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) ntyb: str = _SC_B1 p = _mlir_read_type(p, ntyb, 4096) p = _mlir_ws(p) nb: str = _SC_B2 p = _mlir_read_value(p, nb, 4096) p = _mlir_ws(p) succ: str = _SC_P1 p = _mlir_read_value(p, succ, 4096) p = _mlir_ws(p) fail: str = _SC_P2 _mlir_read_value(p, fail, 4096) 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 , 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, 4096) p = _mlir_ws(p) vb: str = _SC_V1 p = _mlir_read_value(p, vb, 4096) p = _mlir_ws(p) if p[0] == ',': p = _mlir_ws(p + 1) atyb: str = _SC_Ty2 _mlir_read_type(p, atyb, 4096) 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] : 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 `, 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, 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, 4096) p = _mlir_ws(p) vb: str = _SC_V1 p = _mlir_read_value(p, vb, 4096) idxs: str = _SC_IdxV idxs[0] = '\0' nidx: int = 0 g: int = 0 # Index list: comma-separated in LLVM syntax (`, 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, 4096) 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, 4096) 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 = ` (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, 4096) 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 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 = ["", ...]` # (a token wrapped in LLVM quotes is a string attr; the surrounding quotes are stripped once) # - a string attr with a value (section) -> ` = ""` # 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 (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 .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\t\t\n # C\t\t\t\t\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