Files
TermiNexus 1b21d58be3 fix(mlirlite): 消除 IR 文本缓冲与解析器的静默截断
构建器:_append_cstr 装不下时改调 MlirFail(原先静默截断会让 switch 少 case、asm 半截、call 少参数);新增 _line_cap_for 按实际片段长度分配行缓冲,覆盖 load/store、12 种转换指令、gep/gep_array/gep_struct、phi、switch、select、atomicrmw、cmpxchg、va_arg、inline asm;build_call 的 callee 不再按 128 截断,参数槽从 32 放宽到 64。解析器:_mlir_read_value/_mlir_read_type 超限即 MlirFail,不再悄悄少写;所有值/类型/符号 scratch 由 512 提到 4096,调用点容量同步;_replace_ssa_numbers 改为两遍精确分配(原 2x 估算会半行截断);全局初始化器的聚合 token 与 addressof 符号名同样改为 4096 + fail-loud;TypePrint 的 "..." 占位现在会置 _MlirTypeTrunc,由 TypePrint 统一报错。
2026-10-17 17:57:40 +08:00

1698 lines
72 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from stdint import *
import string
import stdio
import stdlib
import viperlib
import memhub
from linkedlist import GSList, GSListNode
from .__types import (
LLVMType, TypePrint, Array, PrintStructDefinition, get_struct_name, Struct,
ParamNode, MlirFail, MlirTypeText, MlirSetContextLine,
MlirNamedTypesBegin, MlirNamedTypesClear, MlirNamedTypeRegister,
paramnode_get_ty, paramnode_get_next,
)
from .__function import (
Function, FunctionPrint, _ll_name_needs_quote,
_mlir_read_type, _mlir_read_value, _mlir_ws,
_MlirSymRefAppend, MlirTablesReset,
)
# GlobalVariable: LLVM IR global variable holding name, type, initializer, constant flag
# and linkage; strong-typed Next via GSListNode[GlobalVariable].
novirtual class GlobalVariable(GSListNode[GlobalVariable]):
Name: str # Global name (e.g. ".str" / "my_global")
Ty: LLVMType* # Variable type
Initializer: rawptr # Initializer Value* (None = no initializer)
IsConstant: int # 1=constant, 0=variable
Linkage: str # Linkage ("private"/"internal"/"external"/...)
Section: str # Section name (None = default)
UnnamedAddr: int # 1=unnamed_addr, 2=local_unnamed_addr, 0=none
Align: int # Alignment in bytes (0 = default; e.g. @c.Attribute(t.attr.aligned(16)))
# NamedTypeNode: named-struct type list node holding an LLVMType* (must be a named Struct);
# strong-typed Next via GSListNode[NamedTypeNode].
novirtual class NamedTypeNode(GSListNode[NamedTypeNode]):
Ty: LLVMType* # Named-struct type pointer
IsPacked: int # 1 = packed struct (MLIR !llvm.struct<"name", packed (…)>; no field padding)
# DINode: DI metadata node (model kept in sync; ignored at MLIR print time). Debug Info has no
# MLIR-backend equivalent, so this only records the model (keeping DebugInfoGen compiling) and
# LLVMModulePrint never emits these nodes (emitting them would break the MLIR syntax).
novirtual class DINode(GSListNode[DINode]):
Text: str # node body, e.g. "!DIFile(filename: \"a.vp\", directory: \"/x\")"
# DINamedMeta: named metadata (kept in sync; ignored at MLIR print time)
novirtual class DINamedMeta(GSListNode[DINamedMeta]):
Name: str # e.g. "llvm.dbg.cu" / "llvm.module.flags"
Refs: str # reference list text, e.g. "!1"
# LLVMModule: LLVM IR module container. Uses embedded Head/Tail/Count fields instead of a GSList
# pointer, avoiding the generic GSList's __sizeof__==0 and vtable-offset mismatch. LLVMModulePrint
# emits the full .ll content; the class must be LLVMModule (not Module) to avoid clashing with ast.Module.
novirtual class LLVMModule:
Name: str # Module name
FuncHead: Function* # Function list head
FuncTail: Function* # Function list tail
FuncCount: sizet # Function count
GlobalHead: GlobalVariable* # Global list head
GlobalTail: GlobalVariable* # Global list tail
GlobalCount: sizet # Global count
TargetTriple: str # Target triple (e.g. "x86_64-pc-linux-gnu")
DataLayout: str # Data layout (e.g. "e-m:e-p270:32:32-...")
NamedTypeHead: NamedTypeNode* # Named-struct type list head
NamedTypeTail: NamedTypeNode* # Named-struct type list tail
NamedTypeCount: sizet # Named-struct type count
DIMetaHead: DINode* # DI metadata node list head (kept in sync; not printed by MLIR)
DIMetaTail: DINode* # DI metadata node list tail
DIMetaCount: sizet # DI node count (= next !N id)
DINamedHead: DINamedMeta* # named metadata list head
DINamedTail: DINamedMeta* # named metadata list tail
DINamedCount: sizet # named metadata count
# Total: 18 fields * 8 = 144 bytes (no vtable; same layout as includes/llvmlite)
# Instance methods (objectified: the old module_* top-level functions became methods,
# kept as one-line delegates).
# ---- List operations ----
def add_function(self, func: Function*):
"""Append a function to the module's function list (O(1))."""
if self is None or func is None: return
func.Next = None
if self.FuncHead is None:
self.FuncHead = func
else:
self.FuncTail.Next = func
self.FuncTail = func
self.FuncCount += 1
def add_global(self, gv: GlobalVariable*):
"""Append a global to the module's global list (O(1))."""
if self is None or gv is None: return
gv.Next = None
if self.GlobalHead is None:
self.GlobalHead = gv
else:
self.GlobalTail.Next = gv
self.GlobalTail = gv
self.GlobalCount += 1
# ---- Target attributes ----
def set_target(self, triple: str):
"""Set the target triple."""
if self is None: return
self.TargetTriple = triple
def set_datalayout(self, layout: str):
"""Set the data layout."""
if self is None: return
self.DataLayout = layout
# ---- DI debug metadata (kept in sync; ignored at MLIR print time, model only) ----
def add_di_node(self, pool: memhub.MemBuddy*, text: str) -> int:
"""Append a DI metadata node; return its !N id (0-based, unique), -1 on failure.
text is the node body incl. !M references built from earlier ids. MLIR has no DI
concept, so the node is model-only (LLVMModulePrint does not emit it), keeping
DebugInfoGen compiling without affecting MLIR output.
"""
if self is None or pool is None or text is None:
return -1
node: DINode* = pool.alloc(DI_NODE_SIZE)
if node is None:
return -1
string.memset(node, 0, DI_NODE_SIZE)
# copy body (caller buffer may be reused)
tlen: sizet = string.strlen(text)
tcopy: str = pool.alloc(tlen + 1)
if tcopy is None:
return -1
string.strcpy(tcopy, text)
node.Text = tcopy
if self.DIMetaHead is None:
self.DIMetaHead = node
else:
self.DIMetaTail.Next = node
self.DIMetaTail = node
node_id: int = int(self.DIMetaCount)
self.DIMetaCount += 1
return node_id
def add_named_metadata(self, pool: memhub.MemBuddy*, name: str, refs: str):
"""Register named metadata "!<name> = !{ <refs> }"; merge into an existing
entry with the same name (refs is the brace body, e.g. "!3").
MLIR side keeps the model only, no output (LLVMModulePrint does not emit DI).
"""
if self is None or pool is None or name is None:
return
cur: DINamedMeta* = self.DINamedHead
cur_steps: sizet = 0
while cur is not None:
cur_steps += 1
if cur_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: module di named (DINamedCount=%zu, dump aborted)\n",
self.DINamedCount)
stdlib.exit(1)
if cur.Name is not None and string.strcmp(cur.Name, name) == 0:
if refs is not None:
old_len: sizet = 0
if cur.Refs is not None:
old_len = string.strlen(cur.Refs)
merged: str = pool.alloc(old_len + string.strlen(refs) + 3)
if merged is not None:
merged[0] = '\0'
if cur.Refs is not None:
string.strcat(merged, cur.Refs)
string.strcat(merged, ", ")
string.strcat(merged, refs)
cur.Refs = merged
return
cur = cur.Next
entry: DINamedMeta* = pool.alloc(DI_NAMED_SIZE)
if entry is None:
return
string.memset(entry, 0, DI_NAMED_SIZE)
ncopy: str = pool.alloc(string.strlen(name) + 1)
if ncopy is None:
return
string.strcpy(ncopy, name)
entry.Name = ncopy
if refs is not None:
rcopy: str = pool.alloc(string.strlen(refs) + 1)
if rcopy is None:
return
string.strcpy(rcopy, refs)
entry.Refs = rcopy
if self.DINamedHead is None:
self.DINamedHead = entry
else:
self.DINamedTail.Next = entry
self.DINamedTail = entry
self.DINamedCount += 1
# ---- Named-struct type management ----
def add_named_type(self, pool: memhub.MemBuddy*, ty: LLVMType*):
"""Register a named struct type in the module (MLIR keeps the model only, no output).
Non-named structs (None Name or non-Struct) are ignored. Dedup by Name; a full definition
replaces an opaque declaration of the same name (for cross-module value-type alloca).
MLIR has no standalone type definition: struct types are inlined at the use site, so this
emits nothing and NamedTypeHead is model-only, used by the existing APIs.
"""
if self is None or pool is None or ty is None:
return
name: str = get_struct_name(ty)
if name is None:
return
# Dedup check (a full definition may replace an opaque declaration)
cur: NamedTypeNode* = self.NamedTypeHead
cnt_dedup: int = 0
while cur is not None:
# Step guard: a cyclic NamedTypeHead list (corruption/insert bug) would hang the
# traversal forever. Normal lists are far below this cap; exceeding it means the
# list is corrupt → report and terminate immediately.
cnt_dedup += 1
if cnt_dedup > 100000:
stdio.printf("[ANT-LOOP] NamedTypeHead list suspected cycle, name=%s, visited nodes=%d, forced abort\n",
name if name is not None else "(null)", cnt_dedup)
stdlib.exit(1)
if cur.Ty is not None:
existing_name: str = get_struct_name(cur.Ty)
if existing_name is not None:
if string.strcmp(existing_name, name) == 0:
# Already a full definition: no replacement needed
if _is_opaque_struct(cur.Ty) == 0:
return
# Existing is opaque and the new type is opaque too: skip
if _is_opaque_struct(ty) != 0:
return
# Replace the opaque declaration with the full definition (patch Ty in place)
cur.Ty = ty
return
cur = cur.Next
# Create a new node
node: NamedTypeNode* = pool.alloc(NAMED_TYPE_NODE_SIZE)
if node is None:
return
string.memset(node, 0, NAMED_TYPE_NODE_SIZE)
node.Ty = ty
node.IsPacked = 0
# Insert opaque types at the head so they are declared before definitions that reference
# them (e.g. HashTable's opaque decl must precede TypeRegistry, or llc reports
# "use of undefined type"); append full definitions at the tail to keep their order.
if _is_opaque_struct(ty) != 0:
node.Next = self.NamedTypeHead
if self.NamedTypeHead is None:
self.NamedTypeTail = node
self.NamedTypeHead = node
else:
node.Next = None
if self.NamedTypeHead is None:
self.NamedTypeHead = node
else:
self.NamedTypeTail.Next = node
self.NamedTypeTail = node
self.NamedTypeCount += 1
def _is_type_defined_in_module(self, name: str) -> int:
"""Return 1 if the module already defines a struct with this name, else 0."""
if self is None or name is None:
return 0
cur: NamedTypeNode* = self.NamedTypeHead
cnt_tdm: int = 0
while cur is not None:
# Step guard: as above, prevent infinite traversal of a cyclic NamedTypeHead
cnt_tdm += 1
if cnt_tdm > 100000:
stdio.printf("[TDM-LOOP] NamedTypeHead list suspected cycle, name=%s, forced abort\n",
name if name is not None else "(null)")
stdlib.exit(1)
if cur.Ty is not None:
existing_name: str = get_struct_name(cur.Ty)
if existing_name is not None:
# Fast path: strcmp equality implies equal first chars, so skip strcmp when they differ.
if existing_name[0] == name[0] and string.strcmp(existing_name, name) == 0:
return 1
cur = cur.Next
return 0
def ensure_opaque_for_type(self, pool: memhub.MemBuddy*,
ty: LLVMType*):
"""Add opaque declarations for cross-module struct references in a type.
When the type references Ptr(Struct(name)) with a '.' in name (SHA1-qualified) and the
name is not defined in the module, add an opaque declaration.
"""
_scan_type_for_cross_module_refs(pool, self, ty)
# ---- Printing ----
def PrintModule(self, buf: str, size: sizet,
pool: memhub.MemBuddy*, mode: int):
"""Serialize the module to MLIR (llvm dialect) text into buf.
Skeleton: module attributes {llvm.target_triple, llvm.data_layout} { <globals> <functions> }.
MLIR has no stub/text split, so a comment marks the mode: OUTPUT_FULL → all; OUTPUT_STUB →
global/function declarations only; OUTPUT_TEXT → body-bearing functions only. target triple /
datalayout ride on the MLIR module attributes so mlir-translate emits them into the LLVM IR
header (see PrintModule body).
"""
if self is None or buf is None or size == 0: return
# Clear the line context: the global section is not line-driven, so a type error here
# must not carry a stale line from a previous function (_MlirCtxLine).
MlirSetContextLine(None)
# Publish this module's named-struct registry: name-only type text (%"name") resolves
# through it to the full definition, expanded inline (!llvm.struct<"name", (…)>). The
# table is rebuilt per print and cleared at the end, so its pointers are valid only then.
_MlirPublishNamedTypes(self, pool)
# Invalidate the region-render scratch: it is rebuilt lazily for the current pool on
# first use (see _MlirGvEnsureScratch) and never reused across prints (stale pool).
MlirGvResetScratch()
# Invalidate the parse-time tables/scratch likewise: they are allocated lazily by the first
# FunctionPrint and reused by the rest of this print, but a print after a pool reset_to must
# reallocate (a pointer into a reclaimed window would dangle).
MlirTablesReset()
# Module header: target triple / data layout ride on the MLIR module as LLVM-dialect
# attributes. mlir-translate --mlir-to-llvmir maps `llvm.target_triple` / `llvm.data_layout`
# onto the emitted LLVM IR's `target triple` / `target datalayout` header. They MUST live in
# the module rather than being left to the mlir-translate/llc stage: llc has no -mtriple in
# project.vpj, and — decisive — `opt -passes=asan` selects the ASan shadow scheme from the
# MODULE triple. An empty triple makes it use the non-Windows fixed shadow offset (1<<44)
# instead of the dynamic shadow base (__asan_shadow_memory_dynamic_address) that the linked
# Windows ASan runtime maps, so the first instrumented access (e.g. the injected
# __traceback_install_ctor at startup) faults on an unmapped shadow page.
if self.TargetTriple is not None:
_append_cstr(buf, size, "module attributes {llvm.target_triple = \"")
_append_cstr(buf, size, self.TargetTriple)
_append_cstr(buf, size, "\"")
if self.DataLayout is not None:
_append_cstr(buf, size, ", llvm.data_layout = \"")
_append_cstr(buf, size, self.DataLayout)
_append_cstr(buf, size, "\"")
_append_cstr(buf, size, "} {\n")
else:
_append_cstr(buf, size, "module {\n")
# Mode comment (MLIR has no stub/text split)
if mode == OUTPUT_STUB:
_append_cstr(buf, size, " // MLIR-STUB: global and function declarations only\n")
elif mode == OUTPUT_TEXT:
_append_cstr(buf, size, " // MLIR-TEXT: body-bearing functions and initialized globals only\n")
# Line-indent scratch buffer (module members indent 2 spaces). Its cap is bounded by the
# module pool's buddy limit: a ~2 MiB sub-pool (max_order=15) allows max 1 MiB per block,
# minus a 16-byte header ⇒ 1048560. MLIR text is long (inlined named-struct bodies), so use
# the largest single block the pool can serve; overflow is caught and fails loud.
tmp_cap: sizet = 1048560
tmp: str = pool.alloc(tmp_cap)
if tmp is None:
MlirFail("MLIR render: module indent scratch buffer allocation failed (1048560 ~= 1 MiB - 16-byte block header)")
# Globals (MLIR has no standalone type-definition line: named structs are inlined at the
# use site, so NamedTypeHead definitions are not emitted here, only registered for lookup).
gv: GlobalVariable* = self.GlobalHead
gv_steps: sizet = 0
while gv is not None:
gv_steps += 1
if gv_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: module globals (GlobalCount=%zu, dump aborted)\n",
self.GlobalCount)
stdlib.exit(1)
if mode == OUTPUT_TEXT and gv.Initializer is None:
# text mode skips globals with no initializer (pure declarations)
gv = gv.Next
continue
if tmp is not None:
tmp[0] = '\0'
if mode == OUTPUT_STUB:
_MlirGlobalLine(tmp, tmp_cap, pool, gv, 1)
else:
_MlirGlobalLine(tmp, tmp_cap, pool, gv, 0)
_MlirAppendIndented(buf, size, tmp, 2, pool)
gv = gv.Next
# Walk functions (filter declare/define by mode)
func: Function* = self.FuncHead
func_steps: sizet = 0
while func is not None:
func_steps += 1
if func_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: module funcs print (FuncCount=%zu, dump aborted)\n",
self.FuncCount)
stdlib.exit(1)
emit: int = 0
if mode == OUTPUT_STUB:
if func.IsDeclared == 1:
emit = 1
elif mode == OUTPUT_TEXT:
if func.IsDeclared == 0:
emit = 1
else:
emit = 1
if emit != 0 and tmp is not None:
tmp[0] = '\0'
FunctionPrint(tmp, tmp_cap, func, pool)
_MlirAppendIndented(buf, size, tmp, 2, pool)
func = func.Next
_append_cstr(buf, size, "}\n")
# Print done: clear the named-struct registry (its type pointers are no longer dereferenced)
MlirNamedTypesClear()
# _MlirPublishNamedTypes - publish the module's named-struct list to the __types.vp registry.
# Name-only type text (%"sha1.Cls") must expand to the inline named body, so this publishes a
# name → full-definition lookup (opaque entries too, so they report "still opaque", not
# "unregistered") plus each node's packed flag (NamedTypeNode.IsPacked) for the render side.
def _MlirPublishNamedTypes(mod: LLVMModule*, pool: memhub.MemBuddy*):
"""Rebuild the named-struct registry from NamedTypeHead (print-time lookup, incl. packed).
Count the real list length first (not trusting NamedTypeCount), then allocate exactly that
many slots and register each; real modules can have thousands of named types, so no fixed
capacity is assumed; nothing is allocated when there are none.
"""
if mod is None or pool is None:
return
cnt: sizet = 0
nt: NamedTypeNode* = mod.NamedTypeHead
nt_steps: sizet = 0
while nt is not None:
nt_steps += 1
if nt_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: module named types count (NamedTypeCount=%zu, count aborted)\n",
mod.NamedTypeCount)
stdlib.exit(1)
cnt += 1
nt = nt.Next
MlirNamedTypesBegin(pool, cnt)
nt = mod.NamedTypeHead
nt_steps = 0
while nt is not None:
nt_steps += 1
if nt_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: module named types publish (NamedTypeCount=%zu, publish aborted)\n",
mod.NamedTypeCount)
stdlib.exit(1)
if nt.Ty is not None:
# The packed flag lives only on NamedTypeNode (LLVMType.Struct lacks it), set only by
# module_set_named_type_packed; register it together with the type here.
MlirNamedTypeRegister(nt.Ty, nt.IsPacked)
nt = nt.Next
# Factory functions
def new_module(pool: memhub.MemBuddy*, name: str) -> LLVMModule*:
"""Create a module."""
# Hardcoded 144 bytes: 18 fields * 8 bytes (no vtable, novirtual), incl. the DI fields
# (DIMetaHead/Tail/Count, DINamedHead/Tail/Count), same layout as llvmlite.
ptr: LLVMModule* = pool.alloc(144)
if ptr is None: return None
string.memset(ptr, 0, 144)
ptr.Name = name
ptr.FuncHead = None
ptr.FuncTail = None
ptr.FuncCount = 0
ptr.GlobalHead = None
ptr.GlobalTail = None
ptr.GlobalCount = 0
ptr.TargetTriple = None
ptr.DataLayout = None
ptr.NamedTypeHead = None
ptr.NamedTypeTail = None
ptr.NamedTypeCount = 0
ptr.DIMetaHead = None
ptr.DIMetaTail = None
ptr.DIMetaCount = 0
ptr.DINamedHead = None
ptr.DINamedTail = None
ptr.DINamedCount = 0
return ptr
# GlobalVariable hardcoded size: Next(8)+Name(8)+Ty(8)+Initializer(8)+IsConstant(4)+Linkage(8)+Section(8)+UnnamedAddr(4)+Align(4) = 72 bytes
define GV_SIZE = 72
# NamedTypeNode hardcoded size: Next(8) + Ty(8) + IsPacked(8) = 24 bytes
define NAMED_TYPE_NODE_SIZE = 24
# DINode hardcoded size: Next(8) + Text(8) = 16 bytes (same as llvmlite)
define DI_NODE_SIZE = 16
# DINamedMeta hardcoded size: Next(8) + Name(8) + Refs(8) = 24 bytes (same as llvmlite)
define DI_NAMED_SIZE = 24
def new_global_variable(pool: memhub.MemBuddy*,
name: str, ty: LLVMType*) -> GlobalVariable*:
"""Create a global variable."""
ptr: GlobalVariable* = pool.alloc(GV_SIZE)
if ptr is None: return None
string.memset(ptr, 0, GV_SIZE)
ptr.Name = name
ptr.Ty = ty
ptr.Initializer = None
ptr.IsConstant = 0
ptr.Linkage = "external"
ptr.Section = None
ptr.UnnamedAddr = 0
ptr.Align = 0
return ptr
# List operations (embedded Head/Tail/Count, O(1) append)
def module_add_function(mod: LLVMModule*, func: Function*):
mod.add_function(func)
def module_get_func_head(mod: LLVMModule*) -> Function*:
"""Return the module's function list head (cross-module accessor bypassing the stub-field trap)."""
if mod is None:
return None
return mod.FuncHead
def module_add_global(mod: LLVMModule*, gv: GlobalVariable*):
mod.add_global(gv)
def module_set_target(mod: LLVMModule*, triple: str):
mod.set_target(triple)
def module_set_datalayout(mod: LLVMModule*, layout: str):
mod.set_datalayout(layout)
# DI debug metadata (kept in sync; ignored at MLIR print time, model only)
def module_add_di_node(mod: LLVMModule*, pool: memhub.MemBuddy*,
text: str) -> int:
"""Append a DI metadata node; return its !N id (-1 on failure)."""
return mod.add_di_node(pool, text)
def module_add_named_metadata(mod: LLVMModule*, pool: memhub.MemBuddy*,
name: str, refs: str):
"""Register named metadata "!<name> = !{ <refs> }"."""
mod.add_named_metadata(pool, name, refs)
# Named-struct type management
# _is_opaque_struct - check whether a type is an opaque struct.
# An opaque struct is a Struct with fcount == -1 (sentinel for a cross-module forward declaration,
# name only), created by Struct(pool, None, -1, name). Must be distinguished from an empty struct
# (fcount==0, fields==None), which is a valid full definition ({} type, allocatable).
# Returns: 1 = opaque / 0 = not.
def _is_opaque_struct(ty: LLVMType*) -> int:
"""Return 1 if the type is an opaque struct, else 0.
opaque is a cross-module forward declaration (name only) created by
_scan_type_for_cross_module_refs, marked by fcount=-1. An empty struct (fcount==0,
fields==None) is a valid full definition (LLVM {} type, allocatable) and must be kept
distinct from opaque, or alloca fails with Cannot allocate unsized type.
"""
if ty is None:
return 0
match ty:
case LLVMType.Struct(fields, fcount, name):
# opaque: fcount == -1 (sentinel)
if fcount < 0:
return 1
return 0
case _:
return 0
def module_add_named_type(mod: LLVMModule*, pool: memhub.MemBuddy*,
ty: LLVMType*):
mod.add_named_type(pool, ty)
def module_set_named_type_packed(mod: LLVMModule*, pool: memhub.MemBuddy*,
ty: LLVMType*, is_packed: int):
"""Set a named struct's packed flag by name (MLIR emits !llvm.struct<"name", packed (…)>).
Called after parsing @c.Attribute(t.attr.packed) so llc computes field offsets/GEP with the
packed layout. MLIR carries the flag via the print-time registry (read from
NamedTypeNode.IsPacked) and adds the `packed ` keyword at the use site. ty must be a named
Struct (located by get_struct_name); not found is skipped silently. Anonymous structs return
immediately — the flag has no place on them.
"""
if mod is None or ty is None:
return
name: str = get_struct_name(ty)
if name is None:
return
cur: NamedTypeNode* = mod.NamedTypeHead
cnt_p: int = 0
while cur is not None:
cnt_p += 1
if cnt_p > 100000:
return
if cur.Ty is not None:
existing_name: str = get_struct_name(cur.Ty)
if existing_name is not None and string.strcmp(existing_name, name) == 0:
cur.IsPacked = is_packed
return
cur = cur.Next
# module_ensure_opaque_for_type - add opaque declarations for cross-module struct references.
# Recursively scans the LLVMType tree; for a Ptr(Struct(name)) whose '.'-containing (SHA1-qualified)
# name is undefined in the module, adds an opaque declaration, fixing llc "use of undefined type".
def _is_type_defined_in_module(mod: LLVMModule*, name: str) -> int:
return mod._is_type_defined_in_module(name)
def _scan_type_for_cross_module_refs(pool: memhub.MemBuddy*,
mod: LLVMModule*,
ty: LLVMType*,
depth: int = 0):
"""Recursively scan the type tree and add opaque declarations for cross-module struct refs."""
if ty is None or mod is None or pool is None:
return
# Depth guard: a self-referential struct (list/tree node, e.g. Node{...; Ptr(Node) next})
# would recurse forever; cap the depth like other recursions (TypePrint/DeepCopyType).
if depth >= 100:
return
match ty:
case LLVMType.Ptr(pointee):
if pointee is not None:
match pointee:
case LLVMType.Struct(sfields, sfcount, sname):
if sname is not None:
# Check for a cross-module reference (name contains '.')
if string.strchr(sname, 46) is not None:
# Check whether it is already defined in the module
if _is_type_defined_in_module(mod, sname) == 0:
# Create an opaque type and add it to the module; fcount=-1 marks
# opaque (vs. empty {} struct, whose fcount==0 is a valid
# full definition and can be alloc'd).
opaque_ty: LLVMType* = Struct(pool, None, -1, sname)
if opaque_ty is not None:
module_add_named_type(mod, pool, opaque_ty)
# Recurse into struct fields for cross-module refs
# (e.g. HashTable in TypeRegistry{HashTable*, MemManager*}).
if sfields is not None:
fld: ParamNode* = sfields
fld_steps: sizet = 0
while fld is not None:
fld_steps += 1
if fld_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: struct fields (sname=%s, dump aborted)\n",
sname)
stdlib.exit(1)
if fld.Ty is not None:
_scan_type_for_cross_module_refs(pool, mod, fld.Ty, depth + 1)
fld = fld.Next
case _:
_scan_type_for_cross_module_refs(pool, mod, pointee, depth + 1)
case LLVMType.Struct(sfields2, sfcount2, sname2):
# Top-level struct (non-pointer): recurse into fields
if sfields2 is not None:
fld2: ParamNode* = sfields2
fld2_steps: sizet = 0
while fld2 is not None:
fld2_steps += 1
if fld2_steps > IR_DUMP_CHAIN_LIMIT:
stdio.printf("[IR-DUMP] list cycle: struct fields2 (sname=%s, dump aborted)\n",
sname2)
stdlib.exit(1)
if fld2.Ty is not None:
_scan_type_for_cross_module_refs(pool, mod, fld2.Ty, depth + 1)
fld2 = fld2.Next
case LLVMType.Array(elem_ty, acount):
if elem_ty is not None:
_scan_type_for_cross_module_refs(pool, mod, elem_ty, depth + 1)
case _:
pass
def module_ensure_opaque_for_type(mod: LLVMModule*,
pool: memhub.MemBuddy*,
ty: LLVMType*):
mod.ensure_opaque_for_type(pool, ty)
# Global variable attribute setters
def global_set_constant(gv: GlobalVariable*, is_const: int):
"""Set whether the global is a constant."""
if gv is None: return
gv.IsConstant = is_const
def global_set_linkage(gv: GlobalVariable*, linkage: str):
"""Set the linkage (private/internal/external/...)."""
if gv is None: return
gv.Linkage = linkage
def global_set_unnamed_addr(gv: GlobalVariable*, level: int):
"""Set the unnamed_addr attribute (0=none, 1=unnamed_addr, 2=local_unnamed_addr)."""
if gv is None: return
gv.UnnamedAddr = level
def global_set_section(gv: GlobalVariable*, section: str):
"""Set the section name."""
if gv is None: return
gv.Section = section
# Output mode constants
define OUTPUT_FULL = 0# Full IR (stub + text)
define OUTPUT_STUB = 1# Declarations only (header + type defs + declare + external global)
define OUTPUT_TEXT = 2# Code only (define + global with initializer)
# dump sentinel: max traversal steps for IR lists (cycle detection, fail-fast). If any chain's
# Next is corrupted into a cycle, traversal would hang tpv.exe; exceeding the cap prints the list
# name and exits(1), turning a hang into a deterministic error (like memhub.self_check max_steps).
define IR_DUMP_CHAIN_LIMIT = 100000
_MlirModOutTrunc: int = 0
# LLVMModulePrint: serialize the module to .ll text; mode: OUTPUT_FULL / OUTPUT_STUB / OUTPUT_TEXT.
def LLVMModulePrint(buf: str, size: sizet,
mod: LLVMModule*, pool: memhub.MemBuddy*,
mode: int):
global _MlirModOutTrunc
_MlirModOutTrunc = 0
mod.PrintModule(buf, size, pool, mode)
if _MlirModOutTrunc != 0:
_MlirModOutTrunc = 0
MlirFail("MLIR render: module text exceeds output buffer and was truncated (increase the dump_ir buffer)")
# Global variable rendering (MLIR); linkage/constant/unnamed_addr/section map to keywords/attributes.
# Initializers take two paths (see _MlirGlobalInitNeedsRegion): attribute path (integer/float
# literals, cstring, string literal, pure scalar array) or region path (root-level bare null/
# zeroinitializer, struct/vtable, symbol-referencing aggregates, pointer initializers → empty value
# parens + initializer region). Any form neither path covers fails loud via MlirFail.
def _MlirGlobalLine(buf: str, size: sizet,
pool: memhub.MemBuddy*, gv: GlobalVariable*,
stub: int):
"""Render one llvm.mlir.global (stub=1 emits the external declaration form)."""
if gv is None:
return
_append_cstr(buf, size, "llvm.mlir.global ")
# Linkage
if stub != 0:
_append_cstr(buf, size, "external ")
elif gv.Linkage is not None:
# external + initializer is illegal IR, so drop external
if gv.Initializer is not None and string.strcmp(gv.Linkage, "external") == 0:
pass
else:
_append_cstr(buf, size, gv.Linkage)
_append_cstr(buf, size, " ")
if stub == 0:
# unnamed_addr
if gv.UnnamedAddr == 1:
_append_cstr(buf, size, "unnamed_addr ")
elif gv.UnnamedAddr == 2:
_append_cstr(buf, size, "local_unnamed_addr ")
# constant / plain global (non-constant needs no keyword in MLIR)
if gv.IsConstant == 1:
_append_cstr(buf, size, "constant ")
# Name
_append_cstr(buf, size, "@")
if gv.Name is not None:
if _ll_name_needs_quote(gv.Name) != 0:
_append_cstr(buf, size, "\"")
_append_cstr(buf, size, gv.Name)
_append_cstr(buf, size, "\"")
else:
_append_cstr(buf, size, gv.Name)
# Initializer: aggregates / symbol references / root-level bare zero use the region path
# (see _MlirGlobalInitNeedsRegion).
region: int = 0
if stub == 0 and gv.Initializer is not None:
region = _MlirGlobalInitNeedsRegion(gv.Initializer)
if region == 0:
_append_cstr(buf, size, "(")
_MlirGlobalInit(buf, size, pool, gv)
_append_cstr(buf, size, ")")
else:
# Region form: empty value parens (the initializer is built by insertvalue in the region)
_append_cstr(buf, size, "()")
else:
# No initializer (stub=1 external decl / global without one): the value parens MUST be kept
# — mlir-opt 22.1.8 rejects `@x : i32` ("expected '('") but accepts `@x() : i32`.
_append_cstr(buf, size, "()")
# Attribute dict (section / alignment) MUST be emitted before the type (after the value);
# mlir-opt 22.1.8 fails to parse otherwise (a dict after the type is read as a new op).
# `unnamed_addr` is a keyword (emitted before the name) and must not go in the dict.
if stub == 0 and (gv.Section is not None or gv.Align > 0):
_append_cstr(buf, size, " {")
if gv.Section is not None:
_append_cstr(buf, size, "section = \"")
_append_cstr(buf, size, gv.Section)
_append_cstr(buf, size, "\"")
if gv.Align > 0:
_append_cstr(buf, size, ", ")
if gv.Align > 0:
ab: str = pool.alloc(48)
if ab is not None:
viperlib.snprintf(ab, 48, "alignment = %d : i64", gv.Align)
_append_cstr(buf, size, ab)
_append_cstr(buf, size, "}")
# Type
_append_cstr(buf, size, " : ")
ty_buf: str = pool.alloc(4096)
if ty_buf is not None:
ty_buf[0] = '\0'
TypePrint(ty_buf, 4096, gv.Ty, pool)
_append_cstr(buf, size, ty_buf)
# initializer region (top-level aggregate/pointer initializer: leaf op + undef + insertvalue chain + return)
if region != 0:
_append_cstr(buf, size, " {\n")
_MlirGlobalInitRegion(buf, size, pool, gv)
_append_cstr(buf, size, "}")
_append_cstr(buf, size, "\n")
def _MlirGlobalInit(buf: str, size: sizet,
pool: memhub.MemBuddy*, gv: GlobalVariable*):
"""Render a global initializer (MLIR attribute text)."""
x: str = gv.Initializer
if x is None:
return
if x[0] == 'c' and x[1] == '"':
_MlirCStringToMlir(buf, size, x, pool)
return
if x[0] == '"':
# Already a string literal
_append_cstr(buf, size, x)
return
if x[0] == '[':
# Integer/float array: [i8 1, i8 2] → [1 : i8, 2 : i8]. mlir-opt 22.1.8 rejects
# dense<[...]> (dense only takes builtin shaped types) and untyped elements default to
# i64; the correct form is the array attribute in the value parens with per-element
# `value : type` annotations. The global type is emitted after the parens by _MlirGlobalLine.
_MlirArrayDenseAt(buf, size, x, pool)
return
if string.strcmp(x, "null") == 0 or string.strcmp(x, "zeroinitializer") == 0:
# Root-level bare zero now goes through the region path (_MlirGlobalInitNeedsRegion returns
# 1); this attribute fallback is kept only as a safety net (mlir-opt rejects `(zeroinitializer)`).
_append_cstr(buf, size, "zeroinitializer")
return
if x[0] == '@':
# Symbol-reference initializer (top-level @name uses the region path; this is the fallback)
_append_cstr(buf, size, x)
return
c0: int = x[0]
if c0 == '-' or (c0 >= '0' and c0 <= '9'):
# Integer/float literal: "<text> : <MLIR type>"
_append_cstr(buf, size, x)
_append_cstr(buf, size, " : ")
ty_buf: str = pool.alloc(4096)
if ty_buf is not None:
ty_buf[0] = '\0'
TypePrint(ty_buf, 4096, gv.Ty, pool)
_append_cstr(buf, size, ty_buf)
return
# Residual form not covered by the attribute path and not routed to region: fail loud, never
# emit wrong MLIR (structs/vtables, symbol-referencing aggregates and pointer initializers are
# already handled by _MlirGlobalInitRegion on the region path).
MlirFail("MLIR render: global initializer form unsupported in v1 (neither the attribute path nor the initializer region covers it)")
def _MlirCStringToMlir(buf: str, size: sizet, src: str, pool: memhub.MemBuddy*):
"""c"hello\\00" → "hello\\00" (drop only the 'c' prefix; keep escapes verbatim).
mlir-opt 22.1.8 requires the string attribute length to equal the declared i8 array length.
VPC string constants include a trailing NUL ([N+1 x i8]), so \\00 MUST be kept — stripping it
would drop the C terminator and break the ABI.
"""
p: str = src + 1 # Skip 'c', point at '"'
if p[0] != '"':
MlirFail("MLIR render: illegal global c-string initializer form")
_append_cstr(buf, size, p)
def _mlir_read_aggregate_token(p: str, out: str, outsz: sizet) -> str:
"""读取一个聚合/复合值 token(括号配平),到**顶层**分隔符(`,` 或 `]`)为止,不消费分隔符。
用于 `_mlir_read_value` 覆盖不到的形态,例如 `[16 x i8] zeroinitializer`、`{ i32 1, i32 2 }`:
这类值以 `[`/`{` 开头,`_mlir_read_value` 不会前进,会让外层数组解析原地打转。
如果连一个字符都没读到(调用方随后会做"至少前进 1 字符"的兜底)。
"""
p = _mlir_ws(p)
n: sizet = 0
d: int = 0
while p[0] != '\0':
ch: int = p[0]
if d == 0 and (ch == ',' or ch == ']'):
break
if ch == '[' or ch == '{' or ch == '(' or ch == '<':
d += 1
elif ch == ']' or ch == '}' or ch == ')' or ch == '>':
if d == 0:
break
d -= 1
if n + 1 >= outsz:
# A truncated aggregate value would silently shorten a global constant; fail loud.
MlirFail("MLIR render: aggregate value token is longer than its read buffer and would be truncated")
out[n] = char(ch)
n += 1
p = p + 1
out[n] = '\0'
return p
def _MlirArrayDenseAt(buf: str, size: sizet, src: str,
pool: memhub.MemBuddy*) -> str:
"""LLVM array literal → MLIR dense content (nested recursive); returns the new cursor."""
if src is None or src[0] != '[':
MlirFail("MLIR render: global array initializer form unsupported in v1")
# [修复] 三个临时缓冲**在进入时分配一次**并全程复用:原实现是"每个元素分配
# 512(类型文本) + 512(值文本) + 4096(元素 MLIR 类型) ≈ 5KB 且从不释放",于是
# `[256][16]uint8` 这类大表(4096 元素)单次就吃掉 ~20MB 编译池,多个全局数组叠加
# 直接耗尽(现场:[MEM] alloc failed … order=5/8 占 943MB、attribution site=?)。
tb: str = pool.alloc(4096)
vb: str = pool.alloc(4096)
mtb: str = pool.alloc(4096)
if tb is None or vb is None or mtb is None:
MlirFail("MLIR render: array element buffer allocation failed")
p: str = src + 1
_append_cstr(buf, size, "[")
n: int = 0
g: int = 0
while p[0] != '\0':
g += 1
# 上限只用于防"解析不收敛"的死循环,不是数组容量限制:位图字体表
# `[256] x [16]uint8` 本身就 4352 个元素(外层 256 + 内层 4096),旧值 4096 会误报。
if g > 1048576:
# [DIAG] 元素数冲到上限 ⇒ 直接暴露"是哪个数组、什么形态"(错误诊断增强:正常路径不打印)。
# 有了这段才能判断是"文本里真的百万元素"还是"解析重复吃同一段"。
db: str = pool.alloc(200)
if db is not None:
i2: int = 0
while i2 < 180 and src[i2] != '\0':
db[i2] = src[i2]
i2 += 1
db[i2] = '\0'
stdio.printf("[MLIR-DIAG] too-many-elements n=%d g=%d src_head=%s\n", n, g, db)
MlirFail("MLIR render: too many global array elements")
p = _mlir_ws(p)
if p[0] == ']':
p = p + 1
break
if p[0] == '\0':
break
if n > 0:
_append_cstr(buf, size, ", ")
if p[0] == '[':
p = _MlirArrayDenseAt(buf, size, p, pool)
else:
p = _mlir_read_type(p, tb, 4096)
p = _mlir_ws(p)
p2: str = _mlir_read_value(p, vb, 4096)
# [修复] 值不是 %name / @"name" / 字面量时(聚合值:`[16 x i8] zeroinitializer`、
# `{ i32 1, i32 2 }`),`_mlir_read_value` 原地不前进 ⇒ 外层 while 不收敛 ⇒
# g 冲到上限并误报 "too many global array elements"。这里回退为括号配平读取,
# 并保证**至少前进 1 个字符**,绝不原地打转。
if p2 == p:
p2 = _mlir_read_aggregate_token(p, vb, 4096)
if p2 == p:
vb[0] = char(p[0])
vb[1] = '\0'
p2 = p + 1
p = p2
_append_cstr(buf, size, vb)
# Elements must carry a type annotation: MLIR array attributes default to i64, so
# i8/i32/pointer elements would mismatch the global type (working form:
# `[1 : i8, 2 : i8] : !llvm.array<2 x i8>`).
_append_cstr(buf, size, " : ")
mtb[0] = '\0'
MlirTypeText(mtb, 4096, tb, pool)
_append_cstr(buf, size, mtb)
n += 1
p = _mlir_ws(p)
if p[0] == ',':
p = p + 1
_append_cstr(buf, size, "]")
return p
# Global initializer region rendering (struct/vtable aggregates and pointer initializers): leaves
# (addressof/zero/constant) first, then undef + insertvalue chain, whose type suffix is always the
# top-level aggregate and whose index path comes from a parallel initializer/type walk. Element MLIR
# types come from the global type tree via TypePrint; constexpr bitcast/GEP are treated as symbol
# references; unsupported leaves fail loud.
# Sized for large aggregate tables (e.g. a [256][16] uint8 font: 4096 leaves plus 4096 insertvalue
# lines of ~110 B each ≈ 450 KiB). These two buffers are malloc'd instead of taken from the module
# sub-pool, so their size is independent of the pool capacity.
define GV_REGION_BUF_SIZE = 4194304
# Index-path reuse stack: one slot per recursion depth (top-level path="" → depth d in slot d).
# Depth cap matches _MlirGvWalk's 64-level guard with margin (72); 512 bytes per slot suffices.
define GV_PATH_SLOT = 512
define GV_PATH_MAX = 72
# Region-render state (single-threaded recursive render, reset per global)
_MlirGvLeafBuf: str = None # Leaf op buffer
_MlirGvInsBuf: str = None # insertvalue chain buffer
_MlirGvTopType: str = None # Top-level aggregate MLIR type text
_MlirGvLast: str = None # SSA name of the current node
_MlirGvPrev: str = None # SSA name of the previous insertvalue result
_MlirGvPool: memhub.MemBuddy* = None
_MlirGvLeafSeq: int = 0
_MlirGvInsSeq: int = 0
# Set when a region scratch buffer (leaf/insertvalue) overflows, so its failure is reported as a
# region-buffer overflow instead of being conflated with the module output buffer.
_MlirGvOutTrunc: int = 0
# ---- Region-render REUSED scratch buffers (lazily allocated once, shared across globals/nodes) ----
# Allocating new buffers per global/node exhausted the ~2 MiB sub-pool for struct/vtable-heavy
# modules; the render is single-threaded and sequential, so one allocation reused is enough.
_MlirGvSbRead1: str = None # 512B: type-prefix token read scratch
_MlirGvSbRead2: str = None # 512B: leaf type-token read scratch
_MlirGvSbName: str = None # 512B: symbol-name / constexpr symbol-name scratch
_MlirGvSbVal: str = None # 512B: scalar initializer literal scratch
_MlirGvTyBuf: str = None # 4096B: element MLIR type text (zero/const)
_MlirGvNameBuf: str = None # 32B: SSA name generation scratch
_MlirGvPathStack: str = None # GV_PATH_MAX × GV_PATH_SLOT: index-path reuse stack
def MlirGvResetScratch():
"""Invalidate the region-render scratch so EnsureScratch reallocates from the current pool.
The scratch is reused only within a single PrintModule (shared across globals/nodes); clearing
it at print end prevents reusing a previous print's pointers (freed or reused pool → dangling).
The two large region buffers are malloc'd, so they are freed here.
"""
global _MlirGvLeafBuf, _MlirGvInsBuf
if _MlirGvLeafBuf is not None: stdlib.free(_MlirGvLeafBuf)
if _MlirGvInsBuf is not None: stdlib.free(_MlirGvInsBuf)
_MlirGvLeafBuf = None
_MlirGvInsBuf = None
def _MlirGvEnsureScratch(pool: memhub.MemBuddy*) -> int:
"""Lazily allocate this PrintModule's reused scratch buffers (shared across globals/nodes); returns 1 when ready."""
global _MlirGvLeafBuf, _MlirGvInsBuf, _MlirGvTopType, _MlirGvLast, _MlirGvPrev
global _MlirGvSbRead1, _MlirGvSbRead2, _MlirGvSbName, _MlirGvSbVal
global _MlirGvTyBuf, _MlirGvNameBuf, _MlirGvPathStack
if _MlirGvLeafBuf is not None:
return 1
lb: str = stdlib.malloc(GV_REGION_BUF_SIZE)
ib: str = stdlib.malloc(GV_REGION_BUF_SIZE)
tb: str = pool.alloc_tagged(4096, memhub.DETSITE_MODULE_984)
la: str = pool.alloc_tagged(32, memhub.DETSITE_MODULE_985)
pr: str = pool.alloc_tagged(32, memhub.DETSITE_MODULE_986)
# Symbol/type/value scratch: 4096 each (a type text inlines full struct bodies and a symbol
# name can carry a SHA1 prefix + long class/method names; 512 silently truncated both).
r1: str = pool.alloc_tagged(4096, memhub.DETSITE_MODULE_987)
r2: str = pool.alloc_tagged(4096, memhub.DETSITE_MODULE_988)
nm: str = pool.alloc_tagged(4096, memhub.DETSITE_MODULE_989)
vl: str = pool.alloc_tagged(4096, memhub.DETSITE_MODULE_990)
tyb: str = pool.alloc_tagged(4096, memhub.DETSITE_MODULE_991)
nb: str = pool.alloc_tagged(32, memhub.DETSITE_MODULE_992)
ps: str = pool.alloc_tagged(GV_PATH_SLOT * GV_PATH_MAX, memhub.DETSITE_MODULE_993)
if lb is None or ib is None or tb is None or la is None or pr is None \
or r1 is None or r2 is None or nm is None or vl is None \
or tyb is None or nb is None or ps is None:
MlirFail("MLIR render: global region buffer allocation failed")
lb[0] = '\0'; ib[0] = '\0'; tb[0] = '\0'; la[0] = '\0'; pr[0] = '\0'
r1[0] = '\0'; r2[0] = '\0'; nm[0] = '\0'; vl[0] = '\0'
tyb[0] = '\0'; nb[0] = '\0'; ps[0] = '\0'
_MlirGvLeafBuf = lb
_MlirGvInsBuf = ib
_MlirGvTopType = tb
_MlirGvLast = la
_MlirGvPrev = pr
_MlirGvSbRead1 = r1
_MlirGvSbRead2 = r2
_MlirGvSbName = nm
_MlirGvSbVal = vl
_MlirGvTyBuf = tyb
_MlirGvNameBuf = nb
_MlirGvPathStack = ps
return 1
def _MlirGlobalInitNeedsRegion(x: str) -> int:
"""Return 1 if a global initializer must go through the initializer region, else 0.
The attribute path (tested; do not regress) covers only c strings, string literals, scalar
literals and pure-scalar arrays. Everything else uses region: root-level bare `null`/
`zeroinitializer`, struct `{…}`, arrays with symbol refs/structs/MLIR type prefixes, top-level
bare `@name`, and pointer/aggregate-prefixed initializers (e.g. `i8* null`, `i8* bitcast (…)`).
"""
if x is None or x[0] == '\0':
return 0
if x[0] == 'c' and x[1] == '"':
return 0
if x[0] == '"':
return 0
if x[0] == '[':
return _MlirGvArrayNeedsRegion(x)
if x[0] == '{':
return 1
if string.strcmp(x, "null") == 0 or string.strcmp(x, "zeroinitializer") == 0:
# Root-level bare zero uses region: the attribute form `(zeroinitializer)` is rejected by
# mlir-opt 22.1.8; region emits `llvm.mlir.zero : <top type>` + a direct `llvm.return`.
return 1
if x[0] == '@':
return 1
c0: int = x[0]
if c0 == '-' or (c0 >= '0' and c0 <= '9'):
return 0
return 1
def _MlirGvArrayNeedsRegion(x: str) -> int:
"""Return 1 if an array initializer must use the initializer region, else 0.
Returns 1 on any of: symbol reference '@' (attribute path cannot express addresses), struct
'{' (aggregate element), or MLIR type prefix '!' (e.g. a nested sub-array prefix). The
attribute path (_MlirArrayDenseAt) only handles dense arrays of `value : scalar type`;
inner type prefixes and sub `zeroinitializer` need the region's insertvalue chain. String
literals are skipped so '{'/'!' inside them are not misdetected.
"""
p: str = x
while p[0] != '\0':
ch: int = p[0]
if ch == '"':
p = p + 1
while p[0] != '\0' and p[0] != '"':
# LLVM strings use \xx hex escapes; skip the byte after a '\'
if p[0] == 0x5C and p[1] != '\0':
p = p + 1
p = p + 1
if p[0] == '"':
p = p + 1
continue
if ch == '@' or ch == '{' or ch == '!':
return 1
p = p + 1
return 0
def _MlirGlobalInitRegion(buf: str, size: sizet,
pool: memhub.MemBuddy*, gv: GlobalVariable*):
"""Render the initializer region body (lines indented 2 spaces, without the outer braces).
Each line ends with '\\n'; the caller appends '}' and the module layer re-indents by 2.
"""
global _MlirGvPool, _MlirGvLeafSeq, _MlirGvInsSeq, _MlirGvOutTrunc
x: str = gv.Initializer
if x is None:
return
# Reused buffers (lazily allocated once, shared across globals) — allocating per global without
# freeing exhausted the module sub-allocator for struct/vtable-heavy modules.
_MlirGvEnsureScratch(pool)
topbuf: str = _MlirGvTopType
leafbuf: str = _MlirGvLeafBuf
insbuf: str = _MlirGvInsBuf
lastbuf: str = _MlirGvLast
prevbuf: str = _MlirGvPrev
topbuf[0] = '\0'
leafbuf[0] = '\0'
insbuf[0] = '\0'
lastbuf[0] = '\0'
prevbuf[0] = '\0'
TypePrint(topbuf, 4096, gv.Ty, pool)
_MlirGvPool = pool
_MlirGvLeafSeq = 0
_MlirGvInsSeq = 0
_MlirGvOutTrunc = 0
is_agg: int = 0
match gv.Ty:
case LLVMType.Struct(sf, sfc, sn):
is_agg = 1
case LLVMType.Array(ae, ac):
is_agg = 1
case _:
is_agg = 0
if is_agg != 0:
# Start of the insertvalue chain: top-level undef
string.strcpy(prevbuf, "%undef")
_MlirGvWalk(x, gv.Ty, "", 0)
if _MlirGvOutTrunc != 0:
MlirFail("MLIR render: global initializer region text exceeds GV_REGION_BUF_SIZE (increase it)")
# Emit leaf ops first, then the undef + insertvalue chain
_append_cstr(buf, size, leafbuf)
if is_agg != 0:
_append_cstr(buf, size, " %undef = llvm.mlir.undef : ")
_append_cstr(buf, size, topbuf)
_append_cstr(buf, size, "\n")
_append_cstr(buf, size, insbuf)
_append_cstr(buf, size, " llvm.return ")
if lastbuf[0] != '\0':
_append_cstr(buf, size, lastbuf)
else:
_append_cstr(buf, size, "%undef")
_append_cstr(buf, size, " : ")
_append_cstr(buf, size, topbuf)
_append_cstr(buf, size, "\n")
def _MlirGvWalk(p: str, ty: LLVMType*, path: str, depth: int) -> str:
"""Walk one node of the initializer-tree × type-tree in parallel; returns the new cursor.
The result SSA name goes to _MlirGvLast (leaf = materialized name; aggregate = last
insertvalue name; empty aggregate = %undef). path looks like "" / "0" / "0, 1".
"""
if depth > 64:
MlirFail("MLIR render: global initializer nested too deeply (> 64 levels)")
if ty is None:
MlirFail("MLIR render: global initializer type tree missing (cannot determine the element MLIR type)")
p = _mlir_ws(p)
if p[0] == '\0':
MlirFail("MLIR render: global initializer text ended unexpectedly")
# Explicit array type prefix (e.g. "[3 x i8] [i8 1, i8 2]") is read off first
if p[0] == '[' and _MlirGvIsArrayType(p) != 0:
tb: str = _MlirGvSbRead1
p = _mlir_read_type(p, tb, 4096)
p = _mlir_ws(p)
# Explicit struct type prefix (e.g. "{ i32, void ()*, i8* } { i32 65535, … }" for a
# llvm.global_ctors element) is read off first; `{…}` may be a type or a literal, told apart
# by whether another `{` immediately follows (see _MlirGvIsStructType).
if p[0] == '{' and _MlirGvIsStructType(p) != 0:
stb: str = _MlirGvSbRead1
p = _mlir_read_type(p, stb, 4096)
p = _mlir_ws(p)
# An MLIR-form aggregate type prefix (`!llvm.array<…>` / `!llvm.struct<…>`) immediately
# followed by an aggregate literal is read off first: it comes from upper layers via
# mlirlite.TypePrint (e.g. a nested sub-array prefix `!llvm.array<16 x i8>[i8 0, …]`); the
# LLVM-form predicates above do not recognize a '!' prefix.
if p[0] == '!':
mtb: str = _MlirGvSbRead1
qm: str = _mlir_read_type(p, mtb, 4096)
qm = _mlir_ws(qm)
if qm[0] == '[' or qm[0] == '{':
p = qm
if p[0] == '{':
return _MlirGvWalkStruct(p, ty, path, depth)
if p[0] == '[':
return _MlirGvWalkArray(p, ty, path, depth)
return _MlirGvWalkLeaf(p, ty, path, depth)
def _MlirGvIsArrayType(p: str) -> int:
"""Return 1 if the cursor is at an LLVM array type prefix "[N x TY]" (vs. an array literal)."""
if p is None or p[0] != '[':
return 0
q: str = p + 1
nd: int = 0
while q[0] >= '0' and q[0] <= '9':
q = q + 1
nd += 1
if nd == 0 or q[0] != ' ':
return 0
q = q + 1
if q[0] != 'x' or q[1] != ' ':
return 0
return 1
def _MlirGvIsStructType(p: str) -> int:
"""Return 1 if the cursor is at an LLVM struct TYPE prefix `{ … } { … }` (vs. a struct literal).
`{ … }` serves as both struct-type and struct-literal syntax, so the first brace group alone
is ambiguous. Reliable test: read the first brace group with depth balancing (skipping quoted
text, where LLVM strings may contain `{`/`}`); if the next non-space token is still '{', the
first group was a type prefix and the second is the initializer, else the group is the value.
"""
if p is None or p[0] != '{':
return 0
q: str = p
dep: int = 0
while q[0] != '\0':
ch: int = q[0]
if ch == '"':
q = q + 1
while q[0] != '\0' and q[0] != '"':
if q[0] == 0x5C and q[1] != '\0':
q = q + 1
q = q + 1
if q[0] == '"':
q = q + 1
continue
if ch == '{':
dep += 1
elif ch == '}':
dep -= 1
if dep == 0:
q = q + 1
break
q = q + 1
if dep != 0:
return 0
q = _mlir_ws(q)
if q[0] == '{':
return 1
return 0
def _MlirGvPathChild(path: str, idx: int, depth: int) -> str:
"""Build a child index path from the parent path and child index (top-level "" → "0").
Writes to the depth-slotted reuse stack (slot = depth): ancestors occupy 0..depth-1, this
level writes slot depth, the child writes depth+1, so nothing overlaps; avoids per-node allocs.
"""
if depth < 0 or depth >= GV_PATH_MAX:
MlirFail("MLIR render: index path depth out of range")
nb: str = _MlirGvPathStack + depth * GV_PATH_SLOT
if path[0] == '\0':
viperlib.snprintf(nb, GV_PATH_SLOT, "%d", idx)
else:
viperlib.snprintf(nb, GV_PATH_SLOT, "%s, %d", path, idx)
return nb
def _MlirGvWalkStruct(p: str, ty: LLVMType*, path: str, depth: int) -> str:
"""Struct initializer { elem, elem, … }: field types come from the type tree's Struct field chain."""
fields: ParamNode* = None
match ty:
case LLVMType.Struct(sf, sfc, sn):
fields = sf
case _:
MlirFail("MLIR render: initializer is a struct but the global type is not a struct")
cur: ParamNode* = fields
p = p + 1
idx: int = 0
done: int = 0
guard: int = 0
while done == 0:
guard += 1
# 防解析不收敛的死循环,不是字段数上限(大结构体的初始化器可能很长)。
if guard > 1048576:
MlirFail("MLIR render: too many struct initializer elements")
p = _mlir_ws(p)
if p[0] == '\0':
MlirFail("MLIR render: struct initializer missing '}'")
if p[0] == '}':
p = p + 1
done = 1
elif p[0] == ',':
p = p + 1
else:
fty: LLVMType* = None
if cur is not None:
fty = paramnode_get_ty(cur)
p = _MlirGvWalk(p, fty, _MlirGvPathChild(path, idx, depth), depth + 1)
if cur is not None:
cur = paramnode_get_next(cur)
idx += 1
if idx == 0:
string.strcpy(_MlirGvLast, "%undef")
return p
def _MlirGvWalkArray(p: str, ty: LLVMType*, path: str, depth: int) -> str:
"""Array initializer [ elem, elem, … ]: element type comes from the type tree's Array element type."""
elem_ty: LLVMType* = None
match ty:
case LLVMType.Array(ae, ac):
elem_ty = ae
case _:
MlirFail("MLIR render: initializer is an array but the global type is not an array")
p = p + 1
idx: int = 0
done: int = 0
guard: int = 0
while done == 0:
guard += 1
# 防解析不收敛的死循环,不是数组元素上限(大数组的初始化器可能很长)。
if guard > 1048576:
MlirFail("MLIR render: too many array initializer elements")
p = _mlir_ws(p)
if p[0] == '\0':
MlirFail("MLIR render: array initializer missing ']'")
if p[0] == ']':
p = p + 1
done = 1
elif p[0] == ',':
p = p + 1
else:
p = _MlirGvWalk(p, elem_ty, _MlirGvPathChild(path, idx, depth), depth + 1)
idx += 1
if idx == 0:
string.strcpy(_MlirGvLast, "%undef")
return p
def _MlirGvIsBareValue(p: str) -> int:
"""Return 1 if the cursor is DIRECTLY at a value (no LLVM/MLIR type prefix).
VPC root initializers may be bare (e.g. a string-pointer global's
`getelementptr inbounds ([N x i8], … @.str.x …)`, or an object storage reference `@name`);
aggregate elements are usually `<type> <value>`. A type token never starts with
`@`/digit/`-`/`.` nor has the names bitcast/getelementptr/null/zeroinitializer, so this is unambiguous.
"""
if p is None or p[0] == '\0':
return 0
if p[0] == '@':
return 1
if string.strncmp(p, "bitcast", 7) == 0:
return 1
if string.strncmp(p, "getelementptr", 13) == 0:
return 1
if string.strncmp(p, "null", 4) == 0:
return 1
if string.strncmp(p, "zeroinitializer", 15) == 0:
return 1
c0: int = p[0]
if c0 == '-' or (c0 >= '0' and c0 <= '9') or c0 == '.':
return 1
return 0
def _MlirGvWalkLeaf(p: str, ty: LLVMType*, path: str, depth: int) -> str:
"""Leaf element `<LLVM type> <value>`: the value parses to a symbol ref / null / scalar literal.
The element MLIR type comes from the type tree (ty), not the text's type token. A top-level
leaf (depth == 0, e.g. a pointer global) skips the insertvalue chain; only leaves inside an
aggregate need insertion.
"""
p = _mlir_ws(p)
if _MlirGvIsBareValue(p) == 0:
# Element carries its own type token (`i8* null` / `!llvm.ptr null`): used only to advance the cursor
tb: str = _MlirGvSbRead2
p = _mlir_read_type(p, tb, 4096)
p = _mlir_ws(p)
if p[0] == '\0':
MlirFail("MLIR render: leaf initializer has only a type and is missing a value")
if p[0] == '@':
# Bare symbol reference (strip LLVM quotes → re-add MLIR-style via _MlirSymRefAppend)
nb: str = _MlirGvSbName
p = _MlirGvReadSymName(p, nb, 4096)
_MlirGvEmitAddressOf(nb)
elif string.strncmp(p, "bitcast", 7) == 0 \
or string.strncmp(p, "getelementptr", 13) == 0:
# Treat the whole constexpr as a symbol reference (bitcast is identity under opaque pointers; GEP takes the first element's address)
nb2: str = _MlirGvSbName
p = _MlirGvScanConstExprSym(p, nb2, 4096)
_MlirGvEmitAddressOf(nb2)
elif string.strncmp(p, "null", 4) == 0:
_MlirGvEmitZero(ty)
p = p + 4
elif string.strncmp(p, "zeroinitializer", 15) == 0:
_MlirGvEmitZero(ty)
p = p + 15
else:
c0: int = p[0]
if c0 == '-' or (c0 >= '0' and c0 <= '9') or c0 == '.':
vb: str = _MlirGvSbVal
p = _mlir_read_value(p, vb, 4096)
_MlirGvEmitConst(vb, ty)
else:
# Unsupported leaf form: fail loud with the raw text, never emit wrong output silently
emb: str = _MlirGvPool.alloc(string.strlen(p) + 128)
if emb is not None:
viperlib.snprintf(emb, string.strlen(p) + 128,
"MLIR render: global initializer leaf form unsupported in v1: '%s'", p)
MlirFail(emb)
MlirFail("MLIR render: global initializer leaf form unsupported in v1")
if depth > 0:
_MlirGvInsertLeaf(path)
return p
def _MlirGvScanConstExprSym(p: str, out: str, outsz: sizet) -> str:
"""Scan `bitcast (…)` / `getelementptr … (…)`: extract the first '@' symbol name inside the
paren group (quotes stripped, written to out); returns the cursor after the group."""
q: str = p
while q[0] != '\0' and q[0] != '(':
q = q + 1
if q[0] != '(':
MlirFail("MLIR render: constant expression missing '(' (bitcast/getelementptr)")
dep: int = 0
at: str = None
guard: int = 0
while q[0] != '\0':
guard += 1
if guard > 4096:
MlirFail("MLIR render: constant expression too long")
ch: int = q[0]
if ch == '(':
dep += 1
elif ch == ')':
dep -= 1
if dep == 0:
q = q + 1
break
elif ch == '@' and at is None:
at = q
q = q + 1
if at is None:
MlirFail("MLIR render: symbol reference '@' not found in constant expression")
_MlirGvReadSymName(at, out, outsz)
return q
def _MlirGvReadSymName(at: str, out: str, outsz: sizet) -> str:
"""Read a symbol name at '@', strip LLVM-style quotes ("…"); returns the cursor after the name."""
if at is None or at[0] != '@':
MlirFail("MLIR render: expected a symbol reference ('@')")
q: str = at + 1
n: sizet = 0
if q[0] == '"':
q = q + 1
while q[0] != '\0' and q[0] != '"':
if n + 1 >= outsz:
MlirFail("MLIR render: symbol name is longer than its read buffer and would be truncated")
out[n] = q[0]
n += 1
q = q + 1
if q[0] == '"':
q = q + 1
else:
while q[0] != '\0' and _MlirGvSymNameEnd(q[0]) == 0:
if n + 1 >= outsz:
MlirFail("MLIR render: symbol name is longer than its read buffer and would be truncated")
out[n] = q[0]
n += 1
q = q + 1
out[n] = '\0'
if n == 0:
MlirFail("MLIR render: empty symbol name")
return q
def _MlirGvSymNameEnd(ch: int) -> int:
"""Terminator for a bare symbol name (whitespace / ',' / ')' / ']' / '}')."""
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 == '}':
return 1
return 0
def _MlirGvNextLeafName() -> str:
"""Allocate the next leaf SSA name (%v0, %v1, …; reuses the shared name buffer — use immediately)."""
global _MlirGvLeafSeq
ln: str = _MlirGvNameBuf
viperlib.snprintf(ln, 32, "%%v%d", _MlirGvLeafSeq)
_MlirGvLeafSeq += 1
return ln
def _MlirGvEmitAddressOf(name: str):
"""Emit a leaf op: %vN = llvm.mlir.addressof @"name" : !llvm.ptr."""
ln: str = _MlirGvNextLeafName()
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " ")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ln)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " = llvm.mlir.addressof ")
_MlirSymRefAppend(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, name)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " : !llvm.ptr\n")
string.strcpy(_MlirGvLast, ln)
def _MlirGvEmitZero(ty: LLVMType*):
"""Emit a leaf op: %vN = llvm.mlir.zero : <element MLIR type>."""
mt: str = _MlirGvTyBuf
mt[0] = '\0'
TypePrint(mt, 4096, ty, _MlirGvPool)
ln: str = _MlirGvNextLeafName()
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " ")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ln)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " = llvm.mlir.zero : ")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, mt)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, "\n")
string.strcpy(_MlirGvLast, ln)
def _MlirGvEmitConst(val: str, ty: LLVMType*):
"""Emit a leaf op: %vN = llvm.mlir.constant(<lit> : <element MLIR type>) : <same type>."""
mt: str = _MlirGvTyBuf
mt[0] = '\0'
TypePrint(mt, 4096, ty, _MlirGvPool)
if mt[0] != 'i' and mt[0] != 'f':
MlirFail("MLIR render: global initializer scalar element type is neither integer nor float")
ln: str = _MlirGvNextLeafName()
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " ")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ln)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " = llvm.mlir.constant(")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, val)
# i1's `true`/`false` are MLIR bare boolean keywords and take no type suffix
# (`llvm.mlir.constant(false : i1)` errors "expected ')'"); other scalars must use
# `value : type` (otherwise they are untyped attrs and mismatch the result type).
if string.strcmp(val, "true") != 0 and string.strcmp(val, "false") != 0:
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, " : ")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, mt)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, ") : ")
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, mt)
_append_gv(_MlirGvLeafBuf, GV_REGION_BUF_SIZE, "\n")
string.strcpy(_MlirGvLast, ln)
def _MlirGvInsertLeaf(path: str):
"""Emit an insertvalue (multi-index path; the type suffix is always the top-level aggregate type)."""
global _MlirGvInsSeq
_MlirGvInsSeq += 1
sn: str = _MlirGvNameBuf
viperlib.snprintf(sn, 32, "%%s%d", _MlirGvInsSeq)
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, " ")
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, sn)
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, " = llvm.insertvalue ")
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, _MlirGvLast)
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, ", ")
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, _MlirGvPrev)
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, "[")
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, path)
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, "] : ")
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, _MlirGvTopType)
_append_gv(_MlirGvInsBuf, GV_REGION_BUF_SIZE, "\n")
string.strcpy(_MlirGvPrev, sn)
string.strcpy(_MlirGvLast, sn)
def _MlirAppendIndented(dst: str, dst_size: sizet, src: str,
indent: int, pool: memhub.MemBuddy*):
"""Append each line of src to dst with indent leading spaces (module member indentation)."""
if dst is None or src is None:
return
cb: str = pool.alloc(2)
if cb is None:
_append_cstr(dst, dst_size, src)
return
p: str = src
while p[0] != '\0':
for i in range(int(indent)):
_append_cstr(dst, dst_size, " ")
while p[0] != '\0' and p[0] != '\n':
cb[0] = p[0]
cb[1] = '\0'
_append_cstr(dst, dst_size, cb)
p = p + 1
if p[0] == '\n':
_append_cstr(dst, dst_size, "\n")
p = p + 1
# Internal helpers
# _append_cstr write-position cache (perf: O(1) positioning for consecutive appends to the same
# buffer, avoiding O(n²) strlen from the start — the IR-serialization hot spot). Falls back to
# strlen when the buffer switches or is reset.
_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; set the truncation flag on buffer-limit truncation."""
global _g_last_dst, _g_last_pos, _MlirModOutTrunc
if dst is None or src is None: return
dlen: sizet = 0
if _g_last_dst == uint64(dst):
dlen = _g_last_pos
# Buffer was reset (caller reused the same address after buf[0]=0) → back to the head
if dlen > 0 and dst[0] == '\0':
dlen = 0
# Cache-consistency check: the cached end must still be the text end (trailing NUL, previous
# byte non-NUL). Interleaved writers or a reused pool address make the cached pos stale, so
# appends land past the first NUL and become invisible; 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 immediately, else remain = dst_size - dlen underflows and writes out of bounds.
if dlen >= dst_size:
_MlirModOutTrunc = 1
return
slen: sizet = string.strlen(src)
remain: sizet = dst_size - dlen
if remain <= 0:
_MlirModOutTrunc = 1
return
i: sizet = 0
while i < slen and i + 1 < remain:
dst[dlen + i] = src[i]
i += 1
if i < slen:
_MlirModOutTrunc = 1
dst[dlen + i] = '\0'
_g_last_pos = dlen + i
def _append_gv(dst: str, dst_size: sizet, src: str):
"""Append to a global-initializer region scratch buffer; overflow sets _MlirGvOutTrunc.
_append_cstr carries a single truncation flag shared with the module output buffer, so that flag
is masked out here and a region-buffer overflow is recorded separately (otherwise a region
overflow is misreported as "module text exceeds output buffer").
"""
global _MlirGvOutTrunc, _MlirModOutTrunc
saved: int = _MlirModOutTrunc
_MlirModOutTrunc = 0
_append_cstr(dst, dst_size, src)
if _MlirModOutTrunc != 0: _MlirGvOutTrunc = 1
_MlirModOutTrunc = saved