diff --git a/_withcontent.vp b/_withcontent.vp index 4d2f96b..b578042 100644 --- a/_withcontent.vp +++ b/_withcontent.vp @@ -27,7 +27,7 @@ _tls_index: ulong = 0 def _init_tls(): """Initialize TLS index (executes once; subsequent calls are skipped).""" global _tls_index - # Double guard: TPV now implements @t.TLS (a per-function one-shot guard setting __tls_init_done), but this re-check is kept for compatibility with older TPV.exe builds. + # Double guard: VPC now implements @t.TLS (a per-function one-shot guard setting __tls_init_done), but this re-check is kept for compatibility with older VPC.exe builds. # _tls_index starts at 0 (TlsAlloc never returns 0 on success). if _tls_index == 0: _tls_index = TlsAlloc() diff --git a/coro.vp b/coro.vp index 53562a4..91b0987 100644 --- a/coro.vp +++ b/coro.vp @@ -40,5 +40,5 @@ def coro_gather(a: Coroutine) -> int: def coro_exception_msg(c: Coroutine) -> rawptr: # M7 placeholder: the compiler inlines coro.coro_exception_msg(frame) into a frame msg-slot read. - # Note: TPV (Viper) does not support bare null; use t.CPtr(0) to build a null pointer (compatible with both compilers). + # Note: VPC (Viper) does not support bare null; use t.CPtr(0) to build a null pointer (compatible with both compilers). return None diff --git a/fltk/__init__.vp b/fltk/__init__.vp index 3a95fc4..b2b0b91 100644 --- a/fltk/__init__.vp +++ b/fltk/__init__.vp @@ -15,7 +15,7 @@ # all FLTK objects are opaque pointers; methods map to cfltk's Fl__. # * The full C API is directly available via `from fltk._cfl import *` etc. # * Callback support: a Viper module-level function can be passed to FLTK directly as an Fl_Callback-- -# TPV auto-bitcasts the function name into a function pointer (e.g. CreateThread passes a function). +# VPC auto-bitcasts the function name into a function pointer (e.g. CreateThread passes a function). # Callback signature convention: def cb(w: bytes, data: bytes): ... (w=widget pointer, data=user data) # def on_click(w: bytes, data: bytes): # Fl_Widget_set_label(w, "Clicked!") diff --git a/mlirlite/README.md b/mlirlite/README.md index 8914936..27ddb35 100644 --- a/mlirlite/README.md +++ b/mlirlite/README.md @@ -4,10 +4,10 @@ **逐名逐签名同形**:内部模型(类型/值/函数/基本块/构建器/模块)与公开 API 完全一致, 唯一区别是"打印"阶段输出 MLIR 文本而不是 LLVM IR 文本。 -目的:让 TransPyV 侧只需把 `llvmlite.` 前缀重命名为 `mlirlite.` 即可编译, +目的:让 ViperC 侧只需把 `llvmlite.` 前缀重命名为 `mlirlite.` 即可编译, 从而在不改内部模型、不改 builders 的前提下切换到 MLIR 后端。 -> 实际切换方式(2026-09):不动 TransPyV 侧任何 `llvmlite.` 调用点,也不动 +> 实际切换方式(2026-09):不动 ViperC 侧任何 `llvmlite.` 调用点,也不动 > `includes/llvmlite/**`——工程文件 `project.vpj` 的 `options.target = "mlir"` 会让 > 模块名 `llvmlite` 在名称→SHA1 解析处改写为 `mlirlite`(唯一改动点是 > `StubMergerIR._FindSha1ByModName`)。开关、回退与缓存标签要求见 @@ -61,7 +61,7 @@ builders 未改,而它们内嵌的类型一律来自 `IRBuilder._type_str` → 同时保留 LLVM 语法分支(`i8*`、`[N x x]`、`{…}`)以兼容手工构造的中间文本; - 结构体类型在中间文本里就已是完整内联形态(匿名 `!llvm.struct<(…)>`、 具名 `!llvm.struct<"name", (…)>`,见 §2.10),因此**不需要**在中间文本里 - 出现 `%"name"` 这种"只有名字"的形态;若出现(手工构造 / TPV 注入), + 出现 `%"name"` 这种"只有名字"的形态;若出现(手工构造 / VPC 注入), `MlirTypeText` 按**模块打印期发布的登记表**回查完整定义后展开为具名内联形态, 查不到 / 仍是 opaque ⇒ 响亮报错; - `_mlir_read_type` 的类型读取按 `[] {} () <>` 统一做括号平衡, @@ -215,10 +215,10 @@ region 内元素 MLIR 类型并行取自**全局类型树**(`Struct` 字段链 **⚠ 五条由 mlir-opt 22.1.8 实测确立的硬约束(违反即解析/校验失败)** 1. **符号名判据与 LLVM 不同**:MLIR 裸标识符**首字符必须是字母或 `_`**,且 - **不允许 `-`**(LLVM 允许)。⇒ TPV 的 `{sha1}.{name}`(sha1 以数字开头)与 + **不允许 `-`**(LLVM 允许)。⇒ VPC 的 `{sha1}.{name}`(sha1 以数字开头)与 `@.str`(以 `.` 开头)**一律需要 `@"…"` 引号形式**。判据见 `__function.vp:_ll_name_needs_quote`,输出统一走 `_MlirSymRefAppend`。 -2. **字符串属性长度必须等于 i8 数组长度**:TPV 的字符串常量类型含尾随 NUL +2. **字符串属性长度必须等于 i8 数组长度**:VPC 的字符串常量类型含尾随 NUL (`[N+1 x i8]`)⇒ 属性里**必须保留 `\00`**。**绝不能**为了对齐长度去剥掉 类型里的 NUL —— 那会丢掉 C 字符串终止符、破坏 ABI。 3. **`dense<[…] : !llvm.array<…>>` 非法**(`dense` 只接受 builtin shaped 类型 @@ -268,7 +268,7 @@ module { - 对齐值只接受**常量十进制 + 2 的幂**:非常量(SSA)/非幂/超范围(> 2^30)一律 `MlirFail` —— mlir-opt 自身不校验幂次(实测 `{alignment = 3 : i64}` exit 0),故由本层把关。 -### 2.8 内联汇编(`build_inline_asm` / TPV 注入文本) +### 2.8 内联汇编(`build_inline_asm` / VPC 注入文本) 两条来源的中间文本 opcode 都是 `call`,由 `_MlirCall` 转交 `_MlirAsm`: @@ -284,14 +284,14 @@ module { `asm_dialect = inteldialect` 报 `expected … enum values … [att, intel]`)。 - **输入操作数在 MLIR 里是逗号分隔的裸操作数、不带括号**(LLVM 词法的 `"…"(%v)` 实测报 `expected ':'`);操作数类型串位于 `: (<操作数类型…>) -> <返回类型|()>`。 -- 汇编/约束串按引号串整体透传:TPV 的 `\22`/`\5C`/`\0A` 转义对 MLIR 词法同样合法(实测 exit 0)。 +- 汇编/约束串按引号串整体透传:VPC 的 `\22`/`\5C`/`\0A` 转义对 MLIR 词法同样合法(实测 exit 0)。 - 只认 `sideeffect` / `inteldialect` 两个关键字;其它记号(含 LLVM 的 `attdialect`/`alignstack`/`unwind`)、缺约束串、实参不成对、有返回值却无结果名 ⇒ `MlirFail` 响亮报错。 (`attdialect`/`alignstack` 在 1024 份真实 `.ll` 语料中 **0 命中** ⇒ **不列入支持计划**, 此处的 `MlirFail` 只是兜底,不是待补齐缺口。) -**多结果内联汇编 + `extractvalue`**(本轮补齐;TPV 全量产物 4163 份 `.ll` 的 +**多结果内联汇编 + `extractvalue`**(本轮补齐;VPC 全量产物 4163 份 `.ll` 的 "指令位置"扫描中 `extractvalue` **70 处**,且是**唯一**未支持却真实使用的 opcode) 真实形态(`Test/DefaultArgTest/output/*.ll` 等,多结果内联汇编是 `extractvalue` @@ -323,7 +323,7 @@ module { - 实测(mlir-opt / mlir-translate 22.1.8):`%v[0]` / `%v[1, 0]` / 具名结构体 `!llvm.struct<"n", (i32, i32)>` / 数组 `!llvm.array<2 x i32>` 作聚合类型均 exit 0; 降级得 `call { i32, i32 } asm sideeffect "…", "=r,r"()` + - `extractvalue { i32, i32 } %1, 0` / `, 1`(与 TPV 产物逐字同形)。 + `extractvalue { i32, i32 } %1, 0` / `, 1`(与 VPC 产物逐字同形)。 - `insertvalue` / `invoke` / `landingpad` / `resume` / `freeze` / `fence` / `shufflevector` / `insertelement` / `extractelement` / `callbr` 在 4163 份真实产物中 **全部 0 命中** ⇒ **不实现**,仍走"未知指令 opcode"响亮报错。 @@ -405,11 +405,11 @@ module { ### 2.10 具名结构体(`%"name" = type {…}`) -**背景(实测语料)**:TPV 对每个类都调 `module_add_named_type`(18 处调用点: +**背景(实测语料)**:VPC 对每个类都调 `module_add_named_type`(18 处调用点: `ClassGenDef.vp:522`、`ClassGenEnum.vp:720/1000`、`StructGenMixin.vp:1802`、 `AsyncGenMixin.vp:733`、`VpiReader.vp:2705/4059` 等),llvmlite 侧由 `PrintStructDefinition` 输出 `%"sha1.Cls" = type {…}` 定义行 -(`.tpv_build/**/*.ll` 里 `^%"[^"]+" = type` 命中 78319 行 / 1815 文件)。 +(`__vpcbuild__/**/*.ll` 里 `^%"[^"]+" = type` 命中 78319 行 / 1815 文件)。 **MLIR 侧两条硬约束(实测 mlir-opt 22.1.8)**: @@ -476,7 +476,7 @@ mlir-opt 报 `identified type already used with a different body`。故凡具名 | 名字为空 / 字段链成环(> 4096) | `命名结构体名字为空` / `命名结构体字段过多或字段链成环` | | 中间文本 `%"name"` 未登记(含"表未发布"的独立 `FunctionPrint`) | `命名结构体类型文本未在模块中登记完整定义(MLIR 不允许无体前向声明…)` | -**opaque 风险查证(结论:按值使用处不会出现 opaque)**:TPV 的 opaque 只由 +**opaque 风险查证(结论:按值使用处不会出现 opaque)**:VPC 的 opaque 只由 `_scan_type_for_cross_module_refs`(`Ptr(Struct(name))` 分支)与 REPL 专用的 `_repl_opaque_class_ty` 产生,两者都服务于**指针**引用;按值位置(C ABI 值形参) 另有显式判据 `_AggregateParamTy`:命中注册表且非 opaque 才按值、否则**降级 i64**; @@ -568,7 +568,7 @@ packed"的组合,无需(也无处)为其单独报错。登记表每槽 16 - **Debug Info**(`module_add_di_node` / `module_add_named_metadata` / `function_set_dbg` / `builder_set_(clear_)debug_location`、`DINode` / `DINamedMeta`、`LLVMModule.DIMeta*`/`DINamed*`、`Function.DbgRef`、 - `IRBuilder.DbgLoc`):为保持与 llvmlite **逐名同形**(并让 TransPyV 的 + `IRBuilder.DbgLoc`):为保持与 llvmlite **逐名同形**(并让 ViperC 的 `DebugInfoGen` 在 `llvmlite.`→`mlirlite.` 改名后可编译),本库**完整保留这些名字 与模型字段/布局**(`LLVMModule` 144B、`Function` 88B、`IRBuilder` 40B, 与 llvmlite 同布局);但 MLIR 语法没有 `!N = …` 元数据与 `, !dbg !N` 后缀, @@ -617,7 +617,7 @@ packed"的组合,无需(也无处)为其单独报错。登记表每槽 16 `()`。详见 §2.5 的硬约束 4/5。 4. ~~`c"hello\00"` → `"hello"`:按设计规范**去掉尾随 `\00`**~~ —— **与实测冲突,已按实测推翻**:MLIR 要求字符串属性长度**等于** i8 数组长度, - 而 TPV 的类型含尾随 NUL(`[N+1 x i8]`)⇒ 属性里**必须保留 `\00`** + 而 VPC 的类型含尾随 NUL(`[N+1 x i8]`)⇒ 属性里**必须保留 `\00`** (`_MlirCStringToMlir` 只剥 `c` 前缀,见 §2.5 硬约束 2)。 5. `Func(Void, …)` 的类型文本渲染为 `!llvm.func`(由 `Void → none` 规则 组合而来);函数签名位置的 void 返回值则是**省略** `-> ty`(不影响)。 @@ -644,9 +644,9 @@ packed"的组合,无需(也无处)为其单独报错。登记表每槽 16 10. `_mlir_read_type` 按 `[] {} () <>` 做**字符级**括号平衡来读 `!llvm.struct<…>`; 具名结构的名字是**带引号字符串**,含 `[`/`]`(泛型特化名如 `100000000000000a.GSList[100000000000000b.BasicBlock]`)时不影响平衡(成对出现)。 - 实测语料(全仓 1462 个 `.ll`/`.vpi`,26476 个唯一具名类型):TPV 名字**全部成对**、 - 无空格、无 `"`/`\`;仅 19 个非 TPV 名(手工 IR 里的 `<<.1` / `>>.10` 类移位临时名) - 含不配对 `<>` —— 它们不经 mlirlite(本库是**生成器**,只打印 TPV 模型)。 + 实测语料(全仓 1462 个 `.ll`/`.vpi`,26476 个唯一具名类型):VPC 名字**全部成对**、 + 无空格、无 `"`/`\`;仅 19 个非 VPC 名(手工 IR 里的 `<<.1` / `>>.10` 类移位临时名) + 含不配对 `<>` —— 它们不经 mlirlite(本库是**生成器**,只打印 VPC 模型)。 MLIR 侧对这些名字的承载已实测:`!llvm.struct<">>.1", (i32)>` exit 0, 降级得 `%">>.1" = type { i32 }`。 diff --git a/mlirlite/__builder.vp b/mlirlite/__builder.vp index df95ae4..489aa12 100644 --- a/mlirlite/__builder.vp +++ b/mlirlite/__builder.vp @@ -1,1928 +1,1928 @@ -from stdint import * -import string -import viperlib -import memhub -import stdio -import stdlib -from linkedlist import GSListNode, GSList -from .__types import LLVMType, TypePrint -from .__values import Value, new_value, SSAValue -from .__function import Function, BasicBlock, block_append_text, block_set_terminated, block_get_terminated, function_move_block_to_end, _ll_name_needs_quote, block_insert_text_before_terminator - -# ============================================================ -# IRBuilder: cursor-style IR instruction emitter -# -# Tracks the current BasicBlock and appends instruction text to the block's instruction list. -# Maintains an incrementing SSA name counter (%0, %1, %2, ...). -# -# Usage: -# builder = new_builder(pool, func) -# builder.PositionAtEnd(entry_block) -# a = builder.build_alloca(i32_ty) -# b = builder.build_alloca(i32_ty) -# builder.build_store(ConstInt(42), a) -# r = builder.build_load(i32_ty, a) -# builder.build_ret(r) -# -# Note: add novirtual to stay consistent with the hardcoded BUILDER_SIZE=40 -# (methods are implemented as instance methods, with no vtable; the same-named top-level function at the end of the file is a one-line delegate). -# ============================================================ -# Instruction text buffer sizing -# -# An MLIR-form type text **inlines the full field body of a named struct** -# (e.g. `!llvm.struct<"sha1.Cls", (i32, !llvm.ptr, ...)>`), so a single type can reach hundreds of bytes, -# far longer than the LLVM-form `%"sha1.Cls"` reference (a dozen-odd bytes). Therefore the instruction line buffer must be sized by -# "how many type texts can be embedded at most in one line": _LINE_CAP covers 5 type texts + the fixed part. -# The rendering side (TypePrint/MlirTypeText) already has an overflow guard: any truncation errors loudly instead of silently -# producing malformed MLIR (historical failure: the alloca line type was truncated by a 128-byte buffer -> mlir-opt reported -# "unbalanced '(' character in pretty dialect name"). -# ============================================================ -# Capacity of a single type text (types embedded by the builders are rendered to MLIR form by TypePrint). -# An inlined named struct expands its **full field body** at the use site, so a type text can reach hundreds of bytes (TransPyV -# class names / nesting are deeper) => 512 is too small and would trigger TypePrint's "type text exceeds the output buffer and is -# truncated" loud error. Raised to 2048; note that arg_cap is derived from this constant -# ((arg_count+1)*(_TY_TEXT_CAP+32)), so a too-high value makes a single multi-arg call allocation approach the module pool's per-block limit. -_TY_TEXT_CAP: sizet = 2048 -_LINE_CAP: sizet = 2560 - -novirtual class IRBuilder: - Pool: memhub.MemBuddy* # Memory pool - Func: Function* # Owning function - CurBlock: BasicBlock* # Current insertion point - Counter: int # SSA name counter - DbgLoc: str # current DILocation ref (kept same-shaped; MLIR printing ignores it, no !dbg suffix) - - # ============================================================ - # Internal helpers - # ============================================================ - def _alloc_ssa_name(self) -> str: - """Allocate an SSA name string "%N" and increment the counter""" - pool: memhub.MemBuddy* = self.Pool - buf: str = pool.alloc(16) - if buf is None: return None - viperlib.snprintf(buf, 16, "%%%d", self.Counter) - self.Counter += 1 - return buf - - - def _type_str(self, ty: LLVMType*) -> str: - """Get the MLIR text of a type (allocates a _TY_TEXT_CAP-byte buffer from pool). - MLIR type text inlines full struct bodies; overflow makes TypePrint fail-fast loudly. - """ - pool: memhub.MemBuddy* = self.Pool - buf: str = pool.alloc_tagged(_TY_TEXT_CAP, memhub.DETSITE_BUILDER_73) - if buf is None: return None - buf[0] = '\0' - TypePrint(buf, _TY_TEXT_CAP, ty, pool) - # [IR-FAIL] guard: LLVMType may be overwritten/dangling (pool reuse after reset), making the - # first char garbage/empty; fail-fast. Legal first chars: i/f/!/[/{/%/" and 'n'. - c0: int = buf[0] - if c0 == 0 or (c0 != 'i' and c0 != 'f' and c0 != '!' and c0 != 'n' and - c0 != '[' and c0 != '{' and c0 != '%' and c0 != '"'): - stdio.printf("[IR-FAIL] _type_str: corrupted LLVMType object ty=0x%llx first char=%d output='%s'\n", - uint64(ty), c0, buf) - stdlib.exit(1) - return None - return buf - - - def _emit(self, text: str): - """Append instruction text to the current block. Unlike llvmlite, MLIR has no ", !dbg !N" - suffix, so DbgLoc is deliberately ignored (set_debug_location only records the model).""" - if self is None or self.CurBlock is None: return - block_append_text(self.Pool, self.CurBlock, text) - - - def set_debug_location(self, ref: str): - """Set the current DILocation ref (kept same-shaped; MLIR only records it, no output effect). - ref=None clears it. - """ - if self is None: return - self.DbgLoc = ref - - def clear_debug_location(self): - """Clear the current DILocation ref (kept same-shaped; MLIR only records it)""" - if self is None: return - self.DbgLoc = None - - - def _mark_terminated(self): - """Mark the current block as terminated""" - if self is None or self.CurBlock is None: return - block_set_terminated(self.CurBlock, 1) - - - def _build_binop(self, op: str, - lhs: Value*, rhs: Value*) -> Value*: - """Shared implementation of binary operations""" - if self is None or lhs is None or rhs is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(lhs.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = %s %s %s, %s", name, op, ty_s, lhs.Name, rhs.Name) - self._emit(line) - return SSAValue(pool, lhs.Ty, name) - - - # ============================================================ - # Block positioning - # ============================================================ - def position_at_end(self, block: BasicBlock*): - """Set the insertion point to the end of block and move block to the end of the - function's block list, keeping text order consistent with control-flow order (else - non-monotonic SSA names break llc).""" - if self is None: return - self.CurBlock = block - if self.Func is not None: - function_move_block_to_end(self.Func, block) - - - def builder_cur_block_is_terminated(self) -> int: - """Check the current block's IsTerminated flag via block_get_terminated instead of the - chained self.CurBlock.IsTerminated, which silently skips the GEP when BasicBlock is opaque.""" - if self is None or self.CurBlock is None: - return 1 - return block_get_terminated(self.CurBlock) - - - # ============================================================ - # Memory instructions - # ============================================================ - def build_alloca(self, ty: LLVMType*) -> Value*: - """%N = alloca - - Returns an SSA value of type ty* (default alignment, backend-decided). - No default parameter: TPC parallel compilation does not pre-register include defaults - (argN fallback); for explicit alignment use build_alloca_aligned. - """ - if self is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = alloca %s", name, ty_s) - self._emit(line) - - # result type is ty* - result_ty: LLVMType* = LLVMType.Ptr(ty) - result_ty_ptr: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if result_ty_ptr is not None: - string.memset(result_ty_ptr, 0, LLVMType.__sizeof__()) - *result_ty_ptr = *result_ty - return SSAValue(pool, result_ty_ptr, name) - - - def build_alloca_aligned(self, ty: LLVMType*, - align: int) -> Value*: - """%N = alloca , align N. Returns an SSA value of type ty* with explicit alignment - (e.g. va_list 24-byte buffer needs 16). align has no default; callers must pass it.""" - if self is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = alloca %s, align %d", name, ty_s, align) - self._emit(line) - - # result type is ty* - result_ty: LLVMType* = LLVMType.Ptr(ty) - result_ty_ptr: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if result_ty_ptr is not None: - string.memset(result_ty_ptr, 0, LLVMType.__sizeof__()) - *result_ty_ptr = *result_ty - return SSAValue(pool, result_ty_ptr, name) - - - def build_alloca_at_entry(self, ty: LLVMType*) -> Value*: - """%N = alloca , emitted in the function entry block to ensure dominance. - Inserts before the entry terminator; alloca inside a conditional branch violates dominance.""" - if self is None: return None - pool: memhub.MemBuddy* = self.Pool - func: Function* = self.Func - if func is None or func.Blocks is None: - return self.build_alloca(ty) - blocks: GSList[BasicBlock]* = func.Blocks - entry_blk: BasicBlock* = blocks.Head - if entry_blk is None: - return self.build_alloca(ty) - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(ty) - line_text: str = pool.alloc(_LINE_CAP) - if line_text is None: return None - viperlib.snprintf(line_text, _LINE_CAP, "%s = alloca %s", name, ty_s) - # Insert before the entry terminator: if the entry block already ends in br/ret, appending - # would put alloca after the terminator (invalid IR); insert-before-terminator handles both. - block_insert_text_before_terminator(pool, entry_blk, line_text) - # result type is ty* - result_ty: LLVMType* = LLVMType.Ptr(ty) - result_ty_ptr: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if result_ty_ptr is not None: - string.memset(result_ty_ptr, 0, LLVMType.__sizeof__()) - *result_ty_ptr = *result_ty - return SSAValue(pool, result_ty_ptr, name) - - - def build_load(self, ty: LLVMType*, - ptr: Value*, - is_volatile: int = 0) -> Value*: - """%N = load [volatile ], - is_volatile=0 (default) emits the plain form; =1 adds the volatile qualifier so the - optimizer cannot hoist/merge the access (e.g. spinlocks).""" - if self is None or ptr is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(ty) - ptr_ty_s: str = self._type_str(ptr.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - if is_volatile != 0: - viperlib.snprintf(line, _LINE_CAP, "%s = load volatile %s, %s %s", name, ty_s, ptr_ty_s, ptr.Name) - else: - viperlib.snprintf(line, _LINE_CAP, "%s = load %s, %s %s", name, ty_s, ptr_ty_s, ptr.Name) - self._emit(line) - return SSAValue(pool, ty, name) - - - def build_store(self, val: Value*, ptr: Value*, - is_volatile: int = 0): - """store [volatile ] , - is_volatile=0 (default) emits the plain form; =1 adds the volatile qualifier - (`store volatile i32 %v, i32* %p`).""" - if self is None or val is None or ptr is None: return - pool: memhub.MemBuddy* = self.Pool - val_ty_s: str = self._type_str(val.Ty) - ptr_ty_s: str = self._type_str(ptr.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return - if is_volatile != 0: - viperlib.snprintf(line, _LINE_CAP, "store volatile %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) - else: - viperlib.snprintf(line, _LINE_CAP, "store %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) - self._emit(line) - - - def build_store_at_entry(self, val: Value*, ptr: Value*, - is_volatile: int = 0): - """store [volatile ] , , emitted in the **entry block** - (inserted before the entry terminator) so the write dominates every path. - - Used to zero/null-initialize a local slot at function entry: a slot that is not - assigned on some path must still hold a safe value, e.g. the M3 owned-slife guard — - the exit-time `free(load slot)` must be `free(NULL)` (a no-op) instead of freeing - uninitialized stack garbage. Mirrors build_alloca_at_entry's insertion strategy. - """ - if self is None or val is None or ptr is None: return - pool: memhub.MemBuddy* = self.Pool - func: Function* = self.Func - if func is None or func.Blocks is None: - self.build_store(val, ptr, is_volatile) - return - blocks: GSList[BasicBlock]* = func.Blocks - entry_blk: BasicBlock* = blocks.Head - if entry_blk is None: - self.build_store(val, ptr, is_volatile) - return - val_ty_s: str = self._type_str(val.Ty) - ptr_ty_s: str = self._type_str(ptr.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return - if is_volatile != 0: - viperlib.snprintf(line, _LINE_CAP, "store volatile %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) - else: - viperlib.snprintf(line, _LINE_CAP, "store %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) - block_insert_text_before_terminator(pool, entry_blk, line) - - - # ============================================================ - # Binary operation instructions - # ============================================================ - def build_add(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = add , """ - return self._build_binop("add", lhs, rhs) - - - def build_sub(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = sub , """ - return self._build_binop("sub", lhs, rhs) - - - def build_mul(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = mul , """ - return self._build_binop("mul", lhs, rhs) - - - def build_sdiv(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = sdiv , """ - return self._build_binop("sdiv", lhs, rhs) - - - def build_udiv(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = udiv , """ - return self._build_binop("udiv", lhs, rhs) - - - def build_srem(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = srem , """ - return self._build_binop("srem", lhs, rhs) - - - def build_urem(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = urem , """ - return self._build_binop("urem", lhs, rhs) - - - def build_and(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = and , """ - return self._build_binop("and", lhs, rhs) - - - def build_or(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = or , """ - return self._build_binop("or", lhs, rhs) - - - def build_xor(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = xor , """ - return self._build_binop("xor", lhs, rhs) - - - def build_shl(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = shl , """ - return self._build_binop("shl", lhs, rhs) - - - def build_lshr(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = lshr , """ - return self._build_binop("lshr", lhs, rhs) - - - def build_ashr(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = ashr , """ - return self._build_binop("ashr", lhs, rhs) - - - # ============================================================ - # Float binary operation instructions - # ============================================================ - def build_fadd(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fadd , """ - return self._build_binop("fadd", lhs, rhs) - - - def build_fsub(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fsub , """ - return self._build_binop("fsub", lhs, rhs) - - - def build_fmul(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fmul , """ - return self._build_binop("fmul", lhs, rhs) - - - def build_fdiv(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fdiv , """ - return self._build_binop("fdiv", lhs, rhs) - - - def build_frem(self, lhs: Value*, - rhs: Value*) -> Value*: - """%N = frem , """ - return self._build_binop("frem", lhs, rhs) - - - def build_fneg(self, val: Value*) -> Value*: - """%N = fneg """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(val.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = fneg %s %s", name, ty_s, val.Name) - self._emit(line) - return SSAValue(pool, val.Ty, name) - - - # ============================================================ - # Comparison instructions - # ============================================================ - def build_icmp(self, pred: int, - lhs: Value*, rhs: Value*) -> Value*: - """%N = icmp , - Returns an SSA value of type i1.""" - if self is None or lhs is None or rhs is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(lhs.Ty) - pred_s: str = _icmp_pred_str(pred) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = icmp %s %s %s, %s", name, pred_s, ty_s, lhs.Name, rhs.Name) - self._emit(line) - - # result type is i1 - i1_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if i1_ty is not None: - string.memset(i1_ty, 0, LLVMType.__sizeof__()) - *i1_ty = *LLVMType.Int(1) - return SSAValue(pool, i1_ty, name) - - - def build_fcmp(self, pred: int, - lhs: Value*, rhs: Value*) -> Value*: - """%N = fcmp , - Returns an SSA value of type i1.""" - if self is None or lhs is None or rhs is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(lhs.Ty) - pred_s: str = _fcmp_pred_str(pred) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = fcmp %s %s %s, %s", name, pred_s, ty_s, lhs.Name, rhs.Name) - self._emit(line) - - # result type is i1 - i1_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if i1_ty is not None: - string.memset(i1_ty, 0, LLVMType.__sizeof__()) - *i1_ty = *LLVMType.Int(1) - return SSAValue(pool, i1_ty, name) - - - # ============================================================ - # Terminator instructions - # ============================================================ - def build_br(self, target: BasicBlock*): - """br label %""" - if self is None or target is None: return - pool: memhub.MemBuddy* = self.Pool - line: str = pool.alloc(64) - if line is None: return - viperlib.snprintf(line, 64, "br label %%%s", target.Name) - self._emit(line) - self._mark_terminated() - - - def build_cond_br(self, cond: Value*, - then_blk: BasicBlock*, else_blk: BasicBlock*): - """br i1 , label %, label %""" - if self is None or cond is None or then_blk is None or else_blk is None: return - pool: memhub.MemBuddy* = self.Pool - line: str = pool.alloc(_LINE_CAP) - if line is None: return - viperlib.snprintf(line, _LINE_CAP, "br i1 %s, label %%%s, label %%%s", - cond.Name, then_blk.Name, else_blk.Name) - self._emit(line) - self._mark_terminated() - - - def build_ret(self, val: Value*): - """ret """ - if self is None or val is None: return - pool: memhub.MemBuddy* = self.Pool - ty_s: str = self._type_str(val.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return - viperlib.snprintf(line, _LINE_CAP, "ret %s %s", ty_s, val.Name) - self._emit(line) - self._mark_terminated() - - - def build_ret_void(self): - """ret void""" - if self is None: return - pool: memhub.MemBuddy* = self.Pool - line: str = pool.alloc(16) - if line is None: return - string.strcpy(line, "ret void") - self._emit(line) - self._mark_terminated() - - - # ============================================================ - # Call instructions - # ============================================================ - def build_call(self, callee: str, - args: Value*, arg_count: int, - ret_ty: LLVMType*, - is_variadic: int = 0) -> Value*: - """%N = call @() - args is the Value list head, arg_count the argument count. If ret_ty is void no SSA name is - allocated (plain call); is_variadic=1 emits the function type signature (e.g. i32 (i8*, ...)).""" - if self is None: return None - pool: memhub.MemBuddy* = self.Pool - - # --traceback: record this direct call's call site (build time; no-op unless enabled and - # inside a DebugInfoGen define context). See the debug call-site section in __function.vp. - _DebugNoteCall(callee) - - # check whether the return type is void - is_void: int = 0 - match ret_ty: - case LLVMType.Void(): - is_void = 1 - case _: - is_void = 0 - - name: str = None - if is_void == 0: - name = self._alloc_ssa_name() - - ret_ty_s: str = self._type_str(ret_ty) - - # Build the argument list text + variadic function type signature; size by argument count - # (struct type text can be _TY_TEXT_CAP each; a fixed 1024 would silently truncate). - arg_cap: sizet = sizet(arg_count + 1) * (_TY_TEXT_CAP + 32) + 32 - args_buf: str = pool.alloc_tagged(arg_cap, memhub.DETSITE_BUILDER_527) - if args_buf is None: return None - args_buf[0] = '\0' - func_ty_buf: str = pool.alloc_tagged(arg_cap, memhub.DETSITE_BUILDER_530) - if func_ty_buf is None: return None - func_ty_buf[0] = '\0' - cur: Value* = args - first: int = 1 - while cur is not None: - if first == 0: - _append_cstr(args_buf, arg_cap, ", ") - _append_cstr(func_ty_buf, arg_cap, ", ") - arg_ty_s: str = self._type_str(cur.Ty) - _append_cstr(args_buf, arg_cap, arg_ty_s) - _append_cstr(args_buf, arg_cap, " ") - if cur.Name is not None: - _append_cstr(args_buf, arg_cap, cur.Name) - _append_cstr(func_ty_buf, arg_cap, arg_ty_s) - cur = cur.Next - first = 0 - if is_variadic == 1: - if first == 0: - _append_cstr(func_ty_buf, arg_cap, ", ") - _append_cstr(func_ty_buf, arg_cap, "...") - - # Build the quoted callee name (SHA1-prefixed names with '.' or a leading digit need quotes) - callee_buf: str = pool.alloc(_LINE_CAP) - if callee_buf is None: return None - callee_buf[0] = '\0' - if _ll_name_needs_quote(callee) != 0: - _append_cstr(callee_buf, 128, "\"") - _append_cstr(callee_buf, 128, callee) - _append_cstr(callee_buf, 128, "\"") - else: - _append_cstr(callee_buf, 128, callee) - - line_cap: sizet = string.strlen(ret_ty_s) + string.strlen(args_buf) \ - + string.strlen(func_ty_buf) + string.strlen(callee_buf) + 64 - line: str = pool.alloc(line_cap) - if line is None: return None - if is_variadic == 1: - if is_void == 1: - viperlib.snprintf(line, line_cap, "call %s (%s) @%s(%s)", ret_ty_s, func_ty_buf, callee_buf, args_buf) - else: - viperlib.snprintf(line, line_cap, "%s = call %s (%s) @%s(%s)", name, ret_ty_s, func_ty_buf, callee_buf, args_buf) - else: - if is_void == 1: - viperlib.snprintf(line, line_cap, "call %s @%s(%s)", ret_ty_s, callee_buf, args_buf) - else: - viperlib.snprintf(line, line_cap, "%s = call %s @%s(%s)", name, ret_ty_s, callee_buf, args_buf) - self._emit(line) - - if is_void == 1: - return None - return SSAValue(pool, ret_ty, name) - - - # ============================================================ - # Indirect call instruction: call %callee() - # - # callee_val is a function-pointer SSA value (must already be a pointer type, e.g. i32(i8*)*) - # ============================================================ - def build_call_indirect(self, callee_val: Value*, - args: Value*, arg_count: int, - ret_ty: LLVMType*) -> Value*: - """%N = call %callee() - Indirect call: callee_val is a function-pointer SSA value.""" - if self is None or callee_val is None: return None - pool: memhub.MemBuddy* = self.Pool - - is_void: int = 0 - match ret_ty: - case LLVMType.Void(): - is_void = 1 - case _: - is_void = 0 - - name: str = None - if is_void == 0: - name = self._alloc_ssa_name() - - ret_ty_s: str = self._type_str(ret_ty) - - # Build the argument list text (sized by argument count, same reason as build_call) - arg_cap2: sizet = sizet(arg_count + 1) * (_TY_TEXT_CAP + 32) + 32 - args_buf: str = pool.alloc_tagged(arg_cap2, memhub.DETSITE_BUILDER_612) - if args_buf is None: return None - args_buf[0] = '\0' - cur: Value* = args - first: int = 1 - while cur is not None: - if first == 0: - _append_cstr(args_buf, arg_cap2, ", ") - arg_ty_s: str = self._type_str(cur.Ty) - _append_cstr(args_buf, arg_cap2, arg_ty_s) - _append_cstr(args_buf, arg_cap2, " ") - if cur.Name is not None: - _append_cstr(args_buf, arg_cap2, cur.Name) - cur = cur.Next - first = 0 - - line_cap2: sizet = string.strlen(ret_ty_s) + string.strlen(args_buf) \ - + string.strlen(callee_val.Name) + 64 - line: str = pool.alloc(line_cap2) - if line is None: return None - if is_void == 1: - viperlib.snprintf(line, line_cap2, "call %s %s(%s)", ret_ty_s, callee_val.Name, args_buf) - else: - viperlib.snprintf(line, line_cap2, "%s = call %s %s(%s)", name, ret_ty_s, callee_val.Name, args_buf) - self._emit(line) - - if is_void == 1: - return None - return SSAValue(pool, ret_ty, name) - - - # ============================================================ - # Type conversion instructions - # ============================================================ - def build_bitcast(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = bitcast to """ - if self is None or val is None: - return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - if val.Name is None: - return None - viperlib.snprintf(line, _LINE_CAP, "%s = bitcast %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - ret: Value* = SSAValue(pool, to_ty, name) - return ret - - - def build_sext(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = sext to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = sext %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_trunc(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = trunc to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = trunc %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_zext(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = zext to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = zext %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_ptrtoint(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = ptrtoint to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = ptrtoint %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_inttoptr(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = inttoptr to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = inttoptr %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - # ============================================================ - # Float conversion instructions - # ============================================================ - def build_fpext(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fpext to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = fpext %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_fptrunc(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fptrunc to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = fptrunc %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_fp2si(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fptosi to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = fptosi %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_si2fp(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = sitofp to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = sitofp %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_fp2ui(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fptoui to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = fptoui %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - def build_ui2fp(self, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = uitofp to """ - if self is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - from_ty_s: str = self._type_str(val.Ty) - to_ty_s: str = self._type_str(to_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = uitofp %s %s to %s", name, from_ty_s, val.Name, to_ty_s) - self._emit(line) - return SSAValue(pool, to_ty, name) - - - # ============================================================ - # _gep_index_zext - zero-extend narrow integer GEP indices - # - # LLVM sign-extends narrow GEP indices (i8 0xE2 = -30); Viper indices are non-negative, so all - # <32-bit indices are zext'd to i32. build_gep/build_gep_array handle this internally. - # ============================================================ - def _gep_index_zext(self, idx: Value*) -> Value*: - """Zero-extend a <32-bit integer index to i32; other types are returned unchanged""" - if self is None or idx is None or idx.Ty is None: - return idx - bits: int = 0 - match idx.Ty: - case LLVMType.Int(b): - bits = b - case _: - bits = 0 - if bits == 0 or bits >= 32: - return idx - i32_ty: LLVMType* = self.Pool.alloc(LLVMType.__sizeof__()) - if i32_ty is None: - return idx - string.memset(i32_ty, 0, LLVMType.__sizeof__()) - *i32_ty = *LLVMType.Int(32) - z: Value* = self.build_zext(idx, i32_ty) - return z if z is not None else idx - - - # ============================================================ - # GEP instructions - # ============================================================ - def build_gep(self, elem_ty: LLVMType*, - ptr: Value*, idx: Value*) -> Value*: - """%N = getelementptr , , """ - if self is None or ptr is None or idx is None: return None - # Defensive check: ptr.Ty must be a pointer type, else llc errors ("base of getelementptr - # must be a pointer") when translate_value returns a non-pointer (e.g. i32 constant 0). - if _is_ptr_type(ptr.Ty) == 0: - return None - idx = self._gep_index_zext(idx) - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - elem_ty_s: str = self._type_str(elem_ty) - ptr_ty_s: str = self._type_str(ptr.Ty) - idx_ty_s: str = self._type_str(idx.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = getelementptr %s, %s %s, %s %s", - name, elem_ty_s, ptr_ty_s, ptr.Name, idx_ty_s, idx.Name) - self._emit(line) - - # GEP result type is elem_ty* - result_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if result_ty is not None: - string.memset(result_ty, 0, LLVMType.__sizeof__()) - *result_ty = *LLVMType.Ptr(elem_ty) - return SSAValue(pool, result_ty, name) - - - # ============================================================ - # build_gep_array - two-index array GEP: getelementptr [N x ty], [N x ty]* %ptr, i32 0, i32 %idx - # ============================================================ - def build_gep_array(self, array_ty: LLVMType*, - elem_ty: LLVMType*, - ptr: Value*, idx: Value*) -> Value*: - """%N = getelementptr [N x elem_ty], [N x elem_ty]* %ptr, i32 0, i32 %idx - Result type is elem_ty*.""" - if self is None or ptr is None or idx is None: return None - idx = self._gep_index_zext(idx) - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - array_ty_s: str = self._type_str(array_ty) - ptr_ty_s: str = self._type_str(ptr.Ty) - idx_ty_s: str = self._type_str(idx.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = getelementptr %s, %s %s, i32 0, %s %s", - name, array_ty_s, ptr_ty_s, ptr.Name, idx_ty_s, idx.Name) - self._emit(line) - - # GEP result type is elem_ty* - result_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if result_ty is not None: - string.memset(result_ty, 0, LLVMType.__sizeof__()) - *result_ty = *LLVMType.Ptr(elem_ty) - return SSAValue(pool, result_ty, name) - - - # ============================================================ - # build_gep_struct - struct-field GEP: getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx - # ============================================================ - def build_gep_struct(self, struct_ty: LLVMType*, - field_ty: LLVMType*, - ptr: Value*, field_idx: int) -> Value*: - """%N = getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx - Result type is field_ty*.""" - if self is None or ptr is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - struct_ty_s: str = self._type_str(struct_ty) - ptr_ty_s: str = self._type_str(ptr.Ty) - line: str = pool.alloc(2048) - if line is None: - stdio.printf("[BGS] line alloc None\n") - return None - viperlib.snprintf(line, 2048, "%s = getelementptr %s, %s %s, i32 0, i32 %d", - name, struct_ty_s, ptr_ty_s, ptr.Name, field_idx) - self._emit(line) - - # GEP result type is field_ty* - result_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if result_ty is None: - stdio.printf("[BGS] result_ty alloc None\n") - else: - string.memset(result_ty, 0, LLVMType.__sizeof__()) - *result_ty = *LLVMType.Ptr(field_ty) - rv: Value* = SSAValue(pool, result_ty, name) - if rv is None: - stdio.printf("[BGS] SSAValue None\n") - return rv - - - # ============================================================ - # Phi instruction - # ============================================================ - def build_phi(self, ty: LLVMType*, - incoming_head: PhiIncoming*, count: int) -> Value*: - """%N = phi [val1, %blk1], [val2, %blk2], ... - incoming_head is the PhiIncoming list head; count is the incoming-edge count.""" - if self is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(ty) - - # Build the incoming list text [val, %blk], ...; there can be many edges (e.g. a 30+ or-chain), - # so a large buffer is required. - parts: str = pool.alloc(4096) - if parts is None: return None - parts[0] = '\0' - cur: PhiIncoming* = incoming_head - first: int = 1 - while cur is not None: - if first == 0: - _append_cstr(parts, 4096, ", ") - _append_cstr(parts, 4096, "[") - # use an intermediate var; chained struct member access would be dropped by the compiler - inc_val: Value* = cur.Val - if inc_val is not None: - inc_val_name: str = inc_val.Name - if inc_val_name is not None: - _append_cstr(parts, 4096, inc_val_name) - _append_cstr(parts, 4096, ", %") - inc_blk: BasicBlock* = cur.Block - if inc_blk is not None: - inc_blk_name: str = inc_blk.Name - if inc_blk_name is not None: - _append_cstr(parts, 4096, inc_blk_name) - _append_cstr(parts, 4096, "]") - cur = cur.Next - first = 0 - - line: str = pool.alloc(8192) - if line is None: return None - viperlib.snprintf(line, 8192, "%s = phi %s %s", name, ty_s, parts) - self._emit(line) - return SSAValue(pool, ty, name) - - - # ============================================================ - # Switch instruction - # ============================================================ - def build_switch(self, val: Value*, - default_blk: BasicBlock*, - cases_head: SwitchCase*, case_count: int): - """switch , label % [, label % ...]""" - if self is None or val is None or default_blk is None: return - pool: memhub.MemBuddy* = self.Pool - ty_s: str = self._type_str(val.Ty) - - # build body: switch , label %default - line: str = pool.alloc(_LINE_CAP) - if line is None: return - viperlib.snprintf(line, _LINE_CAP, "switch %s %s, label %%%s", - ty_s, val.Name, default_blk.Name) - _append_cstr(line, _LINE_CAP, " [") - _append_cstr(line, _LINE_CAP, "\n") - - # iterate the case list - cur: SwitchCase* = cases_head - case_ty_s: str = self._type_str(val.Ty) - while cur is not None: - case_line: str = pool.alloc(_LINE_CAP) - if case_line is not None: - # use an intermediate var; chained struct member access would be dropped by the compiler - cv: Value* = cur.CaseVal - cb: BasicBlock* = cur.Block - if cv is not None and cb is not None: - cv_name: str = cv.Name - cb_name: str = cb.Name - if cv_name is not None and cb_name is not None: - viperlib.snprintf(case_line, _LINE_CAP, " %s %s, label %%%s\n", - case_ty_s, cv_name, cb_name) - _append_cstr(line, _LINE_CAP, case_line) - cur = cur.Next - - _append_cstr(line, _LINE_CAP, "]") - self._emit(line) - self._mark_terminated() - - - # ============================================================ - # Select instruction - # ============================================================ - def build_select(self, cond: Value*, - true_val: Value*, false_val: Value*) -> Value*: - """%N = select i1 , , """ - if self is None or cond is None or true_val is None or false_val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ty_s: str = self._type_str(true_val.Ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = select i1 %s, %s %s, %s %s", - name, cond.Name, ty_s, true_val.Name, ty_s, false_val.Name) - self._emit(line) - return SSAValue(pool, true_val.Ty, name) - - - # ============================================================ - # Unreachable instruction - # ============================================================ - def build_unreachable(self): - """unreachable""" - if self is None: return - pool: memhub.MemBuddy* = self.Pool - line: str = pool.alloc(16) - if line is None: return - string.strcpy(line, "unreachable") - self._emit(line) - self._mark_terminated() - - - # ============================================================ - # Atomic instructions (Phase 3) - # ============================================================ - def build_atomicrmw(self, op: int, - ptr: Value*, val: Value*, - order: int) -> Value*: - """%N = atomicrmw , """ - if self is None or ptr is None or val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ptr_ty_s: str = self._type_str(ptr.Ty) - val_ty_s: str = self._type_str(val.Ty) - op_s: str = _atomic_op_str(op) - ord_s: str = _atomic_order_str(order) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = atomicrmw %s %s %s, %s %s %s", - name, op_s, ptr_ty_s, ptr.Name, val_ty_s, val.Name, ord_s) - self._emit(line) - return SSAValue(pool, val.Ty, name) - - - def build_cmpxchg(self, ptr: Value*, - cmp_val: Value*, new_val: Value*, - succ_order: int, fail_order: int) -> Value*: - """%N = cmpxchg , , - Returns a {, i1} SSA value (simplified to an i64 representing the pair).""" - if self is None or ptr is None or cmp_val is None or new_val is None: return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - ptr_ty_s: str = self._type_str(ptr.Ty) - cmp_ty_s: str = self._type_str(cmp_val.Ty) - new_ty_s: str = self._type_str(new_val.Ty) - succ_s: str = _atomic_order_str(succ_order) - fail_s: str = _atomic_order_str(fail_order) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - viperlib.snprintf(line, _LINE_CAP, "%s = cmpxchg %s %s, %s %s, %s %s %s %s", - name, ptr_ty_s, ptr.Name, cmp_ty_s, cmp_val.Name, - new_ty_s, new_val.Name, succ_s, fail_s) - self._emit(line) - # return the original value's type - return SSAValue(pool, new_val.Ty, name) - - - # ============================================================ - # va_arg instruction: read the next variadic argument from va_list - # ============================================================ - def build_va_arg(self, - va_list_ptr: Value*, - arg_ty: LLVMType*) -> Value*: - """%N = va_arg , """ - if self is None or va_list_ptr is None or arg_ty is None: - return None - pool: memhub.MemBuddy* = self.Pool - name: str = self._alloc_ssa_name() - va_list_ty_s: str = self._type_str(va_list_ptr.Ty) - arg_ty_s: str = self._type_str(arg_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - if va_list_ptr.Name is None: - return None - viperlib.snprintf(line, _LINE_CAP, "%s = va_arg %s %s, %s", - name, va_list_ty_s, va_list_ptr.Name, arg_ty_s) - self._emit(line) - return SSAValue(pool, arg_ty, name) - - - # ============================================================ - # Inline assembly (Phase 3) - # ============================================================ - def build_inline_asm(self, ret_ty: LLVMType*, - asm_text: str, constraints: str, - has_side_effects: int) -> Value*: - """%N = call asm [sideeffect] "" ""() - Simplified inline-assembly call, no-argument version.""" - if self is None: return None - pool: memhub.MemBuddy* = self.Pool - - # check whether the return type is void - is_void: int = 0 - match ret_ty: - case LLVMType.Void(): - is_void = 1 - case _: - is_void = 0 - - name: str = None - if is_void == 0: - name = self._alloc_ssa_name() - - ret_ty_s: str = self._type_str(ret_ty) - line: str = pool.alloc(_LINE_CAP) - if line is None: return None - - if has_side_effects != 0: - if is_void == 1: - viperlib.snprintf(line, _LINE_CAP, "call %s asm sideeffect \"%s\", \"%s\"()", - ret_ty_s, asm_text, constraints) - else: - viperlib.snprintf(line, _LINE_CAP, "%s = call %s asm sideeffect \"%s\", \"%s\"()", - name, ret_ty_s, asm_text, constraints) - else: - if is_void == 1: - viperlib.snprintf(line, _LINE_CAP, "call %s asm \"%s\", \"%s\"()", - ret_ty_s, asm_text, constraints) - else: - viperlib.snprintf(line, _LINE_CAP, "%s = call %s asm \"%s\", \"%s\"()", - name, ret_ty_s, asm_text, constraints) - self._emit(line) - - if is_void == 1: - return None - return SSAValue(pool, ret_ty, name) - - -# ============================================================ -# Factory functions -# ============================================================ -# IRBuilder hardcoded size: Pool(8) + Func(8) + CurBlock(8) + Counter(4) + DbgLoc(8) = 40 bytes (same layout as llvmlite) -define BUILDER_SIZE = 40 - -def new_builder(pool: memhub.MemBuddy*, func: Function*) -> IRBuilder*: - """Create an IRBuilder associated with function func.""" - ptr: IRBuilder* = pool.alloc(BUILDER_SIZE) - if ptr is None: return None - string.memset(ptr, 0, BUILDER_SIZE) - ptr.Pool = pool - ptr.Func = func - ptr.CurBlock = None - ptr.Counter = 0 - ptr.DbgLoc = None - return ptr - - -# ============================================================ -# Block positioning -# ============================================================ -def position_at_end(builder: IRBuilder*, block: BasicBlock*): - """Set the insertion point to the end of block and move block to the end of the - function's block list, keeping text order consistent with control-flow order (else - non-monotonic SSA names break llc).""" - builder.position_at_end(block) - - -# ============================================================ -# Internal helpers -# ============================================================ -def _alloc_ssa_name(builder: IRBuilder*) -> str: - """Allocate an SSA name string "%N" and increment the counter""" - return builder._alloc_ssa_name() - - -def _type_str(builder: IRBuilder*, ty: LLVMType*) -> str: - """Get the IR text of a type (allocates a buffer from pool). Complex struct types (e.g. a - 13-method vtable) can exceed 64 bytes; too small a buffer truncates '}' and breaks the IR.""" - return builder._type_str(ty) - - -def _emit(builder: IRBuilder*, text: str): - """Append instruction text to the current block.""" - builder._emit(text) - - -def _mark_terminated(builder: IRBuilder*): - """Mark the current block as terminated""" - builder._mark_terminated() - - -def builder_cur_block_is_terminated(builder: IRBuilder*) -> int: - """Check the current block's IsTerminated flag via block_get_terminated instead of the - chained builder.CurBlock.IsTerminated, which silently skips the GEP when BasicBlock is opaque.""" - return builder.builder_cur_block_is_terminated() - - -# ============================================================ -# Debug location (kept same-shaped; ignored by MLIR printing) -# ============================================================ -def builder_set_debug_location(builder: IRBuilder*, ref: str): - """Set the current DILocation ref (kept same-shaped; MLIR only records it, no output effect)""" - builder.set_debug_location(ref) - - -def builder_clear_debug_location(builder: IRBuilder*): - """Clear the current DILocation ref (kept same-shaped; MLIR only records it)""" - builder.clear_debug_location() - - -# ============================================================ -# Memory instructions -# ============================================================ -def build_alloca(builder: IRBuilder*, ty: LLVMType*) -> Value*: - """%N = alloca - - Returns an SSA value of type ty* (default alignment, backend-decided). - No default parameter: TPC parallel compilation does not pre-register include defaults - (argN fallback); for explicit alignment use build_alloca_aligned. - """ - return builder.build_alloca(ty) - - -def build_alloca_aligned(builder: IRBuilder*, ty: LLVMType*, - align: int) -> Value*: - """%N = alloca , align N. Returns an SSA value of type ty* with explicit alignment - (e.g. va_list 24-byte buffer needs 16). align has no default; callers must pass it.""" - return builder.build_alloca_aligned(ty, align) - - -def build_alloca_at_entry(builder: IRBuilder*, ty: LLVMType*) -> Value*: - """%N = alloca , emitted in the function entry block to ensure dominance. - Inserts before the entry terminator; alloca inside a conditional branch violates dominance.""" - return builder.build_alloca_at_entry(ty) - - -def build_load(builder: IRBuilder*, ty: LLVMType*, - ptr: Value*, - is_volatile: int = 0) -> Value*: - """%N = load [volatile ], """ - return builder.build_load(ty, ptr, is_volatile) - - -def build_store(builder: IRBuilder*, val: Value*, ptr: Value*, - is_volatile: int = 0): - """store [volatile ] , """ - builder.build_store(val, ptr, is_volatile) - - -def build_store_at_entry(builder: IRBuilder*, val: Value*, ptr: Value*, - is_volatile: int = 0): - """store [volatile ] , emitted in the entry block - (before the entry terminator) so the write dominates every path; used for - null-initializing a local slot at function entry (M3 owned-slife guard).""" - builder.build_store_at_entry(val, ptr, is_volatile) - - -# ============================================================ -# Binary operation instructions -# ============================================================ -def build_add(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = add , """ - return builder.build_add(lhs, rhs) - - -def build_sub(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = sub , """ - return builder.build_sub(lhs, rhs) - - -def build_mul(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = mul , """ - return builder.build_mul(lhs, rhs) - - -def build_sdiv(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = sdiv , """ - return builder.build_sdiv(lhs, rhs) - - -def build_udiv(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = udiv , """ - return builder.build_udiv(lhs, rhs) - - -def build_srem(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = srem , """ - return builder.build_srem(lhs, rhs) - - -def build_urem(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = urem , """ - return builder.build_urem(lhs, rhs) - - -def build_and(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = and , """ - return builder.build_and(lhs, rhs) - - -def build_or(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = or , """ - return builder.build_or(lhs, rhs) - - -def build_xor(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = xor , """ - return builder.build_xor(lhs, rhs) - - -def build_shl(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = shl , """ - return builder.build_shl(lhs, rhs) - - -def build_lshr(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = lshr , """ - return builder.build_lshr(lhs, rhs) - - -def build_ashr(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = ashr , """ - return builder.build_ashr(lhs, rhs) - - -# ============================================================ -# Float binary operation instructions -# ============================================================ -def build_fadd(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fadd , """ - return builder.build_fadd(lhs, rhs) - - -def build_fsub(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fsub , """ - return builder.build_fsub(lhs, rhs) - - -def build_fmul(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fmul , """ - return builder.build_fmul(lhs, rhs) - - -def build_fdiv(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = fdiv , """ - return builder.build_fdiv(lhs, rhs) - - -def build_frem(builder: IRBuilder*, lhs: Value*, - rhs: Value*) -> Value*: - """%N = frem , """ - return builder.build_frem(lhs, rhs) - - -def build_fneg(builder: IRBuilder*, val: Value*) -> Value*: - """%N = fneg """ - return builder.build_fneg(val) - - -def _build_binop(builder: IRBuilder*, op: str, - lhs: Value*, rhs: Value*) -> Value*: - """Shared implementation of binary operations""" - return builder._build_binop(op, lhs, rhs) - - -# ============================================================ -# Comparison instructions -# ============================================================ -# ICMP predicate constants -define ICMP_EQ = 0 -define ICMP_NE = 1 -define ICMP_SGT = 2 -define ICMP_SGE = 3 -define ICMP_SLT = 4 -define ICMP_SLE = 5 -define ICMP_UGT = 6 -define ICMP_UGE = 7 -define ICMP_ULT = 8 -define ICMP_ULE = 9 - -# FCMP predicate constants -define FCMP_FALSE = 0 -define FCMP_OEQ = 1 -define FCMP_OGT = 2 -define FCMP_OGE = 3 -define FCMP_OLT = 4 -define FCMP_OLE = 5 -define FCMP_ONE = 6 -define FCMP_ORD = 7 -define FCMP_UNO = 8 -define FCMP_UEQ = 9 -define FCMP_UGT = 10 -define FCMP_UGE = 11 -define FCMP_ULT = 12 -define FCMP_ULE = 13 -define FCMP_UNE = 14 -define FCMP_TRUE = 15 - - -def _icmp_pred_str(pred: int) -> str: - if pred == ICMP_EQ: return "eq" - if pred == ICMP_NE: return "ne" - if pred == ICMP_SGT: return "sgt" - if pred == ICMP_SGE: return "sge" - if pred == ICMP_SLT: return "slt" - if pred == ICMP_SLE: return "sle" - if pred == ICMP_UGT: return "ugt" - if pred == ICMP_UGE: return "uge" - if pred == ICMP_ULT: return "ult" - if pred == ICMP_ULE: return "ule" - return "eq" - - -def _fcmp_pred_str(pred: int) -> str: - if pred == FCMP_FALSE: return "false" - if pred == FCMP_OEQ: return "oeq" - if pred == FCMP_OGT: return "ogt" - if pred == FCMP_OGE: return "oge" - if pred == FCMP_OLT: return "olt" - if pred == FCMP_OLE: return "ole" - if pred == FCMP_ONE: return "one" - if pred == FCMP_ORD: return "ord" - if pred == FCMP_UNO: return "uno" - if pred == FCMP_UEQ: return "ueq" - if pred == FCMP_UGT: return "ugt" - if pred == FCMP_UGE: return "uge" - if pred == FCMP_ULT: return "ult" - if pred == FCMP_ULE: return "ule" - if pred == FCMP_UNE: return "une" - if pred == FCMP_TRUE: return "true" - return "oeq" - - -def build_icmp(builder: IRBuilder*, pred: int, - lhs: Value*, rhs: Value*) -> Value*: - """%N = icmp , - Returns an SSA value of type i1.""" - return builder.build_icmp(pred, lhs, rhs) - - -def build_fcmp(builder: IRBuilder*, pred: int, - lhs: Value*, rhs: Value*) -> Value*: - """%N = fcmp , - Returns an SSA value of type i1.""" - return builder.build_fcmp(pred, lhs, rhs) - - -# ============================================================ -# Terminator instructions -# ============================================================ -def build_br(builder: IRBuilder*, target: BasicBlock*): - """br label %""" - builder.build_br(target) - - -def build_cond_br(builder: IRBuilder*, cond: Value*, - then_blk: BasicBlock*, else_blk: BasicBlock*): - """br i1 , label %, label %""" - builder.build_cond_br(cond, then_blk, else_blk) - - -def build_ret(builder: IRBuilder*, val: Value*): - """ret """ - builder.build_ret(val) - - -def build_ret_void(builder: IRBuilder*): - """ret void""" - builder.build_ret_void() - - -# ============================================================ -# Call instructions -# ============================================================ -def build_call(builder: IRBuilder*, callee: str, - args: Value*, arg_count: int, - ret_ty: LLVMType*, - is_variadic: int = 0) -> Value*: - """%N = call @() - args is the Value list head, arg_count the argument count. If ret_ty is void no SSA name is - allocated (plain call); is_variadic=1 emits the function type signature (e.g. i32 (i8*, ...)).""" - return builder.build_call(callee, args, arg_count, ret_ty, is_variadic) - - -# ============================================================ -# Indirect call instruction: call %callee() -# -# callee_val is a function-pointer SSA value (must already be a pointer type, e.g. i32(i8*)*) -# ============================================================ -def build_call_indirect(builder: IRBuilder*, callee_val: Value*, - args: Value*, arg_count: int, - ret_ty: LLVMType*) -> Value*: - """%N = call %callee() - Indirect call: callee_val is a function-pointer SSA value.""" - return builder.build_call_indirect(callee_val, args, arg_count, ret_ty) - - -# ============================================================ -# Type conversion instructions -# ============================================================ -def build_bitcast(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = bitcast to """ - return builder.build_bitcast(val, to_ty) - - -def build_sext(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = sext to """ - return builder.build_sext(val, to_ty) - - -def build_trunc(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = trunc to """ - return builder.build_trunc(val, to_ty) - - -def build_zext(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = zext to """ - return builder.build_zext(val, to_ty) - - -def build_ptrtoint(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = ptrtoint to """ - return builder.build_ptrtoint(val, to_ty) - - -def build_inttoptr(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = inttoptr to """ - return builder.build_inttoptr(val, to_ty) - - -# ============================================================ -# Float conversion instructions -# ============================================================ -def build_fpext(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fpext to """ - return builder.build_fpext(val, to_ty) - - -def build_fptrunc(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fptrunc to """ - return builder.build_fptrunc(val, to_ty) - - -def build_fp2si(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fptosi to """ - return builder.build_fp2si(val, to_ty) - - -def build_si2fp(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = sitofp to """ - return builder.build_si2fp(val, to_ty) - - -def build_fp2ui(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = fptoui to """ - return builder.build_fp2ui(val, to_ty) - - -def build_ui2fp(builder: IRBuilder*, val: Value*, - to_ty: LLVMType*) -> Value*: - """%N = uitofp to """ - return builder.build_ui2fp(val, to_ty) - - -# ============================================================ -# GEP instructions -# ============================================================ -def _is_ptr_type(ty: LLVMType*) -> int: - """Check whether ty is a Ptr type (standalone function to avoid the nested-match compiler bug)""" - if ty is None: - return 0 - match ty: - case LLVMType.Ptr(pointee): - return 1 - case _: - return 0 - - -def build_gep(builder: IRBuilder*, elem_ty: LLVMType*, - ptr: Value*, idx: Value*) -> Value*: - """%N = getelementptr , , """ - return builder.build_gep(elem_ty, ptr, idx) - - -# ============================================================ -# build_gep_array - two-index array GEP: getelementptr [N x ty], [N x ty]* %ptr, i32 0, i32 %idx -# ============================================================ -def build_gep_array(builder: IRBuilder*, array_ty: LLVMType*, - elem_ty: LLVMType*, - ptr: Value*, idx: Value*) -> Value*: - """%N = getelementptr [N x elem_ty], [N x elem_ty]* %ptr, i32 0, i32 %idx - Result type is elem_ty*.""" - return builder.build_gep_array(array_ty, elem_ty, ptr, idx) - - -# ============================================================ -# build_gep_struct - struct-field GEP: getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx -# ============================================================ -def build_gep_struct(builder: IRBuilder*, struct_ty: LLVMType*, - field_ty: LLVMType*, - ptr: Value*, field_idx: int) -> Value*: - """%N = getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx - Result type is field_ty*.""" - return builder.build_gep_struct(struct_ty, field_ty, ptr, field_idx) - - -# ============================================================ -# PhiIncoming: a phi incoming edge (value, block) pair -# inherit GSListNode[PhiIncoming] for a strongly-typed Next -# -# Note: novirtual is required (rationale in the Line comment in __function.py). -# ============================================================ -novirtual class PhiIncoming(GSListNode[PhiIncoming]): - Val: Value* - Block: BasicBlock* - - -# PhiIncoming hardcoded size: Next(8) + Val(8) + Block(8) = 24 bytes -define PHIINC_SIZE = 24 - -def new_phi_incoming(pool: memhub.MemBuddy*, - val: Value*, block: BasicBlock*) -> PhiIncoming*: - ptr: PhiIncoming* = pool.alloc(PHIINC_SIZE) - if ptr is None: return None - string.memset(ptr, 0, PHIINC_SIZE) - ptr.Val = val - ptr.Block = block - return ptr - - -# ============================================================ -# Phi instruction -# ============================================================ -def build_phi(builder: IRBuilder*, ty: LLVMType*, - incoming_head: PhiIncoming*, count: int) -> Value*: - """%N = phi [val1, %blk1], [val2, %blk2], ... - incoming_head is the PhiIncoming list head; count is the incoming-edge count.""" - return builder.build_phi(ty, incoming_head, count) - - -# ============================================================ -# SwitchCase: a case branch of the switch instruction -# -# Note: novirtual is required (rationale in the Line comment in __function.py). -# ============================================================ -novirtual class SwitchCase(GSListNode[SwitchCase]): - CaseVal: Value* - Block: BasicBlock* - - -# SwitchCase hardcoded size: Next(8) + CaseVal(8) + Block(8) = 24 bytes -define SWCASE_SIZE = 24 - -def new_switch_case(pool: memhub.MemBuddy*, - case_val: Value*, block: BasicBlock*) -> SwitchCase*: - ptr: SwitchCase* = pool.alloc(SWCASE_SIZE) - if ptr is None: return None - string.memset(ptr, 0, SWCASE_SIZE) - ptr.CaseVal = case_val - ptr.Block = block - return ptr - - -# ============================================================ -# Switch instruction -# ============================================================ -def build_switch(builder: IRBuilder*, val: Value*, - default_blk: BasicBlock*, - cases_head: SwitchCase*, case_count: int): - """switch , label % [, label % ...]""" - builder.build_switch(val, default_blk, cases_head, case_count) - - -# ============================================================ -# Select instruction -# ============================================================ -def build_select(builder: IRBuilder*, cond: Value*, - true_val: Value*, false_val: Value*) -> Value*: - """%N = select i1 , , """ - return builder.build_select(cond, true_val, false_val) - - -# ============================================================ -# Unreachable instruction -# ============================================================ -def build_unreachable(builder: IRBuilder*): - """unreachable""" - builder.build_unreachable() - - -# ============================================================ -# Atomic instructions (Phase 3) -# ============================================================ -# AtomicRMW operation constants -define ATOMIC_XCHG = 0 -define ATOMIC_ADD = 1 -define ATOMIC_SUB = 2 -define ATOMIC_AND = 3 -define ATOMIC_NAND = 4 -define ATOMIC_OR = 5 -define ATOMIC_XOR = 6 -define ATOMIC_MAX = 7 -define ATOMIC_MIN = 8 -define ATOMIC_UMAX = 9 -define ATOMIC_UMIN = 10 - -# Memory ordering constants -define ATOMIC_ORDER_NOTATOMIC = 0 -define ATOMIC_ORDER_UNORDERED = 1 -define ATOMIC_ORDER_MONOTONIC = 2 -define ATOMIC_ORDER_ACQUIRE = 3 -define ATOMIC_ORDER_RELEASE = 4 -define ATOMIC_ORDER_ACQ_REL = 5 -define ATOMIC_ORDER_SEQ_CST = 6 - - -def _atomic_op_str(op: int) -> str: - if op == ATOMIC_XCHG: return "xchg" - if op == ATOMIC_ADD: return "add" - if op == ATOMIC_SUB: return "sub" - if op == ATOMIC_AND: return "and" - if op == ATOMIC_NAND: return "nand" - if op == ATOMIC_OR: return "or" - if op == ATOMIC_XOR: return "xor" - if op == ATOMIC_MAX: return "max" - if op == ATOMIC_MIN: return "min" - if op == ATOMIC_UMAX: return "umax" - if op == ATOMIC_UMIN: return "umin" - return "xchg" - - -def _atomic_order_str(ord: int) -> str: - if ord == ATOMIC_ORDER_UNORDERED: return "unordered" - if ord == ATOMIC_ORDER_MONOTONIC: return "monotonic" - if ord == ATOMIC_ORDER_ACQUIRE: return "acquire" - if ord == ATOMIC_ORDER_RELEASE: return "release" - if ord == ATOMIC_ORDER_ACQ_REL: return "acq_rel" - if ord == ATOMIC_ORDER_SEQ_CST: return "seq_cst" - return "monotonic" - - -def build_atomicrmw(builder: IRBuilder*, op: int, - ptr: Value*, val: Value*, - order: int) -> Value*: - """%N = atomicrmw , """ - return builder.build_atomicrmw(op, ptr, val, order) - - -def build_cmpxchg(builder: IRBuilder*, ptr: Value*, - cmp_val: Value*, new_val: Value*, - succ_order: int, fail_order: int) -> Value*: - """%N = cmpxchg , , - Returns a {, i1} SSA value (simplified to an i64 representing the pair).""" - return builder.build_cmpxchg(ptr, cmp_val, new_val, succ_order, fail_order) - - -# ============================================================ -# va_arg instruction: read the next variadic argument from va_list -# ============================================================ -def build_va_arg(builder: IRBuilder*, - va_list_ptr: Value*, - arg_ty: LLVMType*) -> Value*: - """%N = va_arg , """ - return builder.build_va_arg(va_list_ptr, arg_ty) - - -# ============================================================ -# Inline assembly (Phase 3) -# ============================================================ -def build_inline_asm(builder: IRBuilder*, ret_ty: LLVMType*, - asm_text: str, constraints: str, - has_side_effects: int) -> Value*: - """%N = call asm [sideeffect] "" ""() - Simplified inline-assembly call, no-argument version.""" - return builder.build_inline_asm(ret_ty, asm_text, constraints, has_side_effects) - - -# ============================================================ -# Internal helpers -# ============================================================ -# _append_cstr write-position cache: O(1) positioning for repeated appends to the same buffer, -# avoiding O(n^2) strlen rescans (IR serialization hotspot); falls back to strlen on buffer switch/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.""" - global _g_last_dst, _g_last_pos - 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 (same as __module/__function): a stale pos appends past the first - # NUL and hides content; on failure fall back to strlen (hot path stays O(1)). - 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 >= buffer size (buffer corrupt/dangling) return early, - # else remain = dst_size - dlen underflows and dst[dlen+i] would write past the buffer. - if dlen >= dst_size: - return - slen: sizet = string.strlen(src) - remain: sizet = dst_size - dlen - if remain <= 0: return - i: sizet = 0 - while i < slen and i + 1 < remain: - dst[dlen + i] = src[i] - i += 1 - dst[dlen + i] = '\0' - _g_last_pos = dlen + i +from stdint import * +import string +import viperlib +import memhub +import stdio +import stdlib +from linkedlist import GSListNode, GSList +from .__types import LLVMType, TypePrint +from .__values import Value, new_value, SSAValue +from .__function import Function, BasicBlock, block_append_text, block_set_terminated, block_get_terminated, function_move_block_to_end, _ll_name_needs_quote, block_insert_text_before_terminator + +# ============================================================ +# IRBuilder: cursor-style IR instruction emitter +# +# Tracks the current BasicBlock and appends instruction text to the block's instruction list. +# Maintains an incrementing SSA name counter (%0, %1, %2, ...). +# +# Usage: +# builder = new_builder(pool, func) +# builder.PositionAtEnd(entry_block) +# a = builder.build_alloca(i32_ty) +# b = builder.build_alloca(i32_ty) +# builder.build_store(ConstInt(42), a) +# r = builder.build_load(i32_ty, a) +# builder.build_ret(r) +# +# Note: add novirtual to stay consistent with the hardcoded BUILDER_SIZE=40 +# (methods are implemented as instance methods, with no vtable; the same-named top-level function at the end of the file is a one-line delegate). +# ============================================================ +# Instruction text buffer sizing +# +# An MLIR-form type text **inlines the full field body of a named struct** +# (e.g. `!llvm.struct<"sha1.Cls", (i32, !llvm.ptr, ...)>`), so a single type can reach hundreds of bytes, +# far longer than the LLVM-form `%"sha1.Cls"` reference (a dozen-odd bytes). Therefore the instruction line buffer must be sized by +# "how many type texts can be embedded at most in one line": _LINE_CAP covers 5 type texts + the fixed part. +# The rendering side (TypePrint/MlirTypeText) already has an overflow guard: any truncation errors loudly instead of silently +# producing malformed MLIR (historical failure: the alloca line type was truncated by a 128-byte buffer -> mlir-opt reported +# "unbalanced '(' character in pretty dialect name"). +# ============================================================ +# Capacity of a single type text (types embedded by the builders are rendered to MLIR form by TypePrint). +# An inlined named struct expands its **full field body** at the use site, so a type text can reach hundreds of bytes (ViperC +# class names / nesting are deeper) => 512 is too small and would trigger TypePrint's "type text exceeds the output buffer and is +# truncated" loud error. Raised to 2048; note that arg_cap is derived from this constant +# ((arg_count+1)*(_TY_TEXT_CAP+32)), so a too-high value makes a single multi-arg call allocation approach the module pool's per-block limit. +_TY_TEXT_CAP: sizet = 2048 +_LINE_CAP: sizet = 2560 + +novirtual class IRBuilder: + Pool: memhub.MemBuddy* # Memory pool + Func: Function* # Owning function + CurBlock: BasicBlock* # Current insertion point + Counter: int # SSA name counter + DbgLoc: str # current DILocation ref (kept same-shaped; MLIR printing ignores it, no !dbg suffix) + + # ============================================================ + # Internal helpers + # ============================================================ + def _alloc_ssa_name(self) -> str: + """Allocate an SSA name string "%N" and increment the counter""" + pool: memhub.MemBuddy* = self.Pool + buf: str = pool.alloc(16) + if buf is None: return None + viperlib.snprintf(buf, 16, "%%%d", self.Counter) + self.Counter += 1 + return buf + + + def _type_str(self, ty: LLVMType*) -> str: + """Get the MLIR text of a type (allocates a _TY_TEXT_CAP-byte buffer from pool). + MLIR type text inlines full struct bodies; overflow makes TypePrint fail-fast loudly. + """ + pool: memhub.MemBuddy* = self.Pool + buf: str = pool.alloc_tagged(_TY_TEXT_CAP, memhub.DETSITE_BUILDER_73) + if buf is None: return None + buf[0] = '\0' + TypePrint(buf, _TY_TEXT_CAP, ty, pool) + # [IR-FAIL] guard: LLVMType may be overwritten/dangling (pool reuse after reset), making the + # first char garbage/empty; fail-fast. Legal first chars: i/f/!/[/{/%/" and 'n'. + c0: int = buf[0] + if c0 == 0 or (c0 != 'i' and c0 != 'f' and c0 != '!' and c0 != 'n' and + c0 != '[' and c0 != '{' and c0 != '%' and c0 != '"'): + stdio.printf("[IR-FAIL] _type_str: corrupted LLVMType object ty=0x%llx first char=%d output='%s'\n", + uint64(ty), c0, buf) + stdlib.exit(1) + return None + return buf + + + def _emit(self, text: str): + """Append instruction text to the current block. Unlike llvmlite, MLIR has no ", !dbg !N" + suffix, so DbgLoc is deliberately ignored (set_debug_location only records the model).""" + if self is None or self.CurBlock is None: return + block_append_text(self.Pool, self.CurBlock, text) + + + def set_debug_location(self, ref: str): + """Set the current DILocation ref (kept same-shaped; MLIR only records it, no output effect). + ref=None clears it. + """ + if self is None: return + self.DbgLoc = ref + + def clear_debug_location(self): + """Clear the current DILocation ref (kept same-shaped; MLIR only records it)""" + if self is None: return + self.DbgLoc = None + + + def _mark_terminated(self): + """Mark the current block as terminated""" + if self is None or self.CurBlock is None: return + block_set_terminated(self.CurBlock, 1) + + + def _build_binop(self, op: str, + lhs: Value*, rhs: Value*) -> Value*: + """Shared implementation of binary operations""" + if self is None or lhs is None or rhs is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(lhs.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = %s %s %s, %s", name, op, ty_s, lhs.Name, rhs.Name) + self._emit(line) + return SSAValue(pool, lhs.Ty, name) + + + # ============================================================ + # Block positioning + # ============================================================ + def position_at_end(self, block: BasicBlock*): + """Set the insertion point to the end of block and move block to the end of the + function's block list, keeping text order consistent with control-flow order (else + non-monotonic SSA names break llc).""" + if self is None: return + self.CurBlock = block + if self.Func is not None: + function_move_block_to_end(self.Func, block) + + + def builder_cur_block_is_terminated(self) -> int: + """Check the current block's IsTerminated flag via block_get_terminated instead of the + chained self.CurBlock.IsTerminated, which silently skips the GEP when BasicBlock is opaque.""" + if self is None or self.CurBlock is None: + return 1 + return block_get_terminated(self.CurBlock) + + + # ============================================================ + # Memory instructions + # ============================================================ + def build_alloca(self, ty: LLVMType*) -> Value*: + """%N = alloca + + Returns an SSA value of type ty* (default alignment, backend-decided). + No default parameter: TPC parallel compilation does not pre-register include defaults + (argN fallback); for explicit alignment use build_alloca_aligned. + """ + if self is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = alloca %s", name, ty_s) + self._emit(line) + + # result type is ty* + result_ty: LLVMType* = LLVMType.Ptr(ty) + result_ty_ptr: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if result_ty_ptr is not None: + string.memset(result_ty_ptr, 0, LLVMType.__sizeof__()) + *result_ty_ptr = *result_ty + return SSAValue(pool, result_ty_ptr, name) + + + def build_alloca_aligned(self, ty: LLVMType*, + align: int) -> Value*: + """%N = alloca , align N. Returns an SSA value of type ty* with explicit alignment + (e.g. va_list 24-byte buffer needs 16). align has no default; callers must pass it.""" + if self is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = alloca %s, align %d", name, ty_s, align) + self._emit(line) + + # result type is ty* + result_ty: LLVMType* = LLVMType.Ptr(ty) + result_ty_ptr: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if result_ty_ptr is not None: + string.memset(result_ty_ptr, 0, LLVMType.__sizeof__()) + *result_ty_ptr = *result_ty + return SSAValue(pool, result_ty_ptr, name) + + + def build_alloca_at_entry(self, ty: LLVMType*) -> Value*: + """%N = alloca , emitted in the function entry block to ensure dominance. + Inserts before the entry terminator; alloca inside a conditional branch violates dominance.""" + if self is None: return None + pool: memhub.MemBuddy* = self.Pool + func: Function* = self.Func + if func is None or func.Blocks is None: + return self.build_alloca(ty) + blocks: GSList[BasicBlock]* = func.Blocks + entry_blk: BasicBlock* = blocks.Head + if entry_blk is None: + return self.build_alloca(ty) + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(ty) + line_text: str = pool.alloc(_LINE_CAP) + if line_text is None: return None + viperlib.snprintf(line_text, _LINE_CAP, "%s = alloca %s", name, ty_s) + # Insert before the entry terminator: if the entry block already ends in br/ret, appending + # would put alloca after the terminator (invalid IR); insert-before-terminator handles both. + block_insert_text_before_terminator(pool, entry_blk, line_text) + # result type is ty* + result_ty: LLVMType* = LLVMType.Ptr(ty) + result_ty_ptr: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if result_ty_ptr is not None: + string.memset(result_ty_ptr, 0, LLVMType.__sizeof__()) + *result_ty_ptr = *result_ty + return SSAValue(pool, result_ty_ptr, name) + + + def build_load(self, ty: LLVMType*, + ptr: Value*, + is_volatile: int = 0) -> Value*: + """%N = load [volatile ], + is_volatile=0 (default) emits the plain form; =1 adds the volatile qualifier so the + optimizer cannot hoist/merge the access (e.g. spinlocks).""" + if self is None or ptr is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(ty) + ptr_ty_s: str = self._type_str(ptr.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + if is_volatile != 0: + viperlib.snprintf(line, _LINE_CAP, "%s = load volatile %s, %s %s", name, ty_s, ptr_ty_s, ptr.Name) + else: + viperlib.snprintf(line, _LINE_CAP, "%s = load %s, %s %s", name, ty_s, ptr_ty_s, ptr.Name) + self._emit(line) + return SSAValue(pool, ty, name) + + + def build_store(self, val: Value*, ptr: Value*, + is_volatile: int = 0): + """store [volatile ] , + is_volatile=0 (default) emits the plain form; =1 adds the volatile qualifier + (`store volatile i32 %v, i32* %p`).""" + if self is None or val is None or ptr is None: return + pool: memhub.MemBuddy* = self.Pool + val_ty_s: str = self._type_str(val.Ty) + ptr_ty_s: str = self._type_str(ptr.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return + if is_volatile != 0: + viperlib.snprintf(line, _LINE_CAP, "store volatile %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) + else: + viperlib.snprintf(line, _LINE_CAP, "store %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) + self._emit(line) + + + def build_store_at_entry(self, val: Value*, ptr: Value*, + is_volatile: int = 0): + """store [volatile ] , , emitted in the **entry block** + (inserted before the entry terminator) so the write dominates every path. + + Used to zero/null-initialize a local slot at function entry: a slot that is not + assigned on some path must still hold a safe value, e.g. the M3 owned-slife guard — + the exit-time `free(load slot)` must be `free(NULL)` (a no-op) instead of freeing + uninitialized stack garbage. Mirrors build_alloca_at_entry's insertion strategy. + """ + if self is None or val is None or ptr is None: return + pool: memhub.MemBuddy* = self.Pool + func: Function* = self.Func + if func is None or func.Blocks is None: + self.build_store(val, ptr, is_volatile) + return + blocks: GSList[BasicBlock]* = func.Blocks + entry_blk: BasicBlock* = blocks.Head + if entry_blk is None: + self.build_store(val, ptr, is_volatile) + return + val_ty_s: str = self._type_str(val.Ty) + ptr_ty_s: str = self._type_str(ptr.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return + if is_volatile != 0: + viperlib.snprintf(line, _LINE_CAP, "store volatile %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) + else: + viperlib.snprintf(line, _LINE_CAP, "store %s %s, %s %s", val_ty_s, val.Name, ptr_ty_s, ptr.Name) + block_insert_text_before_terminator(pool, entry_blk, line) + + + # ============================================================ + # Binary operation instructions + # ============================================================ + def build_add(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = add , """ + return self._build_binop("add", lhs, rhs) + + + def build_sub(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = sub , """ + return self._build_binop("sub", lhs, rhs) + + + def build_mul(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = mul , """ + return self._build_binop("mul", lhs, rhs) + + + def build_sdiv(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = sdiv , """ + return self._build_binop("sdiv", lhs, rhs) + + + def build_udiv(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = udiv , """ + return self._build_binop("udiv", lhs, rhs) + + + def build_srem(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = srem , """ + return self._build_binop("srem", lhs, rhs) + + + def build_urem(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = urem , """ + return self._build_binop("urem", lhs, rhs) + + + def build_and(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = and , """ + return self._build_binop("and", lhs, rhs) + + + def build_or(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = or , """ + return self._build_binop("or", lhs, rhs) + + + def build_xor(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = xor , """ + return self._build_binop("xor", lhs, rhs) + + + def build_shl(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = shl , """ + return self._build_binop("shl", lhs, rhs) + + + def build_lshr(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = lshr , """ + return self._build_binop("lshr", lhs, rhs) + + + def build_ashr(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = ashr , """ + return self._build_binop("ashr", lhs, rhs) + + + # ============================================================ + # Float binary operation instructions + # ============================================================ + def build_fadd(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fadd , """ + return self._build_binop("fadd", lhs, rhs) + + + def build_fsub(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fsub , """ + return self._build_binop("fsub", lhs, rhs) + + + def build_fmul(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fmul , """ + return self._build_binop("fmul", lhs, rhs) + + + def build_fdiv(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fdiv , """ + return self._build_binop("fdiv", lhs, rhs) + + + def build_frem(self, lhs: Value*, + rhs: Value*) -> Value*: + """%N = frem , """ + return self._build_binop("frem", lhs, rhs) + + + def build_fneg(self, val: Value*) -> Value*: + """%N = fneg """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(val.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = fneg %s %s", name, ty_s, val.Name) + self._emit(line) + return SSAValue(pool, val.Ty, name) + + + # ============================================================ + # Comparison instructions + # ============================================================ + def build_icmp(self, pred: int, + lhs: Value*, rhs: Value*) -> Value*: + """%N = icmp , + Returns an SSA value of type i1.""" + if self is None or lhs is None or rhs is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(lhs.Ty) + pred_s: str = _icmp_pred_str(pred) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = icmp %s %s %s, %s", name, pred_s, ty_s, lhs.Name, rhs.Name) + self._emit(line) + + # result type is i1 + i1_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if i1_ty is not None: + string.memset(i1_ty, 0, LLVMType.__sizeof__()) + *i1_ty = *LLVMType.Int(1) + return SSAValue(pool, i1_ty, name) + + + def build_fcmp(self, pred: int, + lhs: Value*, rhs: Value*) -> Value*: + """%N = fcmp , + Returns an SSA value of type i1.""" + if self is None or lhs is None or rhs is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(lhs.Ty) + pred_s: str = _fcmp_pred_str(pred) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = fcmp %s %s %s, %s", name, pred_s, ty_s, lhs.Name, rhs.Name) + self._emit(line) + + # result type is i1 + i1_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if i1_ty is not None: + string.memset(i1_ty, 0, LLVMType.__sizeof__()) + *i1_ty = *LLVMType.Int(1) + return SSAValue(pool, i1_ty, name) + + + # ============================================================ + # Terminator instructions + # ============================================================ + def build_br(self, target: BasicBlock*): + """br label %""" + if self is None or target is None: return + pool: memhub.MemBuddy* = self.Pool + line: str = pool.alloc(64) + if line is None: return + viperlib.snprintf(line, 64, "br label %%%s", target.Name) + self._emit(line) + self._mark_terminated() + + + def build_cond_br(self, cond: Value*, + then_blk: BasicBlock*, else_blk: BasicBlock*): + """br i1 , label %, label %""" + if self is None or cond is None or then_blk is None or else_blk is None: return + pool: memhub.MemBuddy* = self.Pool + line: str = pool.alloc(_LINE_CAP) + if line is None: return + viperlib.snprintf(line, _LINE_CAP, "br i1 %s, label %%%s, label %%%s", + cond.Name, then_blk.Name, else_blk.Name) + self._emit(line) + self._mark_terminated() + + + def build_ret(self, val: Value*): + """ret """ + if self is None or val is None: return + pool: memhub.MemBuddy* = self.Pool + ty_s: str = self._type_str(val.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return + viperlib.snprintf(line, _LINE_CAP, "ret %s %s", ty_s, val.Name) + self._emit(line) + self._mark_terminated() + + + def build_ret_void(self): + """ret void""" + if self is None: return + pool: memhub.MemBuddy* = self.Pool + line: str = pool.alloc(16) + if line is None: return + string.strcpy(line, "ret void") + self._emit(line) + self._mark_terminated() + + + # ============================================================ + # Call instructions + # ============================================================ + def build_call(self, callee: str, + args: Value*, arg_count: int, + ret_ty: LLVMType*, + is_variadic: int = 0) -> Value*: + """%N = call @() + args is the Value list head, arg_count the argument count. If ret_ty is void no SSA name is + allocated (plain call); is_variadic=1 emits the function type signature (e.g. i32 (i8*, ...)).""" + if self is None: return None + pool: memhub.MemBuddy* = self.Pool + + # --traceback: record this direct call's call site (build time; no-op unless enabled and + # inside a DebugInfoGen define context). See the debug call-site section in __function.vp. + _DebugNoteCall(callee) + + # check whether the return type is void + is_void: int = 0 + match ret_ty: + case LLVMType.Void(): + is_void = 1 + case _: + is_void = 0 + + name: str = None + if is_void == 0: + name = self._alloc_ssa_name() + + ret_ty_s: str = self._type_str(ret_ty) + + # Build the argument list text + variadic function type signature; size by argument count + # (struct type text can be _TY_TEXT_CAP each; a fixed 1024 would silently truncate). + arg_cap: sizet = sizet(arg_count + 1) * (_TY_TEXT_CAP + 32) + 32 + args_buf: str = pool.alloc_tagged(arg_cap, memhub.DETSITE_BUILDER_527) + if args_buf is None: return None + args_buf[0] = '\0' + func_ty_buf: str = pool.alloc_tagged(arg_cap, memhub.DETSITE_BUILDER_530) + if func_ty_buf is None: return None + func_ty_buf[0] = '\0' + cur: Value* = args + first: int = 1 + while cur is not None: + if first == 0: + _append_cstr(args_buf, arg_cap, ", ") + _append_cstr(func_ty_buf, arg_cap, ", ") + arg_ty_s: str = self._type_str(cur.Ty) + _append_cstr(args_buf, arg_cap, arg_ty_s) + _append_cstr(args_buf, arg_cap, " ") + if cur.Name is not None: + _append_cstr(args_buf, arg_cap, cur.Name) + _append_cstr(func_ty_buf, arg_cap, arg_ty_s) + cur = cur.Next + first = 0 + if is_variadic == 1: + if first == 0: + _append_cstr(func_ty_buf, arg_cap, ", ") + _append_cstr(func_ty_buf, arg_cap, "...") + + # Build the quoted callee name (SHA1-prefixed names with '.' or a leading digit need quotes) + callee_buf: str = pool.alloc(_LINE_CAP) + if callee_buf is None: return None + callee_buf[0] = '\0' + if _ll_name_needs_quote(callee) != 0: + _append_cstr(callee_buf, 128, "\"") + _append_cstr(callee_buf, 128, callee) + _append_cstr(callee_buf, 128, "\"") + else: + _append_cstr(callee_buf, 128, callee) + + line_cap: sizet = string.strlen(ret_ty_s) + string.strlen(args_buf) \ + + string.strlen(func_ty_buf) + string.strlen(callee_buf) + 64 + line: str = pool.alloc(line_cap) + if line is None: return None + if is_variadic == 1: + if is_void == 1: + viperlib.snprintf(line, line_cap, "call %s (%s) @%s(%s)", ret_ty_s, func_ty_buf, callee_buf, args_buf) + else: + viperlib.snprintf(line, line_cap, "%s = call %s (%s) @%s(%s)", name, ret_ty_s, func_ty_buf, callee_buf, args_buf) + else: + if is_void == 1: + viperlib.snprintf(line, line_cap, "call %s @%s(%s)", ret_ty_s, callee_buf, args_buf) + else: + viperlib.snprintf(line, line_cap, "%s = call %s @%s(%s)", name, ret_ty_s, callee_buf, args_buf) + self._emit(line) + + if is_void == 1: + return None + return SSAValue(pool, ret_ty, name) + + + # ============================================================ + # Indirect call instruction: call %callee() + # + # callee_val is a function-pointer SSA value (must already be a pointer type, e.g. i32(i8*)*) + # ============================================================ + def build_call_indirect(self, callee_val: Value*, + args: Value*, arg_count: int, + ret_ty: LLVMType*) -> Value*: + """%N = call %callee() + Indirect call: callee_val is a function-pointer SSA value.""" + if self is None or callee_val is None: return None + pool: memhub.MemBuddy* = self.Pool + + is_void: int = 0 + match ret_ty: + case LLVMType.Void(): + is_void = 1 + case _: + is_void = 0 + + name: str = None + if is_void == 0: + name = self._alloc_ssa_name() + + ret_ty_s: str = self._type_str(ret_ty) + + # Build the argument list text (sized by argument count, same reason as build_call) + arg_cap2: sizet = sizet(arg_count + 1) * (_TY_TEXT_CAP + 32) + 32 + args_buf: str = pool.alloc_tagged(arg_cap2, memhub.DETSITE_BUILDER_612) + if args_buf is None: return None + args_buf[0] = '\0' + cur: Value* = args + first: int = 1 + while cur is not None: + if first == 0: + _append_cstr(args_buf, arg_cap2, ", ") + arg_ty_s: str = self._type_str(cur.Ty) + _append_cstr(args_buf, arg_cap2, arg_ty_s) + _append_cstr(args_buf, arg_cap2, " ") + if cur.Name is not None: + _append_cstr(args_buf, arg_cap2, cur.Name) + cur = cur.Next + first = 0 + + line_cap2: sizet = string.strlen(ret_ty_s) + string.strlen(args_buf) \ + + string.strlen(callee_val.Name) + 64 + line: str = pool.alloc(line_cap2) + if line is None: return None + if is_void == 1: + viperlib.snprintf(line, line_cap2, "call %s %s(%s)", ret_ty_s, callee_val.Name, args_buf) + else: + viperlib.snprintf(line, line_cap2, "%s = call %s %s(%s)", name, ret_ty_s, callee_val.Name, args_buf) + self._emit(line) + + if is_void == 1: + return None + return SSAValue(pool, ret_ty, name) + + + # ============================================================ + # Type conversion instructions + # ============================================================ + def build_bitcast(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = bitcast to """ + if self is None or val is None: + return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + if val.Name is None: + return None + viperlib.snprintf(line, _LINE_CAP, "%s = bitcast %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + ret: Value* = SSAValue(pool, to_ty, name) + return ret + + + def build_sext(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = sext to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = sext %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_trunc(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = trunc to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = trunc %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_zext(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = zext to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = zext %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_ptrtoint(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = ptrtoint to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = ptrtoint %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_inttoptr(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = inttoptr to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = inttoptr %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + # ============================================================ + # Float conversion instructions + # ============================================================ + def build_fpext(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fpext to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = fpext %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_fptrunc(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fptrunc to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = fptrunc %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_fp2si(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fptosi to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = fptosi %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_si2fp(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = sitofp to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = sitofp %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_fp2ui(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fptoui to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = fptoui %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + def build_ui2fp(self, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = uitofp to """ + if self is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + from_ty_s: str = self._type_str(val.Ty) + to_ty_s: str = self._type_str(to_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = uitofp %s %s to %s", name, from_ty_s, val.Name, to_ty_s) + self._emit(line) + return SSAValue(pool, to_ty, name) + + + # ============================================================ + # _gep_index_zext - zero-extend narrow integer GEP indices + # + # LLVM sign-extends narrow GEP indices (i8 0xE2 = -30); Viper indices are non-negative, so all + # <32-bit indices are zext'd to i32. build_gep/build_gep_array handle this internally. + # ============================================================ + def _gep_index_zext(self, idx: Value*) -> Value*: + """Zero-extend a <32-bit integer index to i32; other types are returned unchanged""" + if self is None or idx is None or idx.Ty is None: + return idx + bits: int = 0 + match idx.Ty: + case LLVMType.Int(b): + bits = b + case _: + bits = 0 + if bits == 0 or bits >= 32: + return idx + i32_ty: LLVMType* = self.Pool.alloc(LLVMType.__sizeof__()) + if i32_ty is None: + return idx + string.memset(i32_ty, 0, LLVMType.__sizeof__()) + *i32_ty = *LLVMType.Int(32) + z: Value* = self.build_zext(idx, i32_ty) + return z if z is not None else idx + + + # ============================================================ + # GEP instructions + # ============================================================ + def build_gep(self, elem_ty: LLVMType*, + ptr: Value*, idx: Value*) -> Value*: + """%N = getelementptr , , """ + if self is None or ptr is None or idx is None: return None + # Defensive check: ptr.Ty must be a pointer type, else llc errors ("base of getelementptr + # must be a pointer") when translate_value returns a non-pointer (e.g. i32 constant 0). + if _is_ptr_type(ptr.Ty) == 0: + return None + idx = self._gep_index_zext(idx) + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + elem_ty_s: str = self._type_str(elem_ty) + ptr_ty_s: str = self._type_str(ptr.Ty) + idx_ty_s: str = self._type_str(idx.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = getelementptr %s, %s %s, %s %s", + name, elem_ty_s, ptr_ty_s, ptr.Name, idx_ty_s, idx.Name) + self._emit(line) + + # GEP result type is elem_ty* + result_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if result_ty is not None: + string.memset(result_ty, 0, LLVMType.__sizeof__()) + *result_ty = *LLVMType.Ptr(elem_ty) + return SSAValue(pool, result_ty, name) + + + # ============================================================ + # build_gep_array - two-index array GEP: getelementptr [N x ty], [N x ty]* %ptr, i32 0, i32 %idx + # ============================================================ + def build_gep_array(self, array_ty: LLVMType*, + elem_ty: LLVMType*, + ptr: Value*, idx: Value*) -> Value*: + """%N = getelementptr [N x elem_ty], [N x elem_ty]* %ptr, i32 0, i32 %idx + Result type is elem_ty*.""" + if self is None or ptr is None or idx is None: return None + idx = self._gep_index_zext(idx) + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + array_ty_s: str = self._type_str(array_ty) + ptr_ty_s: str = self._type_str(ptr.Ty) + idx_ty_s: str = self._type_str(idx.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = getelementptr %s, %s %s, i32 0, %s %s", + name, array_ty_s, ptr_ty_s, ptr.Name, idx_ty_s, idx.Name) + self._emit(line) + + # GEP result type is elem_ty* + result_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if result_ty is not None: + string.memset(result_ty, 0, LLVMType.__sizeof__()) + *result_ty = *LLVMType.Ptr(elem_ty) + return SSAValue(pool, result_ty, name) + + + # ============================================================ + # build_gep_struct - struct-field GEP: getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx + # ============================================================ + def build_gep_struct(self, struct_ty: LLVMType*, + field_ty: LLVMType*, + ptr: Value*, field_idx: int) -> Value*: + """%N = getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx + Result type is field_ty*.""" + if self is None or ptr is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + struct_ty_s: str = self._type_str(struct_ty) + ptr_ty_s: str = self._type_str(ptr.Ty) + line: str = pool.alloc(2048) + if line is None: + stdio.printf("[BGS] line alloc None\n") + return None + viperlib.snprintf(line, 2048, "%s = getelementptr %s, %s %s, i32 0, i32 %d", + name, struct_ty_s, ptr_ty_s, ptr.Name, field_idx) + self._emit(line) + + # GEP result type is field_ty* + result_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if result_ty is None: + stdio.printf("[BGS] result_ty alloc None\n") + else: + string.memset(result_ty, 0, LLVMType.__sizeof__()) + *result_ty = *LLVMType.Ptr(field_ty) + rv: Value* = SSAValue(pool, result_ty, name) + if rv is None: + stdio.printf("[BGS] SSAValue None\n") + return rv + + + # ============================================================ + # Phi instruction + # ============================================================ + def build_phi(self, ty: LLVMType*, + incoming_head: PhiIncoming*, count: int) -> Value*: + """%N = phi [val1, %blk1], [val2, %blk2], ... + incoming_head is the PhiIncoming list head; count is the incoming-edge count.""" + if self is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(ty) + + # Build the incoming list text [val, %blk], ...; there can be many edges (e.g. a 30+ or-chain), + # so a large buffer is required. + parts: str = pool.alloc(4096) + if parts is None: return None + parts[0] = '\0' + cur: PhiIncoming* = incoming_head + first: int = 1 + while cur is not None: + if first == 0: + _append_cstr(parts, 4096, ", ") + _append_cstr(parts, 4096, "[") + # use an intermediate var; chained struct member access would be dropped by the compiler + inc_val: Value* = cur.Val + if inc_val is not None: + inc_val_name: str = inc_val.Name + if inc_val_name is not None: + _append_cstr(parts, 4096, inc_val_name) + _append_cstr(parts, 4096, ", %") + inc_blk: BasicBlock* = cur.Block + if inc_blk is not None: + inc_blk_name: str = inc_blk.Name + if inc_blk_name is not None: + _append_cstr(parts, 4096, inc_blk_name) + _append_cstr(parts, 4096, "]") + cur = cur.Next + first = 0 + + line: str = pool.alloc(8192) + if line is None: return None + viperlib.snprintf(line, 8192, "%s = phi %s %s", name, ty_s, parts) + self._emit(line) + return SSAValue(pool, ty, name) + + + # ============================================================ + # Switch instruction + # ============================================================ + def build_switch(self, val: Value*, + default_blk: BasicBlock*, + cases_head: SwitchCase*, case_count: int): + """switch , label % [, label % ...]""" + if self is None or val is None or default_blk is None: return + pool: memhub.MemBuddy* = self.Pool + ty_s: str = self._type_str(val.Ty) + + # build body: switch , label %default + line: str = pool.alloc(_LINE_CAP) + if line is None: return + viperlib.snprintf(line, _LINE_CAP, "switch %s %s, label %%%s", + ty_s, val.Name, default_blk.Name) + _append_cstr(line, _LINE_CAP, " [") + _append_cstr(line, _LINE_CAP, "\n") + + # iterate the case list + cur: SwitchCase* = cases_head + case_ty_s: str = self._type_str(val.Ty) + while cur is not None: + case_line: str = pool.alloc(_LINE_CAP) + if case_line is not None: + # use an intermediate var; chained struct member access would be dropped by the compiler + cv: Value* = cur.CaseVal + cb: BasicBlock* = cur.Block + if cv is not None and cb is not None: + cv_name: str = cv.Name + cb_name: str = cb.Name + if cv_name is not None and cb_name is not None: + viperlib.snprintf(case_line, _LINE_CAP, " %s %s, label %%%s\n", + case_ty_s, cv_name, cb_name) + _append_cstr(line, _LINE_CAP, case_line) + cur = cur.Next + + _append_cstr(line, _LINE_CAP, "]") + self._emit(line) + self._mark_terminated() + + + # ============================================================ + # Select instruction + # ============================================================ + def build_select(self, cond: Value*, + true_val: Value*, false_val: Value*) -> Value*: + """%N = select i1 , , """ + if self is None or cond is None or true_val is None or false_val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ty_s: str = self._type_str(true_val.Ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = select i1 %s, %s %s, %s %s", + name, cond.Name, ty_s, true_val.Name, ty_s, false_val.Name) + self._emit(line) + return SSAValue(pool, true_val.Ty, name) + + + # ============================================================ + # Unreachable instruction + # ============================================================ + def build_unreachable(self): + """unreachable""" + if self is None: return + pool: memhub.MemBuddy* = self.Pool + line: str = pool.alloc(16) + if line is None: return + string.strcpy(line, "unreachable") + self._emit(line) + self._mark_terminated() + + + # ============================================================ + # Atomic instructions (Phase 3) + # ============================================================ + def build_atomicrmw(self, op: int, + ptr: Value*, val: Value*, + order: int) -> Value*: + """%N = atomicrmw , """ + if self is None or ptr is None or val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ptr_ty_s: str = self._type_str(ptr.Ty) + val_ty_s: str = self._type_str(val.Ty) + op_s: str = _atomic_op_str(op) + ord_s: str = _atomic_order_str(order) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = atomicrmw %s %s %s, %s %s %s", + name, op_s, ptr_ty_s, ptr.Name, val_ty_s, val.Name, ord_s) + self._emit(line) + return SSAValue(pool, val.Ty, name) + + + def build_cmpxchg(self, ptr: Value*, + cmp_val: Value*, new_val: Value*, + succ_order: int, fail_order: int) -> Value*: + """%N = cmpxchg , , + Returns a {, i1} SSA value (simplified to an i64 representing the pair).""" + if self is None or ptr is None or cmp_val is None or new_val is None: return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + ptr_ty_s: str = self._type_str(ptr.Ty) + cmp_ty_s: str = self._type_str(cmp_val.Ty) + new_ty_s: str = self._type_str(new_val.Ty) + succ_s: str = _atomic_order_str(succ_order) + fail_s: str = _atomic_order_str(fail_order) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + viperlib.snprintf(line, _LINE_CAP, "%s = cmpxchg %s %s, %s %s, %s %s %s %s", + name, ptr_ty_s, ptr.Name, cmp_ty_s, cmp_val.Name, + new_ty_s, new_val.Name, succ_s, fail_s) + self._emit(line) + # return the original value's type + return SSAValue(pool, new_val.Ty, name) + + + # ============================================================ + # va_arg instruction: read the next variadic argument from va_list + # ============================================================ + def build_va_arg(self, + va_list_ptr: Value*, + arg_ty: LLVMType*) -> Value*: + """%N = va_arg , """ + if self is None or va_list_ptr is None or arg_ty is None: + return None + pool: memhub.MemBuddy* = self.Pool + name: str = self._alloc_ssa_name() + va_list_ty_s: str = self._type_str(va_list_ptr.Ty) + arg_ty_s: str = self._type_str(arg_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + if va_list_ptr.Name is None: + return None + viperlib.snprintf(line, _LINE_CAP, "%s = va_arg %s %s, %s", + name, va_list_ty_s, va_list_ptr.Name, arg_ty_s) + self._emit(line) + return SSAValue(pool, arg_ty, name) + + + # ============================================================ + # Inline assembly (Phase 3) + # ============================================================ + def build_inline_asm(self, ret_ty: LLVMType*, + asm_text: str, constraints: str, + has_side_effects: int) -> Value*: + """%N = call asm [sideeffect] "" ""() + Simplified inline-assembly call, no-argument version.""" + if self is None: return None + pool: memhub.MemBuddy* = self.Pool + + # check whether the return type is void + is_void: int = 0 + match ret_ty: + case LLVMType.Void(): + is_void = 1 + case _: + is_void = 0 + + name: str = None + if is_void == 0: + name = self._alloc_ssa_name() + + ret_ty_s: str = self._type_str(ret_ty) + line: str = pool.alloc(_LINE_CAP) + if line is None: return None + + if has_side_effects != 0: + if is_void == 1: + viperlib.snprintf(line, _LINE_CAP, "call %s asm sideeffect \"%s\", \"%s\"()", + ret_ty_s, asm_text, constraints) + else: + viperlib.snprintf(line, _LINE_CAP, "%s = call %s asm sideeffect \"%s\", \"%s\"()", + name, ret_ty_s, asm_text, constraints) + else: + if is_void == 1: + viperlib.snprintf(line, _LINE_CAP, "call %s asm \"%s\", \"%s\"()", + ret_ty_s, asm_text, constraints) + else: + viperlib.snprintf(line, _LINE_CAP, "%s = call %s asm \"%s\", \"%s\"()", + name, ret_ty_s, asm_text, constraints) + self._emit(line) + + if is_void == 1: + return None + return SSAValue(pool, ret_ty, name) + + +# ============================================================ +# Factory functions +# ============================================================ +# IRBuilder hardcoded size: Pool(8) + Func(8) + CurBlock(8) + Counter(4) + DbgLoc(8) = 40 bytes (same layout as llvmlite) +define BUILDER_SIZE = 40 + +def new_builder(pool: memhub.MemBuddy*, func: Function*) -> IRBuilder*: + """Create an IRBuilder associated with function func.""" + ptr: IRBuilder* = pool.alloc(BUILDER_SIZE) + if ptr is None: return None + string.memset(ptr, 0, BUILDER_SIZE) + ptr.Pool = pool + ptr.Func = func + ptr.CurBlock = None + ptr.Counter = 0 + ptr.DbgLoc = None + return ptr + + +# ============================================================ +# Block positioning +# ============================================================ +def position_at_end(builder: IRBuilder*, block: BasicBlock*): + """Set the insertion point to the end of block and move block to the end of the + function's block list, keeping text order consistent with control-flow order (else + non-monotonic SSA names break llc).""" + builder.position_at_end(block) + + +# ============================================================ +# Internal helpers +# ============================================================ +def _alloc_ssa_name(builder: IRBuilder*) -> str: + """Allocate an SSA name string "%N" and increment the counter""" + return builder._alloc_ssa_name() + + +def _type_str(builder: IRBuilder*, ty: LLVMType*) -> str: + """Get the IR text of a type (allocates a buffer from pool). Complex struct types (e.g. a + 13-method vtable) can exceed 64 bytes; too small a buffer truncates '}' and breaks the IR.""" + return builder._type_str(ty) + + +def _emit(builder: IRBuilder*, text: str): + """Append instruction text to the current block.""" + builder._emit(text) + + +def _mark_terminated(builder: IRBuilder*): + """Mark the current block as terminated""" + builder._mark_terminated() + + +def builder_cur_block_is_terminated(builder: IRBuilder*) -> int: + """Check the current block's IsTerminated flag via block_get_terminated instead of the + chained builder.CurBlock.IsTerminated, which silently skips the GEP when BasicBlock is opaque.""" + return builder.builder_cur_block_is_terminated() + + +# ============================================================ +# Debug location (kept same-shaped; ignored by MLIR printing) +# ============================================================ +def builder_set_debug_location(builder: IRBuilder*, ref: str): + """Set the current DILocation ref (kept same-shaped; MLIR only records it, no output effect)""" + builder.set_debug_location(ref) + + +def builder_clear_debug_location(builder: IRBuilder*): + """Clear the current DILocation ref (kept same-shaped; MLIR only records it)""" + builder.clear_debug_location() + + +# ============================================================ +# Memory instructions +# ============================================================ +def build_alloca(builder: IRBuilder*, ty: LLVMType*) -> Value*: + """%N = alloca + + Returns an SSA value of type ty* (default alignment, backend-decided). + No default parameter: TPC parallel compilation does not pre-register include defaults + (argN fallback); for explicit alignment use build_alloca_aligned. + """ + return builder.build_alloca(ty) + + +def build_alloca_aligned(builder: IRBuilder*, ty: LLVMType*, + align: int) -> Value*: + """%N = alloca , align N. Returns an SSA value of type ty* with explicit alignment + (e.g. va_list 24-byte buffer needs 16). align has no default; callers must pass it.""" + return builder.build_alloca_aligned(ty, align) + + +def build_alloca_at_entry(builder: IRBuilder*, ty: LLVMType*) -> Value*: + """%N = alloca , emitted in the function entry block to ensure dominance. + Inserts before the entry terminator; alloca inside a conditional branch violates dominance.""" + return builder.build_alloca_at_entry(ty) + + +def build_load(builder: IRBuilder*, ty: LLVMType*, + ptr: Value*, + is_volatile: int = 0) -> Value*: + """%N = load [volatile ], """ + return builder.build_load(ty, ptr, is_volatile) + + +def build_store(builder: IRBuilder*, val: Value*, ptr: Value*, + is_volatile: int = 0): + """store [volatile ] , """ + builder.build_store(val, ptr, is_volatile) + + +def build_store_at_entry(builder: IRBuilder*, val: Value*, ptr: Value*, + is_volatile: int = 0): + """store [volatile ] , emitted in the entry block + (before the entry terminator) so the write dominates every path; used for + null-initializing a local slot at function entry (M3 owned-slife guard).""" + builder.build_store_at_entry(val, ptr, is_volatile) + + +# ============================================================ +# Binary operation instructions +# ============================================================ +def build_add(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = add , """ + return builder.build_add(lhs, rhs) + + +def build_sub(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = sub , """ + return builder.build_sub(lhs, rhs) + + +def build_mul(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = mul , """ + return builder.build_mul(lhs, rhs) + + +def build_sdiv(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = sdiv , """ + return builder.build_sdiv(lhs, rhs) + + +def build_udiv(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = udiv , """ + return builder.build_udiv(lhs, rhs) + + +def build_srem(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = srem , """ + return builder.build_srem(lhs, rhs) + + +def build_urem(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = urem , """ + return builder.build_urem(lhs, rhs) + + +def build_and(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = and , """ + return builder.build_and(lhs, rhs) + + +def build_or(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = or , """ + return builder.build_or(lhs, rhs) + + +def build_xor(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = xor , """ + return builder.build_xor(lhs, rhs) + + +def build_shl(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = shl , """ + return builder.build_shl(lhs, rhs) + + +def build_lshr(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = lshr , """ + return builder.build_lshr(lhs, rhs) + + +def build_ashr(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = ashr , """ + return builder.build_ashr(lhs, rhs) + + +# ============================================================ +# Float binary operation instructions +# ============================================================ +def build_fadd(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fadd , """ + return builder.build_fadd(lhs, rhs) + + +def build_fsub(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fsub , """ + return builder.build_fsub(lhs, rhs) + + +def build_fmul(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fmul , """ + return builder.build_fmul(lhs, rhs) + + +def build_fdiv(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = fdiv , """ + return builder.build_fdiv(lhs, rhs) + + +def build_frem(builder: IRBuilder*, lhs: Value*, + rhs: Value*) -> Value*: + """%N = frem , """ + return builder.build_frem(lhs, rhs) + + +def build_fneg(builder: IRBuilder*, val: Value*) -> Value*: + """%N = fneg """ + return builder.build_fneg(val) + + +def _build_binop(builder: IRBuilder*, op: str, + lhs: Value*, rhs: Value*) -> Value*: + """Shared implementation of binary operations""" + return builder._build_binop(op, lhs, rhs) + + +# ============================================================ +# Comparison instructions +# ============================================================ +# ICMP predicate constants +define ICMP_EQ = 0 +define ICMP_NE = 1 +define ICMP_SGT = 2 +define ICMP_SGE = 3 +define ICMP_SLT = 4 +define ICMP_SLE = 5 +define ICMP_UGT = 6 +define ICMP_UGE = 7 +define ICMP_ULT = 8 +define ICMP_ULE = 9 + +# FCMP predicate constants +define FCMP_FALSE = 0 +define FCMP_OEQ = 1 +define FCMP_OGT = 2 +define FCMP_OGE = 3 +define FCMP_OLT = 4 +define FCMP_OLE = 5 +define FCMP_ONE = 6 +define FCMP_ORD = 7 +define FCMP_UNO = 8 +define FCMP_UEQ = 9 +define FCMP_UGT = 10 +define FCMP_UGE = 11 +define FCMP_ULT = 12 +define FCMP_ULE = 13 +define FCMP_UNE = 14 +define FCMP_TRUE = 15 + + +def _icmp_pred_str(pred: int) -> str: + if pred == ICMP_EQ: return "eq" + if pred == ICMP_NE: return "ne" + if pred == ICMP_SGT: return "sgt" + if pred == ICMP_SGE: return "sge" + if pred == ICMP_SLT: return "slt" + if pred == ICMP_SLE: return "sle" + if pred == ICMP_UGT: return "ugt" + if pred == ICMP_UGE: return "uge" + if pred == ICMP_ULT: return "ult" + if pred == ICMP_ULE: return "ule" + return "eq" + + +def _fcmp_pred_str(pred: int) -> str: + if pred == FCMP_FALSE: return "false" + if pred == FCMP_OEQ: return "oeq" + if pred == FCMP_OGT: return "ogt" + if pred == FCMP_OGE: return "oge" + if pred == FCMP_OLT: return "olt" + if pred == FCMP_OLE: return "ole" + if pred == FCMP_ONE: return "one" + if pred == FCMP_ORD: return "ord" + if pred == FCMP_UNO: return "uno" + if pred == FCMP_UEQ: return "ueq" + if pred == FCMP_UGT: return "ugt" + if pred == FCMP_UGE: return "uge" + if pred == FCMP_ULT: return "ult" + if pred == FCMP_ULE: return "ule" + if pred == FCMP_UNE: return "une" + if pred == FCMP_TRUE: return "true" + return "oeq" + + +def build_icmp(builder: IRBuilder*, pred: int, + lhs: Value*, rhs: Value*) -> Value*: + """%N = icmp , + Returns an SSA value of type i1.""" + return builder.build_icmp(pred, lhs, rhs) + + +def build_fcmp(builder: IRBuilder*, pred: int, + lhs: Value*, rhs: Value*) -> Value*: + """%N = fcmp , + Returns an SSA value of type i1.""" + return builder.build_fcmp(pred, lhs, rhs) + + +# ============================================================ +# Terminator instructions +# ============================================================ +def build_br(builder: IRBuilder*, target: BasicBlock*): + """br label %""" + builder.build_br(target) + + +def build_cond_br(builder: IRBuilder*, cond: Value*, + then_blk: BasicBlock*, else_blk: BasicBlock*): + """br i1 , label %, label %""" + builder.build_cond_br(cond, then_blk, else_blk) + + +def build_ret(builder: IRBuilder*, val: Value*): + """ret """ + builder.build_ret(val) + + +def build_ret_void(builder: IRBuilder*): + """ret void""" + builder.build_ret_void() + + +# ============================================================ +# Call instructions +# ============================================================ +def build_call(builder: IRBuilder*, callee: str, + args: Value*, arg_count: int, + ret_ty: LLVMType*, + is_variadic: int = 0) -> Value*: + """%N = call @() + args is the Value list head, arg_count the argument count. If ret_ty is void no SSA name is + allocated (plain call); is_variadic=1 emits the function type signature (e.g. i32 (i8*, ...)).""" + return builder.build_call(callee, args, arg_count, ret_ty, is_variadic) + + +# ============================================================ +# Indirect call instruction: call %callee() +# +# callee_val is a function-pointer SSA value (must already be a pointer type, e.g. i32(i8*)*) +# ============================================================ +def build_call_indirect(builder: IRBuilder*, callee_val: Value*, + args: Value*, arg_count: int, + ret_ty: LLVMType*) -> Value*: + """%N = call %callee() + Indirect call: callee_val is a function-pointer SSA value.""" + return builder.build_call_indirect(callee_val, args, arg_count, ret_ty) + + +# ============================================================ +# Type conversion instructions +# ============================================================ +def build_bitcast(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = bitcast to """ + return builder.build_bitcast(val, to_ty) + + +def build_sext(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = sext to """ + return builder.build_sext(val, to_ty) + + +def build_trunc(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = trunc to """ + return builder.build_trunc(val, to_ty) + + +def build_zext(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = zext to """ + return builder.build_zext(val, to_ty) + + +def build_ptrtoint(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = ptrtoint to """ + return builder.build_ptrtoint(val, to_ty) + + +def build_inttoptr(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = inttoptr to """ + return builder.build_inttoptr(val, to_ty) + + +# ============================================================ +# Float conversion instructions +# ============================================================ +def build_fpext(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fpext to """ + return builder.build_fpext(val, to_ty) + + +def build_fptrunc(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fptrunc to """ + return builder.build_fptrunc(val, to_ty) + + +def build_fp2si(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fptosi to """ + return builder.build_fp2si(val, to_ty) + + +def build_si2fp(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = sitofp to """ + return builder.build_si2fp(val, to_ty) + + +def build_fp2ui(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = fptoui to """ + return builder.build_fp2ui(val, to_ty) + + +def build_ui2fp(builder: IRBuilder*, val: Value*, + to_ty: LLVMType*) -> Value*: + """%N = uitofp to """ + return builder.build_ui2fp(val, to_ty) + + +# ============================================================ +# GEP instructions +# ============================================================ +def _is_ptr_type(ty: LLVMType*) -> int: + """Check whether ty is a Ptr type (standalone function to avoid the nested-match compiler bug)""" + if ty is None: + return 0 + match ty: + case LLVMType.Ptr(pointee): + return 1 + case _: + return 0 + + +def build_gep(builder: IRBuilder*, elem_ty: LLVMType*, + ptr: Value*, idx: Value*) -> Value*: + """%N = getelementptr , , """ + return builder.build_gep(elem_ty, ptr, idx) + + +# ============================================================ +# build_gep_array - two-index array GEP: getelementptr [N x ty], [N x ty]* %ptr, i32 0, i32 %idx +# ============================================================ +def build_gep_array(builder: IRBuilder*, array_ty: LLVMType*, + elem_ty: LLVMType*, + ptr: Value*, idx: Value*) -> Value*: + """%N = getelementptr [N x elem_ty], [N x elem_ty]* %ptr, i32 0, i32 %idx + Result type is elem_ty*.""" + return builder.build_gep_array(array_ty, elem_ty, ptr, idx) + + +# ============================================================ +# build_gep_struct - struct-field GEP: getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx +# ============================================================ +def build_gep_struct(builder: IRBuilder*, struct_ty: LLVMType*, + field_ty: LLVMType*, + ptr: Value*, field_idx: int) -> Value*: + """%N = getelementptr struct_ty, struct_ty* %ptr, i32 0, i32 %field_idx + Result type is field_ty*.""" + return builder.build_gep_struct(struct_ty, field_ty, ptr, field_idx) + + +# ============================================================ +# PhiIncoming: a phi incoming edge (value, block) pair +# inherit GSListNode[PhiIncoming] for a strongly-typed Next +# +# Note: novirtual is required (rationale in the Line comment in __function.py). +# ============================================================ +novirtual class PhiIncoming(GSListNode[PhiIncoming]): + Val: Value* + Block: BasicBlock* + + +# PhiIncoming hardcoded size: Next(8) + Val(8) + Block(8) = 24 bytes +define PHIINC_SIZE = 24 + +def new_phi_incoming(pool: memhub.MemBuddy*, + val: Value*, block: BasicBlock*) -> PhiIncoming*: + ptr: PhiIncoming* = pool.alloc(PHIINC_SIZE) + if ptr is None: return None + string.memset(ptr, 0, PHIINC_SIZE) + ptr.Val = val + ptr.Block = block + return ptr + + +# ============================================================ +# Phi instruction +# ============================================================ +def build_phi(builder: IRBuilder*, ty: LLVMType*, + incoming_head: PhiIncoming*, count: int) -> Value*: + """%N = phi [val1, %blk1], [val2, %blk2], ... + incoming_head is the PhiIncoming list head; count is the incoming-edge count.""" + return builder.build_phi(ty, incoming_head, count) + + +# ============================================================ +# SwitchCase: a case branch of the switch instruction +# +# Note: novirtual is required (rationale in the Line comment in __function.py). +# ============================================================ +novirtual class SwitchCase(GSListNode[SwitchCase]): + CaseVal: Value* + Block: BasicBlock* + + +# SwitchCase hardcoded size: Next(8) + CaseVal(8) + Block(8) = 24 bytes +define SWCASE_SIZE = 24 + +def new_switch_case(pool: memhub.MemBuddy*, + case_val: Value*, block: BasicBlock*) -> SwitchCase*: + ptr: SwitchCase* = pool.alloc(SWCASE_SIZE) + if ptr is None: return None + string.memset(ptr, 0, SWCASE_SIZE) + ptr.CaseVal = case_val + ptr.Block = block + return ptr + + +# ============================================================ +# Switch instruction +# ============================================================ +def build_switch(builder: IRBuilder*, val: Value*, + default_blk: BasicBlock*, + cases_head: SwitchCase*, case_count: int): + """switch , label % [, label % ...]""" + builder.build_switch(val, default_blk, cases_head, case_count) + + +# ============================================================ +# Select instruction +# ============================================================ +def build_select(builder: IRBuilder*, cond: Value*, + true_val: Value*, false_val: Value*) -> Value*: + """%N = select i1 , , """ + return builder.build_select(cond, true_val, false_val) + + +# ============================================================ +# Unreachable instruction +# ============================================================ +def build_unreachable(builder: IRBuilder*): + """unreachable""" + builder.build_unreachable() + + +# ============================================================ +# Atomic instructions (Phase 3) +# ============================================================ +# AtomicRMW operation constants +define ATOMIC_XCHG = 0 +define ATOMIC_ADD = 1 +define ATOMIC_SUB = 2 +define ATOMIC_AND = 3 +define ATOMIC_NAND = 4 +define ATOMIC_OR = 5 +define ATOMIC_XOR = 6 +define ATOMIC_MAX = 7 +define ATOMIC_MIN = 8 +define ATOMIC_UMAX = 9 +define ATOMIC_UMIN = 10 + +# Memory ordering constants +define ATOMIC_ORDER_NOTATOMIC = 0 +define ATOMIC_ORDER_UNORDERED = 1 +define ATOMIC_ORDER_MONOTONIC = 2 +define ATOMIC_ORDER_ACQUIRE = 3 +define ATOMIC_ORDER_RELEASE = 4 +define ATOMIC_ORDER_ACQ_REL = 5 +define ATOMIC_ORDER_SEQ_CST = 6 + + +def _atomic_op_str(op: int) -> str: + if op == ATOMIC_XCHG: return "xchg" + if op == ATOMIC_ADD: return "add" + if op == ATOMIC_SUB: return "sub" + if op == ATOMIC_AND: return "and" + if op == ATOMIC_NAND: return "nand" + if op == ATOMIC_OR: return "or" + if op == ATOMIC_XOR: return "xor" + if op == ATOMIC_MAX: return "max" + if op == ATOMIC_MIN: return "min" + if op == ATOMIC_UMAX: return "umax" + if op == ATOMIC_UMIN: return "umin" + return "xchg" + + +def _atomic_order_str(ord: int) -> str: + if ord == ATOMIC_ORDER_UNORDERED: return "unordered" + if ord == ATOMIC_ORDER_MONOTONIC: return "monotonic" + if ord == ATOMIC_ORDER_ACQUIRE: return "acquire" + if ord == ATOMIC_ORDER_RELEASE: return "release" + if ord == ATOMIC_ORDER_ACQ_REL: return "acq_rel" + if ord == ATOMIC_ORDER_SEQ_CST: return "seq_cst" + return "monotonic" + + +def build_atomicrmw(builder: IRBuilder*, op: int, + ptr: Value*, val: Value*, + order: int) -> Value*: + """%N = atomicrmw , """ + return builder.build_atomicrmw(op, ptr, val, order) + + +def build_cmpxchg(builder: IRBuilder*, ptr: Value*, + cmp_val: Value*, new_val: Value*, + succ_order: int, fail_order: int) -> Value*: + """%N = cmpxchg , , + Returns a {, i1} SSA value (simplified to an i64 representing the pair).""" + return builder.build_cmpxchg(ptr, cmp_val, new_val, succ_order, fail_order) + + +# ============================================================ +# va_arg instruction: read the next variadic argument from va_list +# ============================================================ +def build_va_arg(builder: IRBuilder*, + va_list_ptr: Value*, + arg_ty: LLVMType*) -> Value*: + """%N = va_arg , """ + return builder.build_va_arg(va_list_ptr, arg_ty) + + +# ============================================================ +# Inline assembly (Phase 3) +# ============================================================ +def build_inline_asm(builder: IRBuilder*, ret_ty: LLVMType*, + asm_text: str, constraints: str, + has_side_effects: int) -> Value*: + """%N = call asm [sideeffect] "" ""() + Simplified inline-assembly call, no-argument version.""" + return builder.build_inline_asm(ret_ty, asm_text, constraints, has_side_effects) + + +# ============================================================ +# Internal helpers +# ============================================================ +# _append_cstr write-position cache: O(1) positioning for repeated appends to the same buffer, +# avoiding O(n^2) strlen rescans (IR serialization hotspot); falls back to strlen on buffer switch/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.""" + global _g_last_dst, _g_last_pos + 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 (same as __module/__function): a stale pos appends past the first + # NUL and hides content; on failure fall back to strlen (hot path stays O(1)). + 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 >= buffer size (buffer corrupt/dangling) return early, + # else remain = dst_size - dlen underflows and dst[dlen+i] would write past the buffer. + if dlen >= dst_size: + return + slen: sizet = string.strlen(src) + remain: sizet = dst_size - dlen + if remain <= 0: return + i: sizet = 0 + while i < slen and i + 1 < remain: + dst[dlen + i] = src[i] + i += 1 + dst[dlen + i] = '\0' + _g_last_pos = dlen + i diff --git a/mlirlite/__function.vp b/mlirlite/__function.vp index 4ba6cd2..51a718d 100644 --- a/mlirlite/__function.vp +++ b/mlirlite/__function.vp @@ -1,3934 +1,3934 @@ -from stdint import * -import string -import viperlib -import memhub -import stdio -import stdlib -from linkedlist import GSListNode, GSList -from .__types import ( - LLVMType, ParamNode, TypePrint, - MlirTypeText, MlirTypeTextIsPointer, MlirFail, MlirSetContextLine, -) - - -# dump sentinel: IR list traversal step cap (cycle detection, fail-fast, aligned with __module.vp). -# A broken Next cycle would hang traversal; over the cap it prints the list name and exit(1). -define IR_DUMP_CHAIN_LIMIT = 100000 - -# Peephole master switch (T2 forwarding/ret zeroing, T3 no-op bitcast removal). -# --no-llvmpeephole zeroes it so FunctionPrint skips local peepholes for IR diffing (RenumberSSA still runs). -G_PeepEnable: bool = True - -# Function-text truncation flag: _append_cstr sets it to 1 when output hits the buffer cap. -# Checked before FunctionPrint returns and MlirFail'd: truncated MLIR must fail loudly, never silently. -_MlirOutTrunc: int = 0 - - -# ============================================================ -# Line: instruction text line -# -# One LLVM IR instruction serializes to one text line, held in a linked list. -# Extends GSListNode[Line] for typed Next: Line|CPtr -# -# NOTE: must carry novirtual, else TransPyC adds a vtable ptr (T*) but drops -# the parent GSListNode[T] Next field, misaligning the struct so field reads return garbage. -# ============================================================ -novirtual class Line(GSListNode[Line]): - Buf: str # line text (NUL-terminated) - Len: sizet # text length (excluding NUL) - - -# ============================================================ -# BasicBlock: basic block -# -# Holds name + instruction-line list + terminator flag. -# Extends GSListNode[BasicBlock] for typed Next: BasicBlock|CPtr -# Lines is a GSList[Line] pointer providing O(1) append. -# -# NOTE: must carry novirtual (same reason as Line). -# ============================================================ -novirtual class BasicBlock(GSListNode[BasicBlock]): - Name: str # block name (e.g. "entry"/"if.then") - Lines: GSList[Line]* # instruction-line list container (Head/Tail/Count) - IsTerminated: int # 1=terminator present (br/ret) - - -# ============================================================ -# Param: function parameter -# -# Extends GSListNode[Param] for typed Next: Param|CPtr -# Attrs holds parameter attribute text (e.g. "noundef"/"nocapture"/"readonly"), None=none. -# -# NOTE: must carry novirtual (same reason as Line). -# ============================================================ -novirtual class Param(GSListNode[Param]): - Ty: LLVMType* # parameter type - Name: str # parameter name (e.g. "%0"/"%a") - Attrs: str # parameter attribute text (None=none) - - -# ============================================================ -# Function: function -# -# Holds name + return type + parameter list + basic-block list. -# Extends GSListNode[Function] for typed Next: Function|CPtr -# Params/Blocks are GSList pointers providing O(1) append. -# Attrs holds function-level attribute text (e.g. "nounwind"/"noinline"), None=none. -# -# NOTE: must carry novirtual (same reason as Line). -# ============================================================ -novirtual class Function(GSListNode[Function]): - Name: str # function name - RetTy: LLVMType* # return type - Params: GSList[Param]* # parameter list container - Blocks: GSList[BasicBlock]* # basic-block list container - IsDeclared: int # 1=declaration only (declare), 0=definition (define) - IsVarArg: int # 1=variadic function (...) - Attrs: str # function attribute text (None=none) - AttrGroup: str # string attribute group content (e.g. '"section"=".text.startup"', None=none) - AttrGroupId: int # attribute group id (>0 emits #N on the function line and attributes #N at module end) - NameLen: sizet # function name length (computed once at creation, always == strlen(Name); appended last so existing field offsets stay) - DbgRef: str # DISubprogram ref (kept as-is; ignored by MLIR printing, e.g. "!3") - PeepDone: int # 1=local peephole already applied to this body (runs once; see FunctionPrint) - - -# ============================================================ -# Factory functions -# ============================================================ -# Line hardcoded size: Next(8) + Buf(8) + Len(8) = 24 bytes -define LINE_SIZE = 24 - -def new_line(pool: memhub.MemBuddy*, text: str) -> Line*: - """Create an instruction line, copying text into a newly allocated buffer""" - ptr: Line* = pool.alloc(LINE_SIZE) - if ptr is None: return None - string.memset(ptr, 0, LINE_SIZE) - if text is not None: - slen: sizet = string.strlen(text) - buf: str = pool.alloc(slen + 1) - if buf is not None: - string.strcpy(buf, text) - ptr.Buf = buf - ptr.Len = slen - return ptr - - -def _new_gslist(pool: memhub.MemBuddy*, list_size: sizet) -> rawptr: - """Allocate and zero-init a GSList container from pool (Head/Tail/Count all zero)""" - gsl: rawptr = pool.alloc(list_size) - if gsl is None: return None - string.memset(gsl, 0, list_size) - return gsl - - -# BasicBlock hardcoded size: Next(8) + Name(8) + Lines(8) + IsTerminated(4) = 32 bytes -define BLOCK_SIZE = 32 - -def new_basic_block(pool: memhub.MemBuddy*, name: str) -> BasicBlock*: - """Create a basic block""" - ptr: BasicBlock* = pool.alloc(BLOCK_SIZE) - if ptr is None: return None - string.memset(ptr, 0, BLOCK_SIZE) - ptr.Name = name - ptr.Lines = _new_gslist(pool, 24) # GSList: Head+Tail+Count - ptr.IsTerminated = 0 - return ptr - - -# Param hardcoded size: Next(8) + Ty(8) + Name(8) + Attrs(8) = 32 bytes -define PARAM_SIZE = 32 - -def new_param(pool: memhub.MemBuddy*, ty: LLVMType*, - name: str) -> Param*: - """Create a function parameter""" - ptr: Param* = pool.alloc(PARAM_SIZE) - if ptr is None: return None - string.memset(ptr, 0, PARAM_SIZE) - ptr.Ty = ty - ptr.Name = name - ptr.Attrs = None - return ptr - - -# Function hardcoded size: Next(8) + Name(8) + RetTy(8) + Params(8) + Blocks(8) + IsDeclared(4) + IsVarArg(4) + Attrs(8) + AttrGroup(8) + AttrGroupId(8) + NameLen(8) + DbgRef(8) + PeepDone(8) = 96 bytes (same layout as llvmlite) -define FUNC_SIZE = 96 - -def new_function(pool: memhub.MemBuddy*, name: str, - ret_ty: LLVMType*) -> Function*: - """Create a function""" - ptr: Function* = pool.alloc(FUNC_SIZE) - if ptr is None: return None - string.memset(ptr, 0, FUNC_SIZE) - ptr.Name = name - ptr.RetTy = ret_ty - ptr.Params = _new_gslist(pool, 24) # GSList: Head+Tail+Count - ptr.Blocks = _new_gslist(pool, 24) - ptr.IsDeclared = 0 - ptr.Attrs = None - ptr.AttrGroup = None - ptr.AttrGroupId = 0 - ptr.PeepDone = 0 - # NameLen computed once: Name never changes, so NameLen == strlen(Name) (avoids repeated strlen). - # name=None sets 0, matching new_line's null convention (explicit for fixed semantics, not allocator behavior). - if name is not None: - ptr.NameLen = string.strlen(name) - else: - ptr.NameLen = 0 - return ptr - - -# ============================================================ -# List operations (delegating to GSList.append) -# ============================================================ -def function_add_param(func: Function*, param: Param*): - """Append a parameter to the function's parameter list""" - if func is None or param is None: return - # NOTE: use a local variable + type parameter (see function_move_block_to_end): chaining - # func.Params.append() mis-resolves to LinkedNode.append and corrupts memory 48 bytes before the container. - params: GSList[Param]* = func.Params - if params is None: return - params.append(param) - - -def function_add_block(func: Function*, block: BasicBlock*): - """Append a basic block to the function's block list""" - if func is None or block is None: return - blocks: GSList[BasicBlock]* = func.Blocks - if blocks is None: return - blocks.append(block) - - -def function_move_block_to_end(func: Function*, block: BasicBlock*): - """Move block to the end of the function's block list (BB text order == control-flow order). - - Fixes non-monotonic SSA numbering (BBs are created in order but instructions are emitted in - control-flow order) that makes llc error; called at position_at_end so BB order matches position order. - NOTE: must use the typed GSList[BasicBlock] parameter, else TransPyC builds an incomplete struct - type and blocks.Head/Tail/Count field reads return garbage. - """ - if func is None or block is None: return - blocks: GSList[BasicBlock]* = func.Blocks - if blocks is None: return - - # Already Tail; nothing to move - if blocks.Tail is block: - return - - # Remove block from the list - if blocks.Head is block: - # block is Head; advance Head directly - blocks.Head = block.Next - else: - # Walk to find the predecessor - prev: BasicBlock* = blocks.Head - while prev is not None and prev.Next is not block: - prev = prev.Next - if prev is None: - # block not in list; append directly - blocks.append(block) - return - prev.Next = block.Next - - # block.Next handled by the removal logic; reset to None - block.Next = None - - # Append at the end (no blocks.append call to avoid double-incrementing Count) - if blocks.Tail is not None: - blocks.Tail.Next = block - blocks.Tail = block - - -def block_append_line(block: BasicBlock*, line: Line*): - """Append an instruction line to the basic block""" - if block is None or line is None: return - lines: GSList[Line]* = block.Lines - if lines is None: return - lines.append(line) - - -def block_set_terminated(block: BasicBlock*, val: int): - """Set the block terminator flag (cross-module call to avoid opaque chained field access)""" - if block is None: return - block.IsTerminated = val - - -def block_get_terminated(block: BasicBlock*) -> int: - """Get the block terminator flag (cross-module call to avoid opaque chained field access)""" - if block is None: - return 1 - return block.IsTerminated - - -def block_append_text(pool: memhub.MemBuddy*, block: BasicBlock*, - text: str): - """Create an instruction line and append it to the basic block""" - if block is None or text is None: return - line: Line* = new_line(pool, text) - if line is not None: - block_append_line(block, line) - - -def block_insert_text_before_terminator(pool: memhub.MemBuddy*, - block: BasicBlock*, - text: str): - """Create an instruction line and insert it before the block terminator. - - If the block is not terminated, append at the end; if terminated (br/ret), insert before - the terminator so alloca and similar instructions do not land after it. - """ - if block is None or text is None: return - if block.Lines is None: return - line: Line* = new_line(pool, text) - if line is None: return - # Block not terminated: append directly - if block.IsTerminated == 0: - block_append_line(block, line) - return - # Block terminated: insert before Tail (the terminator) - lines: GSList[Line]* = block.Lines - if lines.Head is None: - block_append_line(block, line) - return - # Single node: insert before Head - if lines.Head is lines.Tail: - line.Next = lines.Head - lines.Head = line - lines.Count += 1 - return - # Find the predecessor of Tail - prev: Line* = lines.Head - while prev is not None and prev.Next is not lines.Tail: - prev = prev.Next - if prev is None: - # No predecessor found; fall back to append - block_append_line(block, line) - return - # Insert line between prev and Tail - line.Next = lines.Tail - prev.Next = line - lines.Count += 1 - - -# ============================================================ -# Attribute setters -# ============================================================ -def function_set_attrs(func: Function*, attrs: str): - """Set function-level attribute text (e.g. "nounwind"/"noinline")""" - if func is None: return - func.Attrs = attrs - - -_g_attr_group_counter: int = 0 - -def function_set_attr_group(func: Function*, group: str): - """Set function string attribute group (e.g. '"section"=".text.startup"') and assign #N. - - LLVM string attributes (with a value, e.g. section) cannot be inlined on the signature - line; they must use a group: the function line emits #N and the group is emitted at the end. - """ - global _g_attr_group_counter - if func is None or group is None: - return - func.AttrGroup = group - _g_attr_group_counter += 1 - func.AttrGroupId = _g_attr_group_counter - - -def param_set_attrs(param: Param*, attrs: str): - """Set parameter attribute text (e.g. "noundef"/"nocapture"/"readonly")""" - if param is None: return - param.Attrs = attrs - - -def function_set_dbg(func: Function*, ref: str): - """Set the function-level debug ref (DISubprogram node ref, e.g. "!3"). - - Kept as-is: MLIR printing ignores it (FunctionPrint emits no !dbg), so this only - records the model without changing output. ref=None clears it. - """ - if func is None: return - func.DbgRef = ref - - -# ============================================================ -# _ll_name_needs_quote - whether a symbol name needs @"..." quoting in MLIR -# -# WARNING: the rule differs from LLVM (measured on mlir-opt 22.1.8): -# - A bare identifier's first char must be a letter or '_': leading digit, '.', '$' must be quoted. -# => TPV's {sha1}.{name} all start with a hex digit, so all need quotes; -# => the string-constant global @.str starts with '.', so it needs quotes too. -# - Later chars allow only [A-Za-z0-9_$] and '.'. -# - '-' is illegal in a bare MLIR name (LLVM allows it); measured error "expected '('". -# - '['/']' (generic specialization names) are likewise illegal. -# This predicate takes the conservative direction: when unsure, quote (@"..." is legal for any name). -# ============================================================ -def _ll_name_needs_quote(name: str) -> int: - """MLIR bare-name predicate: returns 1 (needs quotes) unless it matches [A-Za-z_][A-Za-z0-9_$.]*""" - if name is None: - return 0 - name_len: sizet = string.strlen(name) - if name_len == 0: - return 1 - c0: char = name[0] - if not ((c0 >= 'a' and c0 <= 'z') or (c0 >= 'A' and c0 <= 'Z') or c0 == '_'): - return 1 - i: sizet = 1 - while i < name_len: - ch: char = name[i] - if not ((ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or - (ch >= '0' and ch <= '9') or ch == '_' or ch == '$' or ch == '.'): - return 1 - i += 1 - return 0 - - -# ============================================================ -# _MlirSymRefAppend - emit a symbol reference (unified quoting rule) -# -# name does NOT include a leading '@'; an already-quoted form (first char '"') is emitted as-is. -# Used where a symbol appears as an operand (e.g. llvm.mlir.addressof), sharing the same -# predicate with _MlirFuncSig / call target / global name. -# ============================================================ -def _MlirSymRefAppend(out: str, outsz: sizet, name: str): - if name is None or name[0] == '\0': - MlirFail("MLIR render: empty symbol name") - if name[0] == '"': - _append_cstr(out, outsz, "@") - _append_cstr(out, outsz, name) - return - if _ll_name_needs_quote(name) != 0: - _append_cstr(out, outsz, "@\"") - _append_cstr(out, outsz, name) - _append_cstr(out, outsz, "\"") - return - _append_cstr(out, outsz, "@") - _append_cstr(out, outsz, name) - - -# ============================================================ -# RenumberSSA: renumber numeric SSA names within a function -# -# Fixes the non-monotonic SSA names in text output caused by build_alloca_at_entry -# inserting allocas before the entry block terminator (llc error: -# "instruction expected to be numbered '%N' or greater"). -# -# Example: entry originally %0-%6 + br; alloca_at_entry inserts %28/%41 before br, -# so text order is entry(%0..%6, %28, %41) -> if.then(%7..%27), -# placing %28 before %7, which llc rejects. -# -# Algorithm: -# 1. Scan all BB instruction lines, collect numeric SSA names %N in text-output order -# 2. Build an old->new map by first-occurrence order (incrementing from 0) -# 3. Replace %N in every instruction line text with the new number -# -# NOTE: -# - Only pure numeric SSA names (%0, %1, ...) are renumbered -# - Named SSA names (%entry, %.tmp, %self), block labels and global symbols @N are not -# - Only called for define (with a body); declare has no body to process -# - TransPyV params are mostly named (%self/%env/%n), so no numeric param renumbering needed -# ============================================================ - -# Whether ch is a decimal digit -def _is_digit_ch(ch: char) -> int: - """Return 1 if ch is '0'-'9'. - - ch is a value type (char = i8); TPC has fixed is-None handling for value types - (always false, generating an i1 0), so no null deref occurs. - """ - if ch is None: - return 0 - if ch >= '0' and ch <= '9': - return 1 - return 0 - - -# ============================================================ -# Safe i32 array access (explicit byte offset) -# -# TransPyC's ptr[i] pointer indexing may be treated as byte indexing (i8*) instead of i32* -# indexing, corrupting memory. Must compute the byte offset explicitly: addr = base + idx * 4, -# then cast to i32*. See HandlesTranslator.py:358's uint64(ptr) + offset * sizeof usage. -# ============================================================ -def _map_get_i32(map_arr: str, idx: int) -> int: - """Read i32 array map_arr[idx] (explicit byte offset idx*4). - - Casts the byte offset to i32* via int*(uint64), then dereferences with defef. - TPC has fixed type-union casts (_HandleTypeUnionCastLlvm fallback), so this form works. - """ - if map_arr is None: - return -1 - byte_off: uint64 = uint64(map_arr) + idx * 4 - elem_ptr: int* = int*(byte_off) - return *elem_ptr - - -def _map_set_i32(map_arr: str, idx: int, val: int): - """Write map_arr[idx] = val (i32 array, explicit byte offset idx*4). - - Casts via int*(uint64) and assigns through defefas. - TPC has fixed type-union casts, so this form works. - """ - if map_arr is None: - return - byte_off: uint64 = uint64(map_arr) + idx * 4 - elem_ptr: int* = int*(byte_off) - *elem_ptr = val - - -def _scan_max_ssa_in_text(text: str, cur_max: int) -> int: - """Scan text for the largest numeric %N and return max(cur_max, found). - - Only matches '%' immediately followed by a digit; skips named SSA names (%entry etc.). - """ - result: int = cur_max - p: str = text - while p is not None and p[0] != '\0': - if p[0] == '%': - nxt: str = p + 1 - if nxt is not None and _is_digit_ch(nxt[0]) != 0: - num: int = string.atoi(nxt) - if num > result: - result = num - p = p + 1 - return result - - -def _find_max_ssa_number(func: Function*) -> int: - """Walk all BB instruction lines for the largest %N; returns -1 if no numeric SSA names""" - max_n: int = -1 - if func is None or func.Blocks is None: - return max_n - blocks: GSList[BasicBlock]* = func.Blocks - blk: BasicBlock* = blocks.Head - blk_steps: sizet = 0 - while blk is not None: - blk_steps += 1 - if blk_steps > IR_DUMP_CHAIN_LIMIT: - stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa scan aborted)\n", func.Name) - stdlib.exit(1) - if blk.Lines is not None: - lines: GSList[Line]* = blk.Lines - line: Line* = lines.Head - line_steps: sizet = 0 - while line is not None: - line_steps += 1 - if line_steps > IR_DUMP_CHAIN_LIMIT: - stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa scan aborted)\n", - func.Name, blk.Name) - stdlib.exit(1) - if line.Buf is not None: - max_n = _scan_max_ssa_in_text(line.Buf, max_n) - line = line.Next - blk = blk.Next - return max_n - - -def _assign_new_numbers(text: str, - map_arr: str, - max_n: int, next_new: int) -> int: - """Scan text; for each numeric %N, assign next_new if map_arr[N]==-1. - - Returns the updated next_new. Mapping is built in first-occurrence order. - """ - result: int = next_new - p: str = text - while p is not None and p[0] != '\0': - if p[0] == '%': - nxt: str = p + 1 - if nxt is not None and _is_digit_ch(nxt[0]) != 0: - num: int = string.atoi(nxt) - if num >= 0 and num <= max_n: - if _map_get_i32(map_arr, num) == -1: - _map_set_i32(map_arr, num, result) - result += 1 - p = p + 1 - return result - - -def _replace_ssa_numbers(text: str, - pool: memhub.MemBuddy*, - map_arr: str, - max_n: int) -> str: - """Build new text with each %N replaced by its mapped new number. - - Char-by-char scan: a numeric %N is looked up and substituted, others copied as-is. - Returns a newly allocated buffer (caller need not free the old Buf; pool manages it). - """ - src_len: sizet = string.strlen(text) - # New buffer: worst case each number grows in digits; allocate src_len*2 + 64 - new_size: sizet = src_len * 2 + 64 - new_buf: str = pool.alloc(new_size) - if new_buf is None: - return None - # Allocate the digit buffer once (reused in the loop) - num_buf: str = pool.alloc(16) - if num_buf is None: - new_buf[0] = '\0' - return new_buf - - dst_idx: sizet = 0 - src_p: str = text - while src_p is not None and src_p[0] != '\0': - if dst_idx + 16 >= new_size: - # Buffer too small; truncate - break - if src_p[0] == '%': - nxt: str = src_p + 1 - if nxt is not None and _is_digit_ch(nxt[0]) != 0: - num: int = string.atoi(nxt) - # Range-check before reading map_arr: when num > max_n the array holds only - # (max_n+1)*4 bytes, so a read-before-check would read out of bounds. - if num >= 0 and num <= max_n: - new_num: int = _map_get_i32(map_arr, num) - if new_num != -1: - # Write '%' - new_buf[dst_idx] = '%' - dst_idx += 1 - # Write the new number - num_buf[0] = '\0' - viperlib.snprintf(num_buf, 16, "%d", new_num) - k: sizet = 0 - while num_buf[k] != '\0' and dst_idx + 1 < new_size: - new_buf[dst_idx] = num_buf[k] - dst_idx += 1 - k += 1 - # Skip the old digit chars - src_p = src_p + 1 - while src_p is not None and _is_digit_ch(src_p[0]) != 0: - src_p = src_p + 1 - continue - # Ordinary char: copy directly - new_buf[dst_idx] = src_p[0] - dst_idx += 1 - src_p = src_p + 1 - - new_buf[dst_idx] = '\0' - return new_buf - - -def RenumberSSA(func: Function*, pool: memhub.MemBuddy*): - """Renumber all numeric SSA names in the function so text-output order is monotonic. - - Called before FunctionPrint serializes the body; only applies to define (IsDeclared==0) - functions with non-empty Blocks. - """ - if func is None or pool is None: - return - if func.IsDeclared == 1: - return # declare has no body - if func.Blocks is None: - return - - # 1. First pass: find the largest SSA number - max_n: int = _find_max_ssa_number(func) - if max_n < 0: - return # no numeric SSA names - - # 2. Allocate and init map_arr[0..max_n] = -1; stored as i8* (pool.alloc), accessed via - # _map_get_i32/_map_set_i32; memset to 0xFF (-1 per 4-byte i32) to avoid ptr[i] indexing issues. - map_bytes: sizet = (max_n + 1) * 4 # int = 4 bytes - map_arr: str = pool.alloc(map_bytes) - if map_arr is None: - return - string.memset(map_arr, 0xFF, map_bytes) # 0xFF * 4 = 0xFFFFFFFF = -1 - - # 3. Second pass: assign new numbers in text order - next_new: int = 0 - blocks: GSList[BasicBlock]* = func.Blocks - blk: BasicBlock* = blocks.Head - blk_steps: sizet = 0 - while blk is not None: - blk_steps += 1 - if blk_steps > IR_DUMP_CHAIN_LIMIT: - stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa renumber aborted)\n", func.Name) - stdlib.exit(1) - if blk.Lines is not None: - lines: GSList[Line]* = blk.Lines - line: Line* = lines.Head - line_steps: sizet = 0 - while line is not None: - line_steps += 1 - if line_steps > IR_DUMP_CHAIN_LIMIT: - stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa renumber aborted)\n", - func.Name, blk.Name) - stdlib.exit(1) - if line.Buf is not None: - next_new = _assign_new_numbers(line.Buf, map_arr, max_n, next_new) - line = line.Next - blk = blk.Next - - # 4. Check whether any renumbering is needed (old != new) - need_renumber: int = 0 - i: int = 0 - while i <= max_n: - v: int = _map_get_i32(map_arr, int(i)) - if v != -1 and v != i: - need_renumber = 1 - break - i += 1 - if need_renumber == 0: - return # already monotonic; no replacement needed - - # 5. Third pass: replace every instruction line text - blk = blocks.Head - blk_steps = 0 - while blk is not None: - blk_steps += 1 - if blk_steps > IR_DUMP_CHAIN_LIMIT: - stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa replace aborted)\n", func.Name) - stdlib.exit(1) - if blk.Lines is not None: - lines: GSList[Line]* = blk.Lines - line: Line* = lines.Head - line_steps = 0 - while line is not None: - line_steps += 1 - if line_steps > IR_DUMP_CHAIN_LIMIT: - stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa replace aborted)\n", - func.Name, blk.Name) - stdlib.exit(1) - if line.Buf is not None: - new_buf: str = _replace_ssa_numbers( - line.Buf, pool, map_arr, max_n) - if new_buf is not None: - line.Buf = new_buf - line.Len = string.strlen(new_buf) - line = line.Next - blk = blk.Next - - -# ============================================================ -# Local peephole optimization (text layer, before output, semantics-preserving) -# T1: ret 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 - st_ok: int = 0 - st_count: int = 0 - st_line_order: int = -1 - load_ltoks: str = pool.alloc(8 * 64) - load_cnt: int = 0 - load_entry_order: int64 = 0 - alloca_order: int = -1 - order: int = 0 - unsafe: int = 0 - # First pass: count this alloca's store/load/other refs and line order - blk_s = blk - while blk_s is not None: - l_s: Line* = None - if blk_s.Lines is not None: - scan_lines: GSList[Line]* = blk_s.Lines - l_s = scan_lines.Head - while l_s is not None: - sb: str = l_s.Buf - if sb is not None: - is_alloc_def: int = 0 - if sb[0] == '%': - if _peep_is_alloc_of(sb, a_name, a_name_len) != 0: - is_alloc_def = 1 - if is_alloc_def == 0 and _peep_last_ref_eq(sb, a_name, a_name_len) != 0: - if _peep_is_startwith(sb, "store ") != 0: - st_count += 1 - if st_count == 1: - vcomma: str = string.strstr(sb + 6, ", ") - if vcomma is not None: - vlen: sizet = sizet(uint64(vcomma) - uint64(sb + 6)) - if vlen > 0: - vbuf: str = pool.alloc(vlen + 1) - if vbuf is not None: - string.strncpy(vbuf, sb + 6, vlen) - vbuf[vlen] = '\0' - st_val = vbuf - st_ok = 1 - else: - unsafe = 1 - if blk_s == entry_blk: - st_line_order = order - elif string.strstr(sb, " = load ") is not None: - ls: str = string.strstr(sb, "%") - if ls is not None and load_cnt < 64: - lname_len: sizet = sizet( - uint64(string.strstr(sb, " = load ")) - uint64(ls)) - lname: str = pool.alloc(lname_len + 1) - if lname is not None: - string.strncpy(lname, ls + 1, lname_len - 1) - lname[lname_len - 1] = '\0' - slotp: str* = (str*)(uint64(load_ltoks) + load_cnt * 8) - slotp[0] = lname - load_cnt += 1 - if blk_s == entry_blk and st_line_order < 0: - load_entry_order += 1 - else: - # lname alloc failed: this load won't enter load_ltoks, but the - # delete phase still removes its def line by slot name, leaving a - # dangling use. Conservatively abandon T2 forwarding for this slot. - unsafe = 1 - else: - # Loads exceed record capacity (load_ltoks=8*64, cap 64) or the line has - # no '%' to parse a def name: unrecorded loads would still have their - # " = load " def lines deleted by slot name, leaving dangling uses - # (llc: "use of undefined value", e.g. strcmp short-circuit >64 branch). - # Only a fully recorded load set allows deleting all defs, so - # conservatively abandon the whole T2 forwarding for this slot (IR stays valid). - unsafe = 1 - else: - unsafe = 1 - if sb is not None: - order += 1 - l_s = l_s.Next - blk_s = blk_s.Next - # Criteria - safe_val: str = None - if unsafe == 0 and st_count == 1 and st_ok != 0 and st_line_order >= 0 \ - and load_cnt > 0 and load_entry_order == 0 and st_val is not None: - pct: str = string.strchr(st_val, '%') - if pct is None: - sp1: str = string.strchr(st_val, ' ') - if sp1 is not None and sp1[1] != 0: - safe_val = sp1 + 1 - else: - safe_val = st_val - else: - if string.strchr(pct, ' ') is None and string.strchr(pct, ',') is None: - safe_val = pct - if safe_val is not None: - # Second pass: replace load-name references - # safe_val is constant during this slot's replacement; strlen computed once per slot - safe_len: sizet = string.strlen(safe_val) - for li2 in range(int(load_cnt)): - slotsp: str* = (str*)(uint64(load_ltoks) + li2 * 8) - ltok: str = slotsp[0] - if ltok is not None: - # ltok is constant across the load name's function-wide scan; strlen computed once - ltok_len: sizet = string.strlen(ltok) - blk_r = blk - while blk_r is not None: - lr: Line* = None - if blk_r.Lines is not None: - repl_lines: GSList[Line]* = blk_r.Lines - lr = repl_lines.Head - while lr is not None: - if lr.Buf is not None: - nb2: str = _peep_replace_token(pool, lr.Buf, ltok, safe_val, - ltok_len, safe_len) - if nb2 is not None and nb2 is not lr.Buf: - lr.Buf = nb2 - lr = lr.Next - blk_r = blk_r.Next - # Delete: alloca def / the single store / all load lines - blk_d = blk - while blk_d is not None: - ld: Line* = None - if blk_d.Lines is not None: - kill_lines: GSList[Line]* = blk_d.Lines - ld = kill_lines.Head - while ld is not None: - db: str = ld.Buf - if db is not None: - kill: int = 0 - if db[0] == '%' and db[1] == a_name[0]: - if _peep_is_alloc_of(db, a_name, a_name_len) != 0: - kill = 1 - if kill == 0 and _peep_last_ref_eq(db, a_name, a_name_len) != 0: - if _peep_is_startwith(db, "store ") != 0 \ - or string.strstr(db, " = load ") is not None: - kill = 1 - if kill != 0: - ld.Buf = None - ld = ld.Next - blk_d = blk_d.Next - e_line = e_line.Next - - # ---------- T3: no-op bitcast removal ---------- - _peep_noop_bitcast(func, pool) - - -def _peep_noop_bitcast(func: Function*, pool: memhub.MemBuddy*): - """T3: remove no-op bitcast (bitcast %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 - -# 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, _MlirTablesValid, _MlirTablesPool - if pool is None: - MlirFail("MLIR render: memory pool is null") - if _MlirTablesValid != 0 and _MlirTablesPool == pool: - # Reuse the live buffers: reset the per-function logical state. Entries past the reset counts - # are unreachable (all lookups scan [0, count)), so no table body needs clearing; only the - # used-name bitmap must be zeroed because _MlirCollectUsedCNames only ever sets bits. - _MLIR_Pool = pool - _MLIR_Pass = 0 - _MLIR_ConstCount = 0 - _MLIR_ConstNameSeq = 0 - _MLIR_AliasCount = 0 - string.memset(_MLIR_UsedC, 0, _MLIR_ConstCap * 4) - return - _MLIR_Pool = pool - _MLIR_Pass = 0 - _MLIR_ConstCount = 0 - _MLIR_ConstNameSeq = 0 - _MLIR_AliasCount = 0 - _MLIR_ConstCap = MLIR_CONST_CAP - _MLIR_ConstTy = pool.alloc(_MLIR_ConstCap * 8) - _MLIR_ConstVal = pool.alloc(_MLIR_ConstCap * 8) - _MLIR_ConstName = pool.alloc(_MLIR_ConstCap * 8) - _MLIR_UsedC = pool.alloc(_MLIR_ConstCap * 4) - _MLIR_AliasCap = MLIR_ALIAS_CAP - _MLIR_AliasFrom = pool.alloc(_MLIR_AliasCap * 8) - _MLIR_AliasTo = pool.alloc(_MLIR_AliasCap * 8) - if _MLIR_ConstTy is None or _MLIR_ConstVal is None or _MLIR_ConstName is None \ - or _MLIR_UsedC is None or _MLIR_AliasFrom is None or _MLIR_AliasTo is None: - MlirFail("MLIR render: constant/alias table allocation failed") - string.memset(_MLIR_ConstTy, 0, _MLIR_ConstCap * 8) - string.memset(_MLIR_ConstVal, 0, _MLIR_ConstCap * 8) - string.memset(_MLIR_ConstName, 0, _MLIR_ConstCap * 8) - string.memset(_MLIR_UsedC, 0, _MLIR_ConstCap * 4) - string.memset(_MLIR_AliasFrom, 0, _MLIR_AliasCap * 8) - string.memset(_MLIR_AliasTo, 0, _MLIR_AliasCap * 8) - _SC_Op = pool.alloc(64) - _SC_Tb = pool.alloc(512) - _SC_Ty = pool.alloc(4096) - _SC_Ty2 = pool.alloc(4096) - _SC_V1 = pool.alloc(512) - _SC_V2 = pool.alloc(512) - _SC_V3 = pool.alloc(512) - _SC_Callee = pool.alloc(512) - _SC_Args = pool.alloc(2048) - _SC_ArgTs = pool.alloc(8192) - _SC_IdxV = pool.alloc(1024) - _SC_IdxT = pool.alloc(4096) - _SC_Mt = pool.alloc(4096) - _SC_Tmp = pool.alloc(8192) - _SC_Tmp2 = pool.alloc(4096) - _SC_P1 = pool.alloc(512) - _SC_P2 = pool.alloc(512) - _SC_B1 = pool.alloc(4096) - _SC_B2 = pool.alloc(4096) - _SC_Buf = pool.alloc(4096) - if _SC_Op is None or _SC_Ty is None or _SC_Mt is None or _SC_Tmp is None: - MlirFail("MLIR render: parse scratch allocation failed") - _SC_Op[0] = '\0' - _SC_Ty[0] = '\0' - _SC_Mt[0] = '\0' - _SC_Tmp[0] = '\0' - _MlirTablesPool = pool - _MlirTablesValid = 1 - - -# ============================================================ -# Constant table / alias table -# ============================================================ -def _MlirAliasLookup(name: str) -> str: - """Look up the alias table; returns None on miss""" - if name is None: - return None - for i in range(int(_MLIR_AliasCount)): - sp: str* = _mlir_slotp(_MLIR_AliasFrom, int(i)) - if sp[0] is not None and string.strcmp(sp[0], name) == 0: - st: str* = _mlir_slotp(_MLIR_AliasTo, int(i)) - return st[0] - return None - - -def _MlirAliasGrow(): - """扩容 alias 表:池内分配新块并拷贝旧内容,容量翻倍(旧块留在池里,不回收)。 - - 原实现把上限写死为 MLIR_ALIAS_CAP(512),超出直接 MlirFail 中断整次编译—— - 这是资源限制,不该让编译失败。大函数里 no-op bitcast(别名)条数会超过它。 - """ - global _MLIR_AliasFrom, _MLIR_AliasTo, _MLIR_AliasCap - old_cap: int = _MLIR_AliasCap - nb_from: bytes = _MLIR_Pool.alloc(old_cap * 16) - nb_to: bytes = _MLIR_Pool.alloc(old_cap * 16) - if nb_from is None or nb_to is None: - MlirFail("MLIR render: alias table growth allocation failed") - string.memcpy(nb_from, _MLIR_AliasFrom, old_cap * 8) - string.memcpy(nb_to, _MLIR_AliasTo, old_cap * 8) - string.memset(nb_from + old_cap * 8, 0, old_cap * 8) - string.memset(nb_to + old_cap * 8, 0, old_cap * 8) - _MLIR_AliasFrom = nb_from - _MLIR_AliasTo = nb_to - _MLIR_AliasCap = old_cap * 2 - - -def _MlirConstGrow(): - """扩容常量表 + 自动常量名空间(%c)+ 已用名表:池内分配翻倍新块并拷贝旧内容。 - - 原实现三处都钉死在 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: - MlirFail("MLIR render: float bit-pattern width mismatches type (f16/f32/f64/f128 need 4/8/16/32 hex digits respectively)") - return val - # Already decimal text: use as-is - return val - - -def _MlirConstFail(tytext: str, valtext: str): - """Unsupported constant form -> loud failure including the type and value. - - MlirFail gives no backtrace, so print (MLIR 类型, 常量文本) — otherwise an - "unsupported constant operand form" is unattributable (which pointer/aggregate constant, - and from which operand shape). Supports i/f literals, true/false, null/zeroinitializer and - `@sym`; anything else (e.g. a pointer/aggregate-typed literal or a constant expression) - lands here and must be lowered explicitly. - """ - tlen: sizet = 0 - vlen: sizet = 0 - if tytext is not None: tlen = string.strlen(tytext) - if valtext is not None: vlen = string.strlen(valtext) - bsize: sizet = tlen + vlen + 160 - buf: str = _MLIR_Pool.alloc(bsize) - if buf is None: - MlirFail("MLIR render: unsupported constant operand form") - viperlib.snprintf(buf, bsize, "MLIR render: unsupported constant operand form (type '%s', value '%s')", tytext, valtext) - MlirFail(buf) - - -def _MlirEmitOneConst(out: str, outsz: sizet, mt: str, val: str, nm: str): - """Emit one constant-materialization op""" - _append_cstr(out, outsz, " ") - _append_cstr(out, outsz, nm) - if val[0] == '@': - # Symbol reference (global/function address): use the unified quoting rule - # (LLVM and MLIR bare-name rules differ: MLIR disallows leading digit/'.' and '-') - _append_cstr(out, outsz, " = llvm.mlir.addressof ") - _MlirSymRefAppend(out, outsz, val + 1) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, mt) - elif string.strcmp(val, "null") == 0 or string.strcmp(val, "zeroinitializer") == 0: - _append_cstr(out, outsz, " = llvm.mlir.zero : ") - _append_cstr(out, outsz, mt) - else: - lit: str = val - if mt[0] == 'f': - lit = _MlirFloatLiteral(val, mt) - elif mt[0] != 'i': - _MlirConstFail(mt, val) - _append_cstr(out, outsz, " = llvm.mlir.constant(") - _append_cstr(out, outsz, lit) - # i1's `true`/`false` are MLIR bare boolean keywords, no type suffix allowed: - # `llvm.mlir.constant(false : i1)` makes mlir-opt error "expected ')'". - # Other scalars must be written `value : type` (otherwise treated as an untyped attr, mismatching the result type). - if string.strcmp(lit, "true") != 0 and string.strcmp(lit, "false") != 0: - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, mt) - _append_cstr(out, outsz, ") : ") - _append_cstr(out, outsz, mt) - _append_cstr(out, outsz, "\n") - - -def _MlirEmitConsts(out: str, outsz: sizet): - """Emit all constant-materialization ops at the entry block head in registration order""" - for i in range(int(_MLIR_ConstCount)): - st: str* = _mlir_slotp(_MLIR_ConstTy, int(i)) - sv: str* = _mlir_slotp(_MLIR_ConstVal, int(i)) - sn: str* = _mlir_slotp(_MLIR_ConstName, int(i)) - _MlirEmitOneConst(out, outsz, st[0], sv[0], sn[0]) - - -# ============================================================ -# Parsing primitives for intermediate instruction text -# ============================================================ -def _mlir_ws(p: str) -> str: - """Skip whitespace (space/tab/newline)""" - if p is None: - return None - while p[0] == ' ' or p[0] == '\t' or p[0] == '\n' or p[0] == '\r': - p = p + 1 - return p - - -def _mlir_delim(ch: int) -> int: - """Whether ch ends a token""" - if ch == 0 or ch == 32 or ch == 9 or ch == 10 or ch == 13: - return 1 - if ch == ',' or ch == '(' or ch == ')' or ch == '[' or ch == ']' \ - or ch == ':' or ch == '=': - return 1 - return 0 - - -def _mlir_read_value(p: str, out: str, outsz: sizet) -> str: - """Read a value token (%name / @"name" / %"name" / literal); returns the new cursor""" - p = _mlir_ws(p) - n: sizet = 0 - if p[0] == '\0': - out[0] = '\0' - return p - if p[0] == '%' or p[0] == '@': - if n + 1 < outsz: - out[n] = p[0] - n += 1 - p = p + 1 - if p[0] == '"': - if n + 1 < outsz: - out[n] = '"' - n += 1 - p = p + 1 - while p[0] != '\0' and p[0] != '"': - if n + 1 < outsz: - out[n] = p[0] - n += 1 - p = p + 1 - if p[0] == '"': - if n + 1 < outsz: - out[n] = '"' - n += 1 - p = p + 1 - out[n] = '\0' - return p - while p[0] != '\0' and _mlir_delim(p[0]) == 0: - if n + 1 < outsz: - out[n] = p[0] - n += 1 - p = p + 1 - out[n] = '\0' - return p - - -def _mlir_read_type(p: str, out: str, outsz: sizet, nofunc: int = 0) -> str: - """Read a type token (MLIR form !llvm... or scalar iN/fN, also LLVM aggregate forms). - - Bracket balancing covers `[] {} () <>`: MLIR's !llvm.array<...>/!llvm.struct<(...)> contain - spaces and commas and must be read whole by depth counting; bracketless types (!llvm.ptr) - end at a top-level delimiter. - - With nofunc=1, does NOT absorb the following ` (...)` function-type suffix: in a call's return - type position, `(...)` (LLVM syntax) is the callee's parameter type list, not part of the return - type, so it must be left to _MlirCall's variadic branch (see the comment there). - """ - p = _mlir_ws(p) - n: sizet = 0 - if p[0] == '\0': - out[0] = '\0' - return p - c0: int = p[0] - if c0 == '!' or c0 == '[' or c0 == '{' or c0 == '(': - d: int = 0 - while p[0] != '\0': - ch: int = p[0] - if ch == '[' or ch == '{' or ch == '(' or ch == '<': - d += 1 - elif ch == ']' or ch == '}' or ch == ')' or ch == '>': - if n + 1 < outsz: - out[n] = char(ch) - n += 1 - p = p + 1 - d -= 1 - if d <= 0: - break - continue - elif d == 0 and (ch == ' ' or ch == ',' or ch == '\t' - or ch == '\n' or ch == '\r'): - # Top-level delimiter: bracketless types (e.g. !llvm.ptr) end here - break - if n + 1 < outsz: - out[n] = char(ch) - n += 1 - p = p + 1 - while p[0] == '*': - if n + 1 < outsz: - out[n] = '*' - n += 1 - p = p + 1 - out[n] = '\0' - return p - if c0 == '%': - p = _mlir_read_value(p, out, outsz) - n = string.strlen(out) - while p[0] == '*': - if n + 1 < outsz: - out[n] = '*' - n += 1 - p = p + 1 - out[n] = '\0' - return p - # Ordinary word (i32 / i8* / void / double ...) - while p[0] != '\0' and _mlir_delim(p[0]) == 0: - if n + 1 < outsz: - out[n] = p[0] - n += 1 - p = p + 1 - # Function-type suffix " ( ... )" (not absorbed when nofunc=1, see the docstring) - q: str = _mlir_ws(p) - if nofunc == 0 and q[0] == '(': - if n + 1 < outsz: - out[n] = ' ' - n += 1 - p = q - d2: int = 0 - while p[0] != '\0': - ch2: int = p[0] - if ch2 == '(': - d2 += 1 - elif ch2 == ')': - if n + 1 < outsz: - out[n] = char(ch2) - n += 1 - p = p + 1 - d2 -= 1 - if d2 <= 0: - break - continue - if n + 1 < outsz: - out[n] = char(ch2) - n += 1 - p = p + 1 - while p[0] == '*': - if n + 1 < outsz: - out[n] = '*' - n += 1 - p = p + 1 - out[n] = '\0' - return p - - -def _mlir_in_set(ob: str, setstr: str) -> int: - """Whether opcode belongs to a "|a|b|c|"-form set""" - tb: str = _SC_Tb - tb[0] = '\0' - string.strcat(tb, "|") - string.strcat(tb, ob) - string.strcat(tb, "|") - if string.strstr(setstr, tb) is not None: - return 1 - return 0 - - -def _MlirBlkRef(t: str) -> str: - """Block reference text: strip the leading '%' (MLIR blocks use ^name)""" - if t is None: - return "" - if t[0] == '%': - return t + 1 - return t - - -def _MlirFindBlock(func: Function*, name: str) -> BasicBlock*: - """Find a basic block by name (without %) within the function""" - if func is None or func.Blocks is None or name is None: - return None - blocks: GSList[BasicBlock]* = func.Blocks - blk: BasicBlock* = blocks.Head - steps: int = 0 - while blk is not None: - steps += 1 - if steps > IR_DUMP_CHAIN_LIMIT: - MlirFail("MLIR render: block chain may be cyclic") - if blk.Name is not None and string.strcmp(blk.Name, name) == 0: - return blk - blk = blk.Next - return None - - -def _MlirIsPhiLine(text: str) -> int: - """Whether the line is a phi line""" - if text is None: - return 0 - if string.strstr(text, " = phi ") is not None: - return 1 - return 0 - - -# ============================================================ -# Operand rendering (with constant materialization) -# ============================================================ -def _MlirOperand(val: str, tyllvm: str, out: str, outsz: sizet) -> str: - """Render one operand and return its final text. - - SSA value -> %name after alias resolution; - non-SSA (literal/symbol ref) -> materialized %cN (registered during the scan pass). - """ - if val is None or val[0] == '\0': - MlirFail("MLIR render: empty operand") - if val[0] == '%': - r: str = _MlirResolve(val) - _append_cstr(out, outsz, r) - return r - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, tyllvm, _MLIR_Pool) - nm: str = _MlirConstGet(mt, val) - _append_cstr(out, outsz, nm) - return nm - - -# ============================================================ -# phi parsing -# ============================================================ -def _MlirPhiScan(text: str): - """Scan a phi line: register all incoming constants (scan pass only)""" - p: str = _mlir_ws(text) - tb: str = _SC_Buf - p = _mlir_read_value(p, tb, 512) - p = _mlir_ws(p) - if p[0] == '=': - p = _mlir_ws(p + 1) - ob: str = _SC_Op - p = _mlir_read_value(p, ob, 64) - if string.strcmp(ob, "phi") != 0: - return - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - g: int = 0 - while p[0] != '\0': - g += 1 - if g > 512: - MlirFail("MLIR render: too many phi incoming edges") - p = _mlir_ws(p) - if p[0] != '[': - break - p = _mlir_ws(p + 1) - vb: str = _SC_P1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - bb: str = _SC_P2 - p = _mlir_read_value(p, bb, 512) - p = _mlir_ws(p) - if p[0] == ']': - p = p + 1 - p = _mlir_ws(p) - if p[0] == ',': - p = p + 1 - # Register this incoming value (constants/symbol refs need materialization) - _MlirOperand(vb, tyb, None, 0) - - -def _MlirPhiParam(text: str) -> int: - """Take a phi's result name and type as block params; on success writes _SC_B1(name)/_SC_B2(type) and returns 1""" - p: str = _mlir_ws(text) - nm: str = _SC_B1 - p = _mlir_read_value(p, nm, 512) - p = _mlir_ws(p) - if p[0] != '=': - return 0 - p = _mlir_ws(p + 1) - ob: str = _SC_Op - p = _mlir_read_value(p, ob, 64) - if string.strcmp(ob, "phi") != 0: - return 0 - tyb: str = _SC_B2 - _mlir_read_type(p, tyb, 512) - return 1 - - -def _MlirPhiFindIncoming(text: str, src: BasicBlock*) -> int: - """Find the phi incoming edge matching the source block; on success writes _SC_B1(value)/_SC_B2(type) and returns 1""" - p: str = _mlir_ws(text) - tb: str = _SC_Buf - p = _mlir_read_value(p, tb, 512) - p = _mlir_ws(p) - if p[0] == '=': - p = _mlir_ws(p + 1) - ob: str = _SC_Op - p = _mlir_read_value(p, ob, 64) - if string.strcmp(ob, "phi") != 0: - return 0 - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - srcn: str = None - if src is not None: - srcn = src.Name - g: int = 0 - while p[0] != '\0': - g += 1 - if g > 512: - MlirFail("MLIR render: too many phi incoming edges") - p = _mlir_ws(p) - if p[0] != '[': - break - p = _mlir_ws(p + 1) - vb: str = _SC_P1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - bb: str = _SC_P2 - p = _mlir_read_value(p, bb, 512) - p = _mlir_ws(p) - if p[0] == ']': - p = p + 1 - p = _mlir_ws(p) - if p[0] == ',': - p = p + 1 - if srcn is not None: - bn: str = bb - if bn[0] == '%': - bn = bn + 1 - if string.strcmp(bn, srcn) == 0: - string.strcpy(_SC_B1, vb) - string.strcpy(_SC_B2, tyb) - return 1 - return 0 - - -# ============================================================ -# Block parameters / block arguments -# ============================================================ -def _MlirBlockHasPhi(blk: BasicBlock*) -> int: - """Whether the block head has a phi (i.e. the block carries block params)""" - if blk is None or blk.Lines is None: - return 0 - lines: GSList[Line]* = blk.Lines - ln: Line* = lines.Head - while ln is not None: - if ln.Buf is not None: - if _MlirIsPhiLine(ln.Buf) != 0: - return 1 - return 0 - ln = ln.Next - return 0 - - -def _MlirBlockOperands(out: str, outsz: sizet, - target: BasicBlock*, src: BasicBlock*): - """Emit target's block arguments when branching to it: (%v : ty, ...). - - Incoming edges are matched to phis by source block; no match -> loud error (never silently drop args). - """ - if target is None or src is None: - return - if _MlirBlockHasPhi(target) == 0: - return - tmp: str = _SC_Tmp - tmp[0] = '\0' - lines: GSList[Line]* = target.Lines - ln: Line* = lines.Head - cnt: int = 0 - while ln is not None: - if ln.Buf is None: - ln = ln.Next - continue - if _MlirIsPhiLine(ln.Buf) == 0: - break - if _MlirPhiFindIncoming(ln.Buf, src) == 0: - MlirFail("MLIR render: target block has phi but no incoming edge matches the source block") - if cnt > 0: - _append_cstr(tmp, 8192, ", ") - _MlirOperand(_SC_B1, _SC_B2, tmp, 4096) - _append_cstr(tmp, 8192, " : ") - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, _SC_B2, _MLIR_Pool) - _append_cstr(tmp, 8192, mt) - cnt += 1 - ln = ln.Next - if cnt == 0: - MlirFail("MLIR render: target block has phi but no block arguments were generated") - _append_cstr(out, outsz, "(") - _append_cstr(out, outsz, tmp) - _append_cstr(out, outsz, ")") - - -def _MlirEmitBlockHeader(out: str, outsz: sizet, blk: BasicBlock*): - """Emit a non-entry block header: ^name(%p: ty, ...):""" - _append_cstr(out, outsz, "^") - if blk.Name is not None: - _append_cstr(out, outsz, blk.Name) - if _MlirBlockHasPhi(blk) != 0: - tmp: str = _SC_Tmp - tmp[0] = '\0' - lines: GSList[Line]* = blk.Lines - ln: Line* = lines.Head - cnt: int = 0 - while ln is not None: - if ln.Buf is None: - ln = ln.Next - continue - if _MlirIsPhiLine(ln.Buf) == 0: - break - if _MlirPhiParam(ln.Buf) != 0: - if cnt > 0: - _append_cstr(tmp, 8192, ", ") - _append_cstr(tmp, 8192, _SC_B1) - _append_cstr(tmp, 8192, ": ") - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, _SC_B2, _MLIR_Pool) - _append_cstr(tmp, 8192, mt) - cnt += 1 - ln = ln.Next - if cnt == 0: - MlirFail("MLIR render: failed to parse leading phi") - _append_cstr(out, outsz, "(") - _append_cstr(out, outsz, tmp) - _append_cstr(out, outsz, ")") - _append_cstr(out, outsz, ":\n") - - -def _MlirEmitBlockLines(out: str, outsz: sizet, blk: BasicBlock*): - """Emit in-block instructions (skip leading phis, which became block params)""" - if blk is None or blk.Lines is None: - return - lines: GSList[Line]* = blk.Lines - ln: Line* = lines.Head - skipping: int = 1 - steps: int = 0 - while ln is not None: - steps += 1 - if steps > IR_DUMP_CHAIN_LIMIT: - MlirFail("MLIR render: block instruction chain may be cyclic") - if ln.Buf is not None: - if skipping != 0 and _MlirIsPhiLine(ln.Buf) != 0: - ln = ln.Next - continue - skipping = 0 - _MlirRenderLine(ln.Buf, out, outsz) - ln = ln.Next - - -def _MlirCheckEntryNoPhi(blk: BasicBlock*): - """entry block must not have phis (no predecessor, cannot become block params)""" - if _MlirBlockHasPhi(blk) != 0: - MlirFail("MLIR render: entry block must not have phi (MLIR entry block takes no block arguments)") - - -# ============================================================ -# Per-instruction MLIR rendering -# ============================================================ -def _MlirEmitRes(out: str, outsz: sizet, resname: str): - """Emit the " [%N = ]" prefix""" - _append_cstr(out, outsz, " ") - if resname is not None: - _append_cstr(out, outsz, resname) - _append_cstr(out, outsz, " = ") - - -def _MlirBinop(p: str, resname: str, ob: str, out: str, outsz: sizet): - """Integer/float binary op: llvm. %a, %b : ty""" - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - lb: str = _SC_V1 - p = _mlir_read_value(p, lb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - rb: str = _SC_V2 - _mlir_read_value(p, rb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.") - _append_cstr(out, outsz, ob) - _append_cstr(out, outsz, " ") - _MlirOperand(lb, tyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(rb, tyb, out, outsz) - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, tyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, mt) - _append_cstr(out, outsz, "\n") - - -def _MlirFneg(p: str, resname: str, out: str, outsz: sizet): - """fneg: llvm.fneg %a : ty""" - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - vb: str = _SC_V1 - _mlir_read_value(p, vb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.fneg ") - _MlirOperand(vb, tyb, out, outsz) - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, tyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, mt) - _append_cstr(out, outsz, "\n") - - -def _MlirCmp(p: str, resname: str, ob: str, out: str, outsz: sizet): - """icmp/fcmp: llvm.icmp "sgt" %a, %b : ty - - The intermediate text is LLVM syntax `icmp , ` -- predicate before type, - so the read order must be predicate -> type -> operands. - Do not read the type first: the predicate (e.g. `sgt`) would be passed as type text, triggering - "cannot convert to MLIR type text" and aborting. - """ - pb: str = _SC_P1 - p = _mlir_read_value(p, pb, 512) - p = _mlir_ws(p) - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - lb: str = _SC_V1 - p = _mlir_read_value(p, lb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - rb: str = _SC_V2 - _mlir_read_value(p, rb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.") - _append_cstr(out, outsz, ob) - _append_cstr(out, outsz, " \"") - _append_cstr(out, outsz, pb) - _append_cstr(out, outsz, "\" ") - _MlirOperand(lb, tyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(rb, tyb, out, outsz) - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, tyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, mt) - _append_cstr(out, outsz, "\n") - - -def _MlirAlloca(p: str, resname: str, out: str, outsz: sizet): - """alloca: llvm.alloca %cN x ty {alignment = N : i64} : (i32) -> !llvm.ptr - - The MLIR form of the alignment attribute was determined by measuring mlir-opt 22.1.8 - (do NOT rely on memory): `{alignment = 16 : i64}` -> exit 0 (adopted); - `align(16)` / `, align 16` (LLVM syntax) -> exit 1 (expected ':'); `{alignment = 16 : i32}` - -> exit 1 (requires i64). So the intermediate `, align N` must be rewritten to `{alignment = N : i64}`. - - Alignment accepts only a decimal constant that is a power of two: mlir-opt does not check - power-of-two (`{alignment = 3 : i64}` exits 0), but non-powers are illegal in LLVM semantics, so this - layer gates it: non-constant/non-power/out-of-range (> 2^30) always errors loudly. - """ - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - p = _mlir_ws(p) - aligntxt: str = _SC_P2 - aligntxt[0] = '\0' - if p[0] == ',': - # ", align N" (the intermediate-text form from build_alloca_aligned) - p = _mlir_ws(p + 1) - if string.strncmp(p, "align", 5) != 0: - MlirFail("MLIR render: alloca only supports the align attribute (unknown attribute)") - p = _mlir_ws(p + 5) - nd: int = 0 - while p[0] >= '0' and p[0] <= '9': - if nd + 1 >= 512: - MlirFail("MLIR render: alloca alignment value too long (out of range)") - aligntxt[nd] = p[0] - nd += 1 - p = p + 1 - aligntxt[nd] = '\0' - p = _mlir_ws(p) - if p[0] != '\0': - MlirFail("MLIR render: extra token after alloca align") - if nd == 0: - MlirFail("MLIR render: alloca alignment must be a constant (decimal constant, not SSA)") - if nd > 10: - MlirFail("MLIR render: alloca alignment out of range (must be a power of 2 and <= 2^30)") - av: int = string.atoi(aligntxt) - if av <= 0 or av > 1073741824: - MlirFail("MLIR render: alloca alignment out of range (must be a power of 2 and <= 2^30)") - if (av & (av - 1)) != 0: - MlirFail("MLIR render: alloca alignment must be a power of 2") - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, tyb, _MLIR_Pool) - cnt: str = _MlirConstGet("i32", "1") - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.alloca ") - _append_cstr(out, outsz, cnt) - _append_cstr(out, outsz, " x ") - _append_cstr(out, outsz, mt) - if aligntxt[0] != '\0': - _append_cstr(out, outsz, " {alignment = ") - _append_cstr(out, outsz, aligntxt) - _append_cstr(out, outsz, " : i64}") - _append_cstr(out, outsz, " : (i32) -> !llvm.ptr\n") - - -def _MlirLoad(p: str, resname: str, out: str, outsz: sizet): - """load: llvm.load %p : !llvm.ptr -> ty""" - p = _mlir_ws(p) - vol: int = 0 - if string.strncmp(p, "volatile ", 9) == 0: - vol = 1 - p = _mlir_ws(p + 9) - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ptyb: str = _SC_Ty2 - p = _mlir_read_type(p, ptyb, 512) - p = _mlir_ws(p) - pb: str = _SC_V1 - _mlir_read_value(p, pb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.load ") - if vol != 0: - _append_cstr(out, outsz, "volatile ") - _MlirOperand(pb, ptyb, out, outsz) - tmt: str = _SC_Mt - tmt[0] = '\0' - MlirTypeText(tmt, 4096, tyb, _MLIR_Pool) - pmt: str = _SC_Tmp2 - pmt[0] = '\0' - MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, pmt) - _append_cstr(out, outsz, " -> ") - _append_cstr(out, outsz, tmt) - _append_cstr(out, outsz, "\n") - - -def _MlirStore(p: str, out: str, outsz: sizet): - """store: llvm.store %v, %p : ty, !llvm.ptr""" - p = _mlir_ws(p) - vol: int = 0 - if string.strncmp(p, "volatile ", 9) == 0: - vol = 1 - p = _mlir_ws(p + 9) - vtyb: str = _SC_Ty - p = _mlir_read_type(p, vtyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ptyb: str = _SC_Ty2 - p = _mlir_read_type(p, ptyb, 512) - p = _mlir_ws(p) - pb: str = _SC_V2 - _mlir_read_value(p, pb, 512) - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.store ") - if vol != 0: - _append_cstr(out, outsz, "volatile ") - _MlirOperand(vb, vtyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(pb, ptyb, out, outsz) - vmt: str = _SC_Mt - vmt[0] = '\0' - MlirTypeText(vmt, 1024, vtyb, _MLIR_Pool) - pmt: str = _SC_Tmp2 - pmt[0] = '\0' - MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, vmt) - _append_cstr(out, outsz, ", ") - _append_cstr(out, outsz, pmt) - _append_cstr(out, outsz, "\n") - - -def _MlirBr(p: str, out: str, outsz: sizet): - """br: in LLVM syntax the conditional branch opcode is also `br`, dispatched by whether the first token is `label`. - - br label %x -> llvm.br ^x - br i1 %c, label %a, label %b -> llvm.cond_br %c, ^a, ^b - - Note: build_cond_br emits intermediate text `br i1 ...` (NOT `cond_br`), so the conditional - form must be forwarded here to _MlirCondBr; otherwise it is taken as unconditional and the - type `i1` is mistaken for a block name (producing `llvm.br ^i1` and dropping the condition). - """ - p = _mlir_ws(p) - if string.strncmp(p, "label ", 6) != 0: - # Conditional branch: ` , label %a, label %b` - _MlirCondBr(p, out, outsz) - return - p = _mlir_ws(p + 6) - tb: str = _SC_V1 - _mlir_read_value(p, tb, 512) - br: str = _MlirBlkRef(tb) - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.br ^") - _append_cstr(out, outsz, br) - _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, br), _MLIR_SrcBlk) - _append_cstr(out, outsz, "\n") - - -def _MlirCondBr(p: str, out: str, outsz: sizet): - """cond_br: llvm.cond_br %c, ^then(%v : ty), ^else(%v : ty)""" - ctyb: str = _SC_Ty - p = _mlir_read_type(p, ctyb, 512) - p = _mlir_ws(p) - cb: str = _SC_V1 - p = _mlir_read_value(p, cb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - if string.strncmp(p, "label ", 6) == 0: - p = _mlir_ws(p + 6) - tb: str = _SC_V2 - p = _mlir_read_value(p, tb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - if string.strncmp(p, "label ", 6) == 0: - p = _mlir_ws(p + 6) - eb: str = _SC_V3 - _mlir_read_value(p, eb, 512) - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.cond_br ") - _MlirOperand(cb, ctyb, out, outsz) - tr: str = _MlirBlkRef(tb) - _append_cstr(out, outsz, ", ^") - _append_cstr(out, outsz, tr) - _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, tr), _MLIR_SrcBlk) - er: str = _MlirBlkRef(eb) - _append_cstr(out, outsz, ", ^") - _append_cstr(out, outsz, er) - _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, er), _MLIR_SrcBlk) - _append_cstr(out, outsz, "\n") - - -def _MlirRet(p: str, out: str, outsz: sizet): - """ret: llvm.return %v : ty (void omits operand and type)""" - p = _mlir_ws(p) - if string.strncmp(p, "void", 4) == 0 and (p[4] == '\0' or p[4] == ' '): - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.return\n") - return - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - vb: str = _SC_V1 - _mlir_read_value(p, vb, 512) - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.return ") - _MlirOperand(vb, tyb, out, outsz) - mt: str = _SC_Mt - mt[0] = '\0' - MlirTypeText(mt, 4096, tyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, mt) - _append_cstr(out, outsz, "\n") - - -def _MlirUnreachable(out: str, outsz: sizet): - """unreachable: llvm.unreachable""" - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.unreachable\n") - - -def _MlirCall(p: str, resname: str, out: str, outsz: sizet): - """call: llvm.call @f(%a) : (i32) -> i32 / indirect llvm.call %fp(%a) : !llvm.ptr, (i32) -> i32 - - Variadic calls (intermediate text `call (...) @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, 512) - fmt: str = _SC_IdxT - fmt[0] = '\0' - MlirTypeText(fmt, 4096, ftb, _MLIR_Pool) - _append_cstr(vftys, 8192, fmt) - p = _mlir_ws(p) - if p[0] == ',': - p = p + 1 - calb: str = _SC_Callee - p = _mlir_read_value(p, calb, 512) - p = _mlir_ws(p) - if p[0] != '(': - MlirFail("MLIR render: call missing argument list") - p = p + 1 - args: str = _SC_Args - argts: str = _SC_ArgTs - args[0] = '\0' - argts[0] = '\0' - nargs: int = 0 - g: int = 0 - while p[0] != '\0': - g += 1 - if g > 256: - MlirFail("MLIR render: too many call arguments") - p = _mlir_ws(p) - if p[0] == ')': - break - atyb: str = _SC_Ty2 - p = _mlir_read_type(p, atyb, 512) - p = _mlir_ws(p) - avb: str = _SC_V1 - p = _mlir_read_value(p, avb, 512) - if nargs > 0: - _append_cstr(args, 2048, ", ") - _append_cstr(argts, 8192, ", ") - _MlirOperand(avb, atyb, args, 2048) - amt: str = _SC_Tmp2 - amt[0] = '\0' - MlirTypeText(amt, 4096, atyb, _MLIR_Pool) - _append_cstr(argts, 8192, amt) - nargs += 1 - p = _mlir_ws(p) - if p[0] == ',': - p = p + 1 - rmt: str = _SC_Mt - rmt[0] = '\0' - MlirTypeText(rmt, 1024, rtyb, _MLIR_Pool) - # LLVM intrinsics (bare symbol @llvm.xxx, not llvm.mlir.*) must use the generic op - # `llvm.call_intrinsic "llvm.xxx"(...) : (...) -> ...`: mlir-translate mangles the intrinsic - # name by operand types (e.g. llvm.va_start -> llvm.va_start.p0). A plain llvm.call would emit - # an unmangled @llvm.va_start and LLVM verification errors - # "Intrinsic name not mangled correctly for type arguments!". - if calb[0] == '@' and string.strncmp(calb + 1, "llvm.", 5) == 0 \ - and string.strncmp(calb + 1, "llvm.mlir.", 10) != 0: - if string.strcmp(rmt, "none") == 0: - _MlirEmitRes(out, outsz, None) - else: - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.call_intrinsic \"") - _append_cstr(out, outsz, calb + 1) - _append_cstr(out, outsz, "\"(") - _append_cstr(out, outsz, args) - _append_cstr(out, outsz, ") : (") - _append_cstr(out, outsz, argts) - _append_cstr(out, outsz, ") -> ") - if string.strcmp(rmt, "none") == 0: - _append_cstr(out, outsz, "()") - else: - _append_cstr(out, outsz, rmt) - _append_cstr(out, outsz, "\n") - return - if string.strcmp(rmt, "none") == 0: - _MlirEmitRes(out, outsz, None) - else: - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.call ") - # callee 也必须过 alias 解析:no-op bitcast(含函数指针 bitcast)被省略后,间接调用点 - # 必须改写成别名目标,否则 %N 悬空 → mlir-opt 报 use of undeclared SSA value - # / definition has type 'i8' but previously used here with type '!llvm.ptr'。 - calb_r: str = _MlirResolve(calb) - if calb_r is None: calb_r = calb - _append_cstr(out, outsz, calb_r) - _append_cstr(out, outsz, "(") - _append_cstr(out, outsz, args) - _append_cstr(out, outsz, ")") - if variadic != 0: - # Variadic call: vararg(the callee's full MLIR function type) (includes `...`) - _append_cstr(out, outsz, " vararg(!llvm.func<") - if string.strcmp(rmt, "none") == 0: - # Void-returning variadic function type: write `void`, not `none` - _append_cstr(out, outsz, "void") - else: - _append_cstr(out, outsz, rmt) - _append_cstr(out, outsz, " (") - _append_cstr(out, outsz, vftys) - # Close three levels in order: the param list `)`, `!llvm.func<`'s `>`, `vararg(`'s `)` - # (omitting vararg's `)` made mlir-opt 22.1.8 report "expected ')'" verbatim) - _append_cstr(out, outsz, ")>)") - _append_cstr(out, outsz, " : ") - if calb_r[0] != '@': - # Indirect call: the callee is a !llvm.ptr operand - # (用解析后的名字判断:别名解析可能把 callee 指到一个 @符号,那就成了直接调用) - _append_cstr(out, outsz, "!llvm.ptr, ") - _append_cstr(out, outsz, "(") - _append_cstr(out, outsz, argts) - _append_cstr(out, outsz, ") -> ") - if string.strcmp(rmt, "none") == 0: - _append_cstr(out, outsz, "()") - else: - _append_cstr(out, outsz, rmt) - _append_cstr(out, outsz, "\n") - - -def _MlirAsm(p: str, resname: str, rtyb: str, out: str, outsz: sizet): - """Inline asm: llvm.inline_asm [has_side_effects] [asm_dialect = intel] "...", "..." %a, %b : (i32, i32) -> i32 - - Intermediate text (LLVM syntax) has two sources: - builders' build_inline_asm -> `call asm sideeffect "...", "..."()` - TPV 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: TPV's - `%N = call {i32, i32} asm sideeffect "...", "=r,r"()` renders as - `%N = llvm.inline_asm has_side_effects "...", "=r,r" : () -> !llvm.struct<(i32, i32)>` - (mlir-opt / mlir-translate 22.1.8 measured exit 0), then fields are extracted with - `llvm.extractvalue` (see _MlirExtractValue). No special branch is needed here: the return type - text goes through MlirTypeText (`{...}` -> `!llvm.struct<(...)>`), nested structs included. - * Measured passing combinations: has_side_effects x asm_dialect = intel x 0/1/2 operands x return/no-return. - Illegal forms always error loudly: unknown tokens besides `sideeffect`/`inteldialect`, missing - constraint string, non-string literal, unpaired args, or a return value without a result name. - """ - p = _mlir_ws(p + 3) - side: int = 0 - intel: int = 0 - g: int = 0 - while g < 8: - g += 1 - if string.strncmp(p, "sideeffect", 10) == 0 and (p[10] == ' ' or p[10] == '\t'): - side = 1 - p = _mlir_ws(p + 10) - continue - if string.strncmp(p, "inteldialect", 12) == 0 and (p[12] == ' ' or p[12] == '\t'): - intel = 1 - p = _mlir_ws(p + 12) - continue - break - # Asm text can reach several KB (TPV's ASM_LINE_BUF_SIZE=4096); use a 4096 buffer, not a - # small one, to avoid truncation; escape pairs (\22/\5C/\0A) pass through as MLIR accepts them. - asmb: str = _SC_Tmp - p = _mlir_read_value(p, asmb, 4096) - if asmb[0] != '"': - MlirFail("MLIR render: inline_asm missing asm text (or contains an unknown keyword)") - p = _mlir_ws(p) - if p[0] != ',': - MlirFail("MLIR render: inline_asm missing constraint string (must be \"asm\", \"constraints\")") - p = _mlir_ws(p + 1) - conb: str = _SC_Args - p = _mlir_read_value(p, conb, 2048) - if conb[0] != '"': - MlirFail("MLIR render: inline_asm constraint string must be a string literal") - p = _mlir_ws(p) - if p[0] != '(': - MlirFail("MLIR render: inline_asm missing argument list '(...)'") - p = p + 1 - ops: str = _SC_ArgTs - ops[0] = '\0' - tys: str = _SC_IdxV - tys[0] = '\0' - nops: int = 0 - g = 0 - while p[0] != '\0': - g += 1 - if g > 256: - MlirFail("MLIR render: too many inline_asm arguments") - p = _mlir_ws(p) - if p[0] == ')': - break - atyb: str = _SC_Ty2 - p = _mlir_read_type(p, atyb, 512) - p = _mlir_ws(p) - avb: str = _SC_V1 - p = _mlir_read_value(p, avb, 512) - if nops > 0: - _append_cstr(ops, 8192, ", ") - _append_cstr(tys, 1024, ", ") - _MlirOperand(avb, atyb, ops, 2048) - amt: str = _SC_Mt - amt[0] = '\0' - MlirTypeText(amt, 4096, atyb, _MLIR_Pool) - _append_cstr(tys, 1024, amt) - nops += 1 - p = _mlir_ws(p) - if p[0] == ',': - p = p + 1 - if p[0] == ')': - p = p + 1 - p = _mlir_ws(p) - if p[0] != '\0': - MlirFail("MLIR render: extra token after inline_asm argument list") - rmt: str = _SC_Buf - rmt[0] = '\0' - MlirTypeText(rmt, 1024, rtyb, _MLIR_Pool) - if string.strcmp(rmt, "none") == 0: - _MlirEmitRes(out, outsz, None) - else: - if resname is None: - MlirFail("MLIR render: inline_asm has a return value but no result name (%N = call ...)") - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.inline_asm") - if side != 0: - _append_cstr(out, outsz, " has_side_effects") - if intel != 0: - _append_cstr(out, outsz, " asm_dialect = intel") - _append_cstr(out, outsz, " ") - _append_cstr(out, outsz, asmb) - _append_cstr(out, outsz, ", ") - _append_cstr(out, outsz, conb) - if nops > 0: - _append_cstr(out, outsz, " ") - _append_cstr(out, outsz, ops) - _append_cstr(out, outsz, " : (") - _append_cstr(out, outsz, tys) - _append_cstr(out, outsz, ") -> ") - if string.strcmp(rmt, "none") == 0: - _append_cstr(out, outsz, "()") - else: - _append_cstr(out, outsz, rmt) - _append_cstr(out, outsz, "\n") - - -def _MlirCast(p: str, resname: str, ob: str, out: str, outsz: sizet): - """Cast instruction: llvm.zext %v : i32 to i64""" - ftyb: str = _SC_Ty - p = _mlir_read_type(p, ftyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if string.strncmp(p, "to ", 3) == 0: - p = _mlir_ws(p + 3) - ttyb: str = _SC_Ty2 - _mlir_read_type(p, ttyb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.") - _append_cstr(out, outsz, ob) - _append_cstr(out, outsz, " ") - _MlirOperand(vb, ftyb, out, outsz) - fmt: str = _SC_Mt - fmt[0] = '\0' - MlirTypeText(fmt, 4096, ftyb, _MLIR_Pool) - tmt: str = _SC_Tmp2 - tmt[0] = '\0' - MlirTypeText(tmt, 4096, ttyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, fmt) - _append_cstr(out, outsz, " to ") - _append_cstr(out, outsz, tmt) - _append_cstr(out, outsz, "\n") - - -def _MlirBitcast(p: str, resname: str, out: str, outsz: sizet): - """bitcast: ptr->ptr is treated as a no-op (records an alias); otherwise llvm.bitcast %v : T to U""" - ftyb: str = _SC_Ty - p = _mlir_read_type(p, ftyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if string.strncmp(p, "to ", 3) == 0: - p = _mlir_ws(p + 3) - ttyb: str = _SC_Ty2 - _mlir_read_type(p, ttyb, 512) - if MlirTypeTextIsPointer(ftyb) != 0 and MlirTypeTextIsPointer(ttyb) != 0: - # With opaque pointers, a ptr->ptr bitcast is meaningless -> no-op: - # register the result name as an alias of the source operand (later refs auto-rewritten), emit nothing - if resname is None: - MlirFail("MLIR render: no-op bitcast missing result name") - opnd: str = _MlirOperand(vb, ftyb, None, 0) - _MlirAliasAdd(resname, opnd) - return - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.bitcast ") - _MlirOperand(vb, ftyb, out, outsz) - fmt: str = _SC_Mt - fmt[0] = '\0' - MlirTypeText(fmt, 4096, ftyb, _MLIR_Pool) - tmt: str = _SC_Tmp2 - tmt[0] = '\0' - MlirTypeText(tmt, 4096, ttyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, fmt) - _append_cstr(out, outsz, " to ") - _append_cstr(out, outsz, tmt) - _append_cstr(out, outsz, "\n") - - -def _MlirSelect(p: str, resname: str, out: str, outsz: sizet): - """select: llvm.select %c, %a, %b : i1, i32""" - ctyb: str = _SC_Ty - p = _mlir_read_type(p, ctyb, 512) - p = _mlir_ws(p) - cb: str = _SC_V1 - p = _mlir_read_value(p, cb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ttyb: str = _SC_Ty2 - p = _mlir_read_type(p, ttyb, 512) - p = _mlir_ws(p) - tb: str = _SC_V2 - p = _mlir_read_value(p, tb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ftyb: str = _SC_Buf - p = _mlir_read_type(p, ftyb, 512) - p = _mlir_ws(p) - fb: str = _SC_V3 - _mlir_read_value(p, fb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.select ") - _MlirOperand(cb, ctyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(tb, ttyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(fb, ttyb, out, outsz) - cmt: str = _SC_Mt - cmt[0] = '\0' - MlirTypeText(cmt, 4096, ctyb, _MLIR_Pool) - vmt: str = _SC_Tmp2 - vmt[0] = '\0' - MlirTypeText(vmt, 1024, ttyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, cmt) - _append_cstr(out, outsz, ", ") - _append_cstr(out, outsz, vmt) - _append_cstr(out, outsz, "\n") - - -def _MlirGep(p: str, resname: str, out: str, outsz: sizet): - """getelementptr: llvm.getelementptr %p[i0, i1] : (!llvm.ptr, t0, t1) -> !llvm.ptr, base""" - baseb: str = _SC_Ty2 - p = _mlir_read_type(p, baseb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ptyb: str = _SC_Buf - p = _mlir_read_type(p, ptyb, 512) - p = _mlir_ws(p) - pb: str = _SC_V3 - p = _mlir_read_value(p, pb, 512) - idxv: str = _SC_IdxV - idxt: str = _SC_IdxT - idxv[0] = '\0' - idxt[0] = '\0' - p = _mlir_ws(p) - nidx: int = 0 - tmpv: str = _SC_Tmp - tmpv[0] = '\0' - g: int = 0 - while p[0] == ',': - g += 1 - if g > 32: - MlirFail("MLIR render: too many GEP indices") - p = _mlir_ws(p + 1) - ityb: str = _SC_Ty - p = _mlir_read_type(p, ityb, 512) - p = _mlir_ws(p) - ivb: str = _SC_V1 - p = _mlir_read_value(p, ivb, 512) - # Constant indices must be written as integer literals (e.g. `[0, 1]`), not materialized - # as SSA operands: MLIR's llvm.getelementptr requires struct-field indices to be constant, - # else the verifier errors 'expected index N indexing a struct to be constant' - # (operand form only populates rawConstantIndices' dynamic slot). Only non-constant (SSA, - # starting with '%') indices are materialized and take a type slot; constants do not. - if nidx > 0: - _append_cstr(idxv, 1024, ", ") - if ivb[0] == '%': - _MlirOperand(ivb, ityb, tmpv, 4096) - _append_cstr(idxv, 1024, tmpv) - tmpv[0] = '\0' - imt: str = _SC_Mt - imt[0] = '\0' - MlirTypeText(imt, 4096, ityb, _MLIR_Pool) - if idxt[0] != '\0': - _append_cstr(idxt, 4096, ", ") - _append_cstr(idxt, 4096, imt) - else: - _append_cstr(idxv, 1024, ivb) - nidx += 1 - p = _mlir_ws(p) - if nidx == 0: - MlirFail("MLIR render: GEP missing index") - bmt: str = _SC_Tmp2 - bmt[0] = '\0' - MlirTypeText(bmt, 4096, baseb, _MLIR_Pool) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.getelementptr ") - _MlirOperand(pb, ptyb, out, outsz) - _append_cstr(out, outsz, "[") - _append_cstr(out, outsz, idxv) - _append_cstr(out, outsz, "] : (!llvm.ptr") - # One type per index - _MlirAppendCommaTypes(out, outsz, idxt) - _append_cstr(out, outsz, ") -> !llvm.ptr, ") - _append_cstr(out, outsz, bmt) - _append_cstr(out, outsz, "\n") - - -def _MlirAppendCommaTypes(out: str, outsz: sizet, tylist: str): - """Append ", t0, t1" to out (tylist is a comma-separated type string)""" - if tylist is None or tylist[0] == '\0': - return - _append_cstr(out, outsz, ", ") - _append_cstr(out, outsz, tylist) - - -def _MlirSwitch(p: str, out: str, outsz: sizet): - """switch: llvm.switch %v : i32, ^default [0: ^case0, 1: ^case1] - - The form was determined by measuring mlir-opt 22.1.8 (the design-doc form - `llvm.switch %v, ^default [0: ^case0] : i32` is REJECTED: "expected ':'"): - `llvm.switch %v : i32, ^default [0: ^c0, 1: ^c1]` -> exit 0 (adopted) - `llvm.switch %v : i32, ^default(%r : i32) [ ... ]` -> exit 0 (default block args) - `llvm.switch %v : i32, ^default [0: ^c0(%r : i32)]` -> exit 0 (case block args) - `llvm.switch %v : i64, ^default [4294967296: ^c0]` -> exit 0 (case value follows value type) - `llvm.switch %v : i32, ^default [-1: ^c0]` -> exit 0 - `llvm.switch %v : i32, ^default []` -> exit 0 (empty case table) - i.e. the value type follows the value (`%v : i32,`); block args are written exactly like - br/cond_br, so _MlirBlockOperands' phi->block-arg facility is reused as-is => target blocks may - carry phis (a missing incoming edge errors loudly, never silently dropping args). - Case values must still be constants: in MLIR they are attributes (case_values), not SSA values. - """ - tyb: str = _SC_Ty - p = _mlir_read_type(p, tyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - if string.strncmp(p, "label ", 6) == 0: - p = _mlir_ws(p + 6) - db: str = _SC_V2 - p = _mlir_read_value(p, db, 512) - p = _mlir_ws(p) - if p[0] == '[': - p = p + 1 - # Materialize the value operand and its MLIR type text first: _MlirBlockOperands inside the - # case loop reuses _SC_Ty/_SC_Buf/_SC_P1/_SC_P2 scratch, so capture into a separate buffer first. - vop: str = _SC_IdxT - vop[0] = '\0' - _MlirOperand(vb, tyb, vop, 1024) - vmt: str = _SC_IdxV - vmt[0] = '\0' - MlirTypeText(vmt, 1024, tyb, _MLIR_Pool) - cases: str = _SC_Tmp2 - cases[0] = '\0' - ncase: int = 0 - g: int = 0 - while p[0] != '\0': - g += 1 - if g > 1024: - MlirFail("MLIR render: too many switch branches") - p = _mlir_ws(p) - if p[0] == ']' or p[0] == '\0': - break - ctb: str = _SC_P1 - p = _mlir_read_type(p, ctb, 512) - p = _mlir_ws(p) - cvb: str = _SC_P2 - p = _mlir_read_value(p, cvb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - if string.strncmp(p, "label ", 6) == 0: - p = _mlir_ws(p + 6) - cb: str = _SC_Ty2 - p = _mlir_read_value(p, cb, 512) - if cvb[0] == '%': - MlirFail("MLIR render: switch case value must be a constant (SSA value not supported)") - cbr: str = _MlirBlkRef(cb) - if ncase > 0: - _append_cstr(cases, 4096, ", ") - _append_cstr(cases, 4096, cvb) - _append_cstr(cases, 4096, ": ^") - _append_cstr(cases, 4096, cbr) - # Target block has phis => append block arguments (same facility as br/cond_br) - _MlirBlockOperands(cases, 1024, _MlirFindBlock(_MLIR_Func, cbr), _MLIR_SrcBlk) - ncase += 1 - p = _mlir_ws(p) - if p[0] == ',': - p = p + 1 - dr: str = _MlirBlkRef(db) - _MlirEmitRes(out, outsz, None) - _append_cstr(out, outsz, "llvm.switch ") - _append_cstr(out, outsz, vop) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, vmt) - _append_cstr(out, outsz, ", ^") - _append_cstr(out, outsz, dr) - _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, dr), _MLIR_SrcBlk) - _append_cstr(out, outsz, " [") - _append_cstr(out, outsz, cases) - _append_cstr(out, outsz, "]\n") - - -def _MlirAtomicRmw(p: str, resname: str, out: str, outsz: sizet): - """atomicrmw: llvm.atomicrmw add %p, %v monotonic : !llvm.ptr, i32""" - aop: str = _SC_P1 - p = _mlir_read_value(p, aop, 512) - p = _mlir_ws(p) - ptyb: str = _SC_Ty - p = _mlir_read_type(p, ptyb, 512) - p = _mlir_ws(p) - pb: str = _SC_V1 - p = _mlir_read_value(p, pb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - vtyb: str = _SC_Ty2 - p = _mlir_read_type(p, vtyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V2 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - ordb: str = _SC_P2 - _mlir_read_value(p, ordb, 512) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.atomicrmw ") - _append_cstr(out, outsz, aop) - _append_cstr(out, outsz, " ") - _MlirOperand(pb, ptyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(vb, vtyb, out, outsz) - _append_cstr(out, outsz, " ") - _append_cstr(out, outsz, ordb) - pmt: str = _SC_Mt - pmt[0] = '\0' - MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) - vmt: str = _SC_Tmp2 - vmt[0] = '\0' - MlirTypeText(vmt, 1024, vtyb, _MLIR_Pool) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, pmt) - _append_cstr(out, outsz, ", ") - _append_cstr(out, outsz, vmt) - _append_cstr(out, outsz, "\n") - - -def _MlirCmpXchg(p: str, resname: str, resname2: str, - out: str, outsz: sizet): - """cmpxchg: MLIR single result struct{T, i1} + two extractvalues for old value / success bit. - - Intermediate text (LLVM syntax, two forms: builder single result / two-result prefix) - %N = cmpxchg , , - %a, %b = cmpxchg ... - MLIR form (mlir-opt 22.1.8 measured exit 0; cmpxchg is a single result in MLIR, yielding - struct{T, i1}, not two SSA values): - %N = llvm.cmpxchg %p, %c, %n acq_rel monotonic : !llvm.ptr, i32 - %_N_v = llvm.extractvalue %N[0] : !llvm.struct<(i32, i1)> ; old value (compared value) - %_N_ok = llvm.extractvalue %N[1] : !llvm.struct<(i32, i1)> ; i1 success bit - The two extractvalue result names are deterministically derived from the struct result name - (_MlirCxResName, e.g. `%0` -> `%_0_v`/`%_0_ok`). - Bindings: `%N` (single result) / `%a` (two-result first name) -> old value `[0]`; - two-result second name `%b` -> success bit `[1]`. Bindings use the same alias facility as - bitcast no-op (_MlirAliasAdd); later references are auto-rewritten by _MlirResolve to the - extractvalue result names. This preserves builder-side build_cmpxchg's "T-typed Value" (old value) semantics. - """ - ptyb: str = _SC_Ty - p = _mlir_read_type(p, ptyb, 512) - p = _mlir_ws(p) - pb: str = _SC_V1 - p = _mlir_read_value(p, pb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ctyb: str = _SC_Ty2 - p = _mlir_read_type(p, ctyb, 512) - p = _mlir_ws(p) - cb: str = _SC_V2 - p = _mlir_read_value(p, cb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - ntyb: str = _SC_B1 - p = _mlir_read_type(p, ntyb, 512) - p = _mlir_ws(p) - nb: str = _SC_B2 - p = _mlir_read_value(p, nb, 512) - p = _mlir_ws(p) - succ: str = _SC_P1 - p = _mlir_read_value(p, succ, 512) - p = _mlir_ws(p) - fail: str = _SC_P2 - _mlir_read_value(p, fail, 512) - if resname is None: - MlirFail("MLIR render: cmpxchg missing result name") - # The two extractvalue result names derive deterministically from the struct name (identical across both passes) - onm: str = _MlirCxResName(resname, "_v") - snm: str = _MlirCxResName(resname, "_ok") - _MlirAliasAdd(resname, onm) - if resname2 is not None: - _MlirAliasAdd(resname2, snm) - # MLIR type text (use a non-_SC_Mt buffer: _MlirOperand occupies _SC_Mt for literals) - pmt: str = _SC_Tmp2 - pmt[0] = '\0' - MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) - vmt: str = _SC_IdxV - vmt[0] = '\0' - MlirTypeText(vmt, 1024, ctyb, _MLIR_Pool) - stmt: str = _SC_IdxT - stmt[0] = '\0' - _append_cstr(stmt, 4096, "!llvm.struct<(") - _append_cstr(stmt, 4096, vmt) - _append_cstr(stmt, 4096, ", i1)>") - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.cmpxchg ") - _MlirOperand(pb, ptyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(cb, ctyb, out, outsz) - _append_cstr(out, outsz, ", ") - _MlirOperand(nb, ntyb, out, outsz) - _append_cstr(out, outsz, " ") - _append_cstr(out, outsz, succ) - _append_cstr(out, outsz, " ") - _append_cstr(out, outsz, fail) - _append_cstr(out, outsz, " : ") - _append_cstr(out, outsz, pmt) - _append_cstr(out, outsz, ", ") - _append_cstr(out, outsz, vmt) - _append_cstr(out, outsz, "\n") - _MlirEmitRes(out, outsz, onm) - _append_cstr(out, outsz, "llvm.extractvalue ") - _append_cstr(out, outsz, resname) - _append_cstr(out, outsz, "[0] : ") - _append_cstr(out, outsz, stmt) - _append_cstr(out, outsz, "\n") - _MlirEmitRes(out, outsz, snm) - _append_cstr(out, outsz, "llvm.extractvalue ") - _append_cstr(out, outsz, resname) - _append_cstr(out, outsz, "[1] : ") - _append_cstr(out, outsz, stmt) - _append_cstr(out, outsz, "\n") - - -def _MlirVaArg(p: str, resname: str, out: str, outsz: sizet): - """va_arg: use the generic op syntax (deliberate choice, see below). - - Intermediate text (LLVM syntax): %N = va_arg , - MLIR text: %N = "llvm.va_arg"(%ap) : (!llvm.ptr) -> i32 - - Why generic form is required (mlir-opt 22.1.8 measured): the custom assembly syntax - `llvm.va_arg %ap, i32` requires an explicit va_list type (in LLVM it is `ptr`), which cannot - be expressed with opaque pointers -- mlir-opt errors "expected builtin.function". So the - generic op form is used, described only by operand/result types. Down-level verification - (mlir-translate --mlir-to-llvmir) yields `%2 = va_arg ptr %0, i32`, isomorphic to LLVM's va_arg. - """ - vtyb: str = _SC_Ty - p = _mlir_read_type(p, vtyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V1 - p = _mlir_read_value(p, vb, 512) - p = _mlir_ws(p) - if p[0] == ',': - p = _mlir_ws(p + 1) - atyb: str = _SC_Ty2 - _mlir_read_type(p, atyb, 512) - if resname is None: - MlirFail("MLIR render: va_arg missing result name") - # Operand/result types (_MlirOperand only uses _SC_Mt, so take separate buffers here) - omt: str = _SC_Tmp2 - omt[0] = '\0' - MlirTypeText(omt, 4096, vtyb, _MLIR_Pool) - rmt: str = _SC_IdxV - rmt[0] = '\0' - MlirTypeText(rmt, 1024, atyb, _MLIR_Pool) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "\"llvm.va_arg\"(") - _MlirOperand(vb, vtyb, out, outsz) - _append_cstr(out, outsz, ") : (") - _append_cstr(out, outsz, omt) - _append_cstr(out, outsz, ") -> ") - _append_cstr(out, outsz, rmt) - _append_cstr(out, outsz, "\n") - - -def _MlirExtractValue(p: str, resname: str, out: str, outsz: sizet): - """extractvalue: llvm.extractvalue %v[i0, i1] : - - 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: TPV's multi-result inline asm (`%asm_result = call {i32, i32} asm ...`) followed by - per-field extraction (`%asm_out_0 = extractvalue {i32, i32} %asm_result, 0`); multi-level indices - are also supported (measured legal). Indices must be constant literals (LLVM syntax) -- SSA index => loud error. - """ - atyb: str = _SC_Ty - p = _mlir_read_type(p, atyb, 512) - p = _mlir_ws(p) - vb: str = _SC_V1 - p = _mlir_read_value(p, vb, 512) - idxs: str = _SC_IdxV - idxs[0] = '\0' - nidx: int = 0 - g: int = 0 - # Index list: comma-separated in LLVM syntax (`, i0, i1`); read each and rewrite. - # Loop bound matches other parse loops in this file (bounded; exceeding errors loudly). - while g < 64: - p = _mlir_ws(p) - if p[0] != ',': - break - g += 1 - if g > 32: - MlirFail("MLIR render: too many extractvalue indices") - p = _mlir_ws(p + 1) - ib: str = _SC_IdxT - p = _mlir_read_value(p, ib, 512) - if ib[0] == '\0': - MlirFail("MLIR render: extractvalue missing index") - if ib[0] == '%': - MlirFail("MLIR render: extractvalue index must be a constant (SSA index not supported)") - if nidx > 0: - _append_cstr(idxs, 1024, ", ") - _append_cstr(idxs, 1024, ib) - nidx += 1 - if nidx == 0: - MlirFail("MLIR render: extractvalue missing index") - if p[0] != '\0': - MlirFail("MLIR render: extra token after extractvalue index") - # Aggregate type text goes into a separate buffer first (_MlirOperand only uses _SC_Mt) - amt: str = _SC_Tmp2 - amt[0] = '\0' - MlirTypeText(amt, 4096, atyb, _MLIR_Pool) - _MlirEmitRes(out, outsz, resname) - _append_cstr(out, outsz, "llvm.extractvalue ") - _MlirOperand(vb, atyb, out, outsz) - _append_cstr(out, outsz, "[") - _append_cstr(out, outsz, idxs) - _append_cstr(out, outsz, "] : ") - _append_cstr(out, outsz, amt) - _append_cstr(out, outsz, "\n") - - -# ============================================================ -# Instruction line -> MLIR dispatch -# ============================================================ -def _MlirRenderLine(text: str, out: str, outsz: sizet): - """Parse one intermediate instruction text and render it to MLIR (out=None: register/validate only)""" - global _MLIR_Pass, _MLIR_CurLine - if text is None: - return - # Stash the "current line" at entry: printed on type-text conversion failure (both passes - # use the same line, so pass1 failures are also locatable). text points to Line.Buf, live for the whole render. - _MLIR_CurLine = text - MlirSetContextLine(_MLIR_CurLine) - p: str = _mlir_ws(text) - if p[0] == '\0': - return - resname: str = None - resname2: str = None - if p[0] == '%': - tb: str = _SC_Tb - q: str = _mlir_read_value(p, tb, 512) - q2: str = _mlir_ws(q) - if q2[0] == '=': - resname = _mlir_dup(tb) - p = _mlir_ws(q2 + 1) - elif q2[0] == ',': - # Two-result prefix `%a, %b = ` (cmpxchg only): MLIR's cmpxchg is a single - # struct{T, i1} result; the two names each bind to one extractvalue (see _MlirCmpXchg). - resname = _mlir_dup(tb) - q3: str = _mlir_ws(q2 + 1) - tb2: str = _SC_V3 - q3 = _mlir_read_value(q3, tb2, 512) - q4: str = _mlir_ws(q3) - if q4[0] != '=': - MlirFail("MLIR render: double-result prefix (%a, %b =) missing '='") - resname2 = _mlir_dup(tb2) - p = _mlir_ws(q4 + 1) - ob: str = _SC_Op - p = _mlir_read_value(p, ob, 64) - if ob[0] == '\0': - MlirFail("MLIR render: empty instruction") - if resname2 is not None and string.strcmp(ob, "cmpxchg") != 0: - MlirFail("MLIR render: double-result prefix (%a, %b =) only supports cmpxchg") - if _mlir_in_set(ob, "|add|sub|mul|sdiv|udiv|srem|urem|and|or|xor|shl|lshr|ashr|") != 0: - _MlirBinop(p, resname, ob, out, outsz) - return - if _mlir_in_set(ob, "|fadd|fsub|fmul|fdiv|frem|") != 0: - _MlirBinop(p, resname, ob, out, outsz) - return - if string.strcmp(ob, "fneg") == 0: - _MlirFneg(p, resname, out, outsz) - return - if string.strcmp(ob, "icmp") == 0: - _MlirCmp(p, resname, ob, out, outsz) - return - if string.strcmp(ob, "fcmp") == 0: - _MlirCmp(p, resname, ob, out, outsz) - return - if string.strcmp(ob, "alloca") == 0: - _MlirAlloca(p, resname, out, outsz) - return - if string.strcmp(ob, "load") == 0: - _MlirLoad(p, resname, out, outsz) - return - if string.strcmp(ob, "store") == 0: - _MlirStore(p, out, outsz) - return - if string.strcmp(ob, "br") == 0: - _MlirBr(p, out, outsz) - return - if string.strcmp(ob, "ret") == 0: - _MlirRet(p, out, outsz) - return - if string.strcmp(ob, "unreachable") == 0: - _MlirUnreachable(out, outsz) - return - if string.strcmp(ob, "call") == 0: - _MlirCall(p, resname, out, outsz) - return - if string.strcmp(ob, "select") == 0: - _MlirSelect(p, resname, out, outsz) - return - if string.strcmp(ob, "phi") == 0: - if out is not None: - # Leading phis are skipped in _MlirEmitBlockLines; reaching here means phi is not at block head - MlirFail("MLIR render: phi is not at block head (v1 only supports leading consecutive phis -> block arguments)") - _MlirPhiScan(text) - return - if string.strcmp(ob, "getelementptr") == 0: - _MlirGep(p, resname, out, outsz) - return - if string.strcmp(ob, "bitcast") == 0: - _MlirBitcast(p, resname, out, outsz) - return - if _mlir_in_set(ob, "|sext|trunc|zext|ptrtoint|inttoptr|fpext|fptrunc|fptosi|sitofp|fptoui|uitofp|") != 0: - _MlirCast(p, resname, ob, out, outsz) - return - if string.strcmp(ob, "switch") == 0: - _MlirSwitch(p, out, outsz) - return - if string.strcmp(ob, "atomicrmw") == 0: - _MlirAtomicRmw(p, resname, out, outsz) - return - if string.strcmp(ob, "cmpxchg") == 0: - _MlirCmpXchg(p, resname, resname2, out, outsz) - return - if string.strcmp(ob, "va_arg") == 0: - _MlirVaArg(p, resname, out, outsz) - return - if string.strcmp(ob, "extractvalue") == 0: - # Real source: TPV's multi-result inline-asm result extraction (see _MlirExtractValue). - # Note: other unsupported opcodes like `insertvalue` never occur in the corpus and fall - # through to the loud fallback error ("unknown instruction opcode"). - _MlirExtractValue(p, resname, out, outsz) - return - # Note: inline asm's opcode in intermediate text is "call" (call asm ...); _MlirCall - # recognizes it and forwards to _MlirAsm (llvm.inline_asm). - MlirFail("MLIR render: unknown instruction opcode") - - -# ============================================================ -# Function signature and body -# ============================================================ -def _MlirFuncSig(buf: str, size: sizet, - func: Function*, pool: memhub.MemBuddy*): - """llvm.func @name(%a: i32, %b: i32) -> i32 (void return omits -> ty)""" - _append_cstr(buf, size, "llvm.func @") - if func.Name is not None: - if _ll_name_needs_quote(func.Name) != 0: - _append_cstr(buf, size, "\"") - _append_cstr(buf, size, func.Name) - _append_cstr(buf, size, "\"") - else: - _append_cstr(buf, size, func.Name) - _append_cstr(buf, size, "(") - params: GSList[Param]* = func.Params - cur: Param* = None - if params is not None: - cur = params.Head - first: int = 1 - cur_steps: int = 0 - while cur is not None: - cur_steps += 1 - if cur_steps > IR_DUMP_CHAIN_LIMIT: - MlirFail("MLIR render: parameter chain may be cyclic") - if first == 0: - _append_cstr(buf, size, ", ") - if cur.Name is not None: - if cur.Name[0] != '%': - _append_cstr(buf, size, "%") - _append_cstr(buf, size, cur.Name) - _append_cstr(buf, size, ": ") - ty_buf: str = _SC_Ty - ty_buf[0] = '\0' - TypePrint(ty_buf, 4096, cur.Ty, pool) - _append_cstr(buf, size, ty_buf) - cur = cur.Next - first = 0 - # Variadic function: append `...` at the end of the parameter list (mlir-opt 22.1.8 measured exit 0: - # `llvm.func @f(%p: !llvm.ptr, ...) -> i32` / with no fixed params `llvm.func @f(...) -> i32`) - if func.IsVarArg == 1: - if params is not None and params.Head is not None: - _append_cstr(buf, size, ", ...") - else: - _append_cstr(buf, size, "...") - _append_cstr(buf, size, ")") - # Return type (Void omitted) - is_void: int = 0 - match func.RetTy: - case LLVMType.Void(): - is_void = 1 - case _: - is_void = 0 - if is_void == 0: - ret_buf: str = _SC_Ty2 - ret_buf[0] = '\0' - TypePrint(ret_buf, 4096, func.RetTy, pool) - _append_cstr(buf, size, " -> ") - _append_cstr(buf, size, ret_buf) - - -def FunctionPrint(buf: str, size: sizet, - func: Function*, pool: memhub.MemBuddy*): - """Serialize function func as MLIR (llvm dialect) text APPENDED to buf. - - NOTE: like the llvmlite version, this appends (so the module printer can concatenate - functions); callers wanting single-function text must set buf[0]='\\0' themselves. - """ - global _MLIR_Pass, _MLIR_Func, _MLIR_SrcBlk, _MLIR_CurLine, _MlirOutTrunc - if func is None or buf is None or size == 0: - return - _MlirOutTrunc = 0 - - # Local peephole first (ret zeroinitializer->0/null, entry-slot store->load forwarding, - # no-op bitcast removal): peepholes match by name and delete lines by setting Buf=None. - # They act on the builders' intermediate instruction text and are orthogonal to MLIR rendering; - # RenumberSSA internally skips declare functions (IsDeclared==1). - # Run at most ONCE per body: the module is printed several times (STUB/TEXT/FULL, see PrintModule) - # and the T2 forwarding is NOT idempotent — a second pass over the already-optimized text can - # drop an entry alloca whose use was only reachable through a line the first pass had removed, - # leaving dangling SSA names (mlir-opt: "op using value defined outside the region"). - if func.IsDeclared == 0 and G_PeepEnable != 0 and func.PeepDone == 0: - _peephole_opt(func, pool) - func.PeepDone = 1 - RenumberSSA(func, pool) - - _MlirTablesInit(pool) - _MLIR_Func = func - # Record the function's own SSA names of the form c0/c1/... so the constant namer never picks a - # colliding %c (a user param/local literally named c1 would clash with a materialized const). - _MlirCollectUsedCNames(func) - # Clear the previous function's line context: signature/declare and other non-line-driven - # positions must not carry an unrelated stale line, avoiding misleading diagnostics. - _MLIR_CurLine = None - MlirSetContextLine(None) - - if func.IsDeclared == 1: - # LLVM intrinsic declarations are NOT emitted: on the MLIR side intrinsics are expressed - # via llvm.call_intrinsic (see _MlirCall). Emitting `llvm.func @llvm.va_start(!llvm.ptr)` - # would be down-leveled to an unmangled `declare void @llvm.va_start(ptr)`, and LLVM - # verification errors "Intrinsic name not mangled correctly for type arguments!". - # Rule: name starts with "llvm." and not "llvm.mlir.*" (the latter are MLIR-internal op names). - if func.Name is not None and string.strncmp(func.Name, "llvm.", 5) == 0 \ - and string.strncmp(func.Name, "llvm.mlir.", 10) != 0: - return - # declare (no body) - _MlirFuncSig(buf, size, func, pool) - _append_cstr(buf, size, "\n") - _MlirOutTruncFail() - return - - blocks: GSList[BasicBlock]* = func.Blocks - if blocks is None: - _MlirFuncSig(buf, size, func, pool) - _append_cstr(buf, size, "\n") - _MlirOutTruncFail() - return - - # ---- pass1: scan (register constants/aliases + shape checks) ---- - _MLIR_Pass = 1 - blk: BasicBlock* = blocks.Head - blk_steps: int = 0 - while blk is not None: - blk_steps += 1 - if blk_steps > IR_DUMP_CHAIN_LIMIT: - MlirFail("MLIR render: function block chain may be cyclic") - _MLIR_SrcBlk = blk - if blk.Lines is not None: - lines: GSList[Line]* = blk.Lines - ln: Line* = lines.Head - ln_steps: int = 0 - while ln is not None: - ln_steps += 1 - if ln_steps > IR_DUMP_CHAIN_LIMIT: - MlirFail("MLIR render: block instruction chain may be cyclic") - if ln.Buf is not None: - _MlirRenderLine(ln.Buf, None, 0) - ln = ln.Next - blk = blk.Next - - # ---- pass2: emit ---- - _MLIR_Pass = 2 - _MlirFuncSig(buf, size, func, pool) - _append_cstr(buf, size, " {\n") - blk = blocks.Head - first: int = 1 - blk_steps = 0 - while blk is not None: - blk_steps += 1 - if blk_steps > IR_DUMP_CHAIN_LIMIT: - MlirFail("MLIR render: function block chain may be cyclic") - _MLIR_SrcBlk = blk - if first != 0: - # entry block has no label; constant-materialization ops go at the entry block head - # (entry dominates the function, so constants are visible everywhere without breaking the phi-at-head rule) - _MlirCheckEntryNoPhi(blk) - _MlirEmitConsts(buf, size) - else: - _MlirEmitBlockHeader(buf, size, blk) - _MlirEmitBlockLines(buf, size, blk) - blk = blk.Next - first = 0 - _append_cstr(buf, size, "}\n") - _MlirOutTruncFail() - - -# ============================================================ -# Internal helpers -# ============================================================ -# _append_cstr write-position cache (perf: O(1) when appending into the same buffer, -# avoiding O(n^2) strlen(dst) rescans -- an IR-serialization hot spot). -# Falls back to strlen on buffer switch (different dst) or buffer reset (dst[0]=0 and pos>0). -_g_last_dst: uint64 = 0 -_g_last_pos: sizet = 0 - -def _append_cstr(dst: str, dst_size: sizet, src: str): - """Append C string src to the end of dst""" - global _g_last_dst, _g_last_pos, _MlirOutTrunc - if dst is None or src is None: return - dlen: sizet = 0 - if _g_last_dst == uint64(dst): - dlen = _g_last_pos - # Buffer reset (caller reuses the same address after buf[0]=0) -> restart from head - if dlen > 0 and dst[0] == '\0': - dlen = 0 - # Cache-consistency check: the cached end must still be the current text end (NUL at the - # end, non-NUL before it). If several modules write the same buf or pool reuse leaves old - # content, the cached pos goes stale and appends past the first NUL become invisible - # (LLvmLiteTest Test5/9 root cause); on failure fall back to strlen. - if dlen == 0: - if dst[0] != '\0': - dlen = string.strlen(dst) - elif dst[dlen] != '\0' or dst[dlen - 1] == '\0': - dlen = string.strlen(dst) - else: - dlen = string.strlen(dst) - _g_last_dst = uint64(dst) - # Unsigned-underflow guard: if dlen exceeds the buffer size (corrupted/dangling buffer), - # return; otherwise remain = dst_size - dlen underflows to a huge value and writes out of bounds. - if dlen >= dst_size: - _MlirOutTrunc = 1 - return - slen: sizet = string.strlen(src) - remain: sizet = dst_size - dlen - if remain <= 0: - _MlirOutTrunc = 1 - return - i: sizet = 0 - while i < slen and i + 1 < remain: - dst[dlen + i] = src[i] - i += 1 - if i < slen: - _MlirOutTrunc = 1 - dst[dlen + i] = '\0' - _g_last_pos = dlen + i - - -def _MlirOutTruncFail(): - """Output-truncation check before printing finishes (truncation errors loudly, never emits partial MLIR silently)""" - global _MlirOutTrunc - if _MlirOutTrunc != 0: - _MlirOutTrunc = 0 - MlirFail("MLIR render: output text truncated by buffer overflow (increase the dump_ir/FunctionPrint buffer)") - - -# ============================================================ -# Debug call-site recording (--traceback only; zero overhead when off) -# -# The MLIR backend emits no DWARF (see README 3.3): the final exe has no .debug_line records -# for our .vp functions, so runtime stackmaps cannot map a caller frame's PC (= return address) -# back to a call-site line. Here, at BUILD time, each define (F record) and each DIRECT call -# (C record) is recorded into a text buffer; after each module is translated TransPyV writes it -# to a .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 TransPyV's --traceback (relayed via DebugInfoGen.SetEnabled) -_g_dbg_on: int = 0 - -# Text buffer (stdlib-allocated, survives pool reset; cleared after DebugTakeText hands off ownership) -_g_dbg_buf: bytes = None -_g_dbg_len: sizet = 0 -_g_dbg_cap: sizet = 0 - -# Current function context (C records pull caller symbol/file/line from here); nested defines -# (lambdas etc.) use a fixed-size Push/Pop stack. -define DBGC_STACK_MAX = 256 -_g_dbg_func: str = None -_g_dbg_file: str = None -_g_dbg_defline: int = 0 -_g_dbg_line: int = 0 -_g_dbg_depth: int = 0 -_g_dbg_stk_func: bytes = None # 8 bytes per slot (char*) -_g_dbg_stk_file: bytes = None # 8 bytes per slot (char*) -_g_dbg_stk_def: bytes = None # 8 bytes per slot (sizet) -_g_dbg_stk_ln: bytes = None # 8 bytes per slot (sizet) - - -def DebugSetEnabled(flag: bool) -> int: - """Enable/disable debug call-site recording (relayed from DebugInfoGen.SetEnabled --traceback).""" - global _g_dbg_on - if flag: - _g_dbg_on = 1 - else: - _g_dbg_on = 0 - return 0 - - -def DebugIsEnabled() -> int: - return _g_dbg_on - - -def _DbgReserve(extra: sizet) -> int: - """Ensure the text buffer can hold at least extra more bytes (excluding the trailing NUL).""" - global _g_dbg_buf, _g_dbg_cap - if _g_dbg_buf is None: - cap0: sizet = 262144 - b: bytes = stdlib.malloc(cap0) - if b is None: - return 1 - b[0] = '\0' - _g_dbg_buf = b - _g_dbg_cap = cap0 - need: sizet = _g_dbg_len + extra + 1 - if need <= _g_dbg_cap: - return 0 - ncap: sizet = _g_dbg_cap - while ncap < need: - ncap = ncap * 2 - nb: bytes = stdlib.realloc(_g_dbg_buf, ncap) - if nb is None: - return 1 - _g_dbg_buf = nb - _g_dbg_cap = ncap - return 0 - - -def _DbgEnsureStack() -> int: - """Lazily allocate the Push/Pop context stack (only when enabled and actual nesting occurs).""" - global _g_dbg_stk_func, _g_dbg_stk_file, _g_dbg_stk_def, _g_dbg_stk_ln - if _g_dbg_stk_func is not None: - return 0 - b1: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8) - b2: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8) - b3: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8) - b4: bytes = stdlib.malloc(sizet(DBGC_STACK_MAX) * 8) - if b1 is None or b2 is None or b3 is None or b4 is None: - return 1 - _g_dbg_stk_func = b1 - _g_dbg_stk_file = b2 - _g_dbg_stk_def = b3 - _g_dbg_stk_ln = b4 - return 0 - - -def DebugNoteFunc(sym: str, file: str, line: int) -> int: - """Enter a define: push the outer context, set as current function, and write an F record.""" - global _g_dbg_func, _g_dbg_file, _g_dbg_defline, _g_dbg_line, _g_dbg_depth, _g_dbg_len - if _g_dbg_on == 0: - return 0 - if _g_dbg_depth >= DBGC_STACK_MAX: - MlirFail("MLIR debug record: function nesting depth exceeds limit (DBGC_STACK_MAX)") - if _DbgEnsureStack() != 0: - MlirFail("MLIR debug record: context stack allocation failed") - fs: str* = (str*)(uint64(_g_dbg_stk_func) + sizet(_g_dbg_depth) * 8) - fs[0] = _g_dbg_func - ff: str* = (str*)(uint64(_g_dbg_stk_file) + sizet(_g_dbg_depth) * 8) - ff[0] = _g_dbg_file - ds: sizet* = (sizet*)(uint64(_g_dbg_stk_def) + sizet(_g_dbg_depth) * 8) - ds[0] = sizet(_g_dbg_defline) - ls: sizet* = (sizet*)(uint64(_g_dbg_stk_ln) + sizet(_g_dbg_depth) * 8) - ls[0] = sizet(_g_dbg_line) - _g_dbg_depth += 1 - _g_dbg_func = sym - _g_dbg_file = file - _g_dbg_defline = line - _g_dbg_line = line - if sym is None or file is None: - return 0 - need: sizet = string.strlen(sym) + string.strlen(file) + 64 - if _DbgReserve(need) != 0: - MlirFail("MLIR debug record: F record buffer allocation failed") - viperlib.snprintf(_g_dbg_buf + _g_dbg_len, _g_dbg_cap - _g_dbg_len, - "F\t%s\t%d\t%s\n", sym, line, file) - _g_dbg_len = _g_dbg_len + string.strlen(_g_dbg_buf + _g_dbg_len) - return 0 - - -def DebugClearFunc() -> int: - """Leave a define: restore the outer context (clear if none).""" - global _g_dbg_func, _g_dbg_file, _g_dbg_defline, _g_dbg_line, _g_dbg_depth - if _g_dbg_on == 0: - return 0 - if _g_dbg_depth <= 0: - _g_dbg_func = None - _g_dbg_file = None - _g_dbg_defline = 0 - _g_dbg_line = 0 - return 0 - _g_dbg_depth -= 1 - fs: str* = (str*)(uint64(_g_dbg_stk_func) + sizet(_g_dbg_depth) * 8) - _g_dbg_func = fs[0] - ff: str* = (str*)(uint64(_g_dbg_stk_file) + sizet(_g_dbg_depth) * 8) - _g_dbg_file = ff[0] - ds: sizet* = (sizet*)(uint64(_g_dbg_stk_def) + sizet(_g_dbg_depth) * 8) - _g_dbg_defline = int(ds[0]) - ls: sizet* = (sizet*)(uint64(_g_dbg_stk_ln) + sizet(_g_dbg_depth) * 8) - _g_dbg_line = int(ls[0]) - return 0 - - -def DebugNoteLine(line: int) -> int: - """Record the current statement line (used by the following build_call to write a C record).""" - global _g_dbg_line - if _g_dbg_on == 0: - return 0 - if line > 0: - _g_dbg_line = line - return 0 - - -def DebugResetFunc() -> int: - """Clear the current function context (module-start fallback: a mid-translation failure may leave an un-Popped context).""" - global _g_dbg_func, _g_dbg_file, _g_dbg_defline, _g_dbg_line, _g_dbg_depth - if _g_dbg_on == 0: - return 0 - _g_dbg_func = None - _g_dbg_file = None - _g_dbg_defline = 0 - _g_dbg_line = 0 - _g_dbg_depth = 0 - return 0 - - -def _DebugNoteCall(callee: str) -> int: - """Write a C record (caller = current function, line = current statement line).""" - global _g_dbg_len - if _g_dbg_on == 0: - return 0 - if _g_dbg_func is None or _g_dbg_file is None or _g_dbg_line <= 0: - return 0 - if callee is None or callee[0] == '\0': - return 0 - if string.strncmp(callee, "llvm.", 5) == 0: - return 0 - need: sizet = string.strlen(_g_dbg_func) + string.strlen(callee) \ - + string.strlen(_g_dbg_file) + 64 - if _DbgReserve(need) != 0: - MlirFail("MLIR debug record: C record buffer allocation failed") - viperlib.snprintf(_g_dbg_buf + _g_dbg_len, _g_dbg_cap - _g_dbg_len, - "C\t%s\t%s\t%d\t%s\n", - _g_dbg_func, callee, _g_dbg_line, _g_dbg_file) - _g_dbg_len = _g_dbg_len + string.strlen(_g_dbg_buf + _g_dbg_len) - return 0 - - -def DebugTakeText() -> str: - """Take and clear the accumulated text (once per module); buffer ownership passes to the caller.""" - global _g_dbg_buf, _g_dbg_len, _g_dbg_cap - if _g_dbg_buf is None or _g_dbg_len == 0: - if _g_dbg_buf is not None: - _g_dbg_len = 0 - _g_dbg_buf[0] = '\0' - return None - out: str = _g_dbg_buf - _g_dbg_buf = None - _g_dbg_len = 0 - _g_dbg_cap = 0 - return out - - -# ============================================================ -# Field accessors (let other modules bypass stub type limits) -# -# Cross-module, the user module only has stub types for Function/Param (missing fields, possibly -# wrong types), so TransPyC silently skips field access (no GEP, no error) and fields stay null. -# These accessors run inside this module where the full type definitions exist, so they can access -# all fields correctly; other modules get field values via function calls, bypassing the stub limits. -# ============================================================ -def function_get_name(func: Function*) -> str: - """Get the function name""" - if func is None: - return None - return func.Name - - -def function_get_name_len(func: Function*) -> sizet: - """Get the function name length (computed once at creation, always == strlen(Name)). - - Mirrors function_get_name's cross-module stub-bypass pattern: the user module only has a stub - Function type (missing fields, possibly wrong types), so reading Name/NameLen directly is - silently skipped. This accessor reads fields inside the full-type module and returns the length - via a call, avoiding repeated strlen on the same name (a program-wide strlen hot spot). - """ - if func is None: - return 0 - return func.NameLen - - -def function_get_ret_ty(func: Function*) -> LLVMType*: - """Get the function return type""" - if func is None: - return None - return func.RetTy - - -def function_get_params(func: Function*) -> GSList[Param]*: - """Get the function parameter list container""" - if func is None: - return None - return func.Params - - -def function_get_param_head(func: Function*) -> Param*: - """Get the function parameter list head (one step, avoiding GSList.Head access by callers)""" - if func is None: - return None - params: GSList[Param]* = func.Params - if params is None: - return None - return params.Head - - -def function_get_next(func: Function*) -> Function*: - """Get the next function in the list""" - if func is None: - return None - return func.Next - - -def function_is_declared(func: Function*) -> int: - """Get whether the function is a declaration (1=declare, 0=define)""" - if func is None: - return 0 - return func.IsDeclared - - -def param_get_name(param: Param*) -> str: - """Get the parameter name""" - if param is None: - return None - return param.Name - - -def param_get_ty(param: Param*) -> LLVMType*: - """Get the parameter type""" - if param is None: - return None - return param.Ty - - -def param_get_next(param: Param*) -> Param*: - """Get the next parameter in the list""" - if param is None: - return None - return param.Next +from stdint import * +import string +import viperlib +import memhub +import stdio +import stdlib +from linkedlist import GSListNode, GSList +from .__types import ( + LLVMType, ParamNode, TypePrint, + MlirTypeText, MlirTypeTextIsPointer, MlirFail, MlirSetContextLine, +) + + +# dump sentinel: IR list traversal step cap (cycle detection, fail-fast, aligned with __module.vp). +# A broken Next cycle would hang traversal; over the cap it prints the list name and exit(1). +define IR_DUMP_CHAIN_LIMIT = 100000 + +# Peephole master switch (T2 forwarding/ret zeroing, T3 no-op bitcast removal). +# --no-llvmpeephole zeroes it so FunctionPrint skips local peepholes for IR diffing (RenumberSSA still runs). +G_PeepEnable: bool = True + +# Function-text truncation flag: _append_cstr sets it to 1 when output hits the buffer cap. +# Checked before FunctionPrint returns and MlirFail'd: truncated MLIR must fail loudly, never silently. +_MlirOutTrunc: int = 0 + + +# ============================================================ +# Line: instruction text line +# +# One LLVM IR instruction serializes to one text line, held in a linked list. +# Extends GSListNode[Line] for typed Next: Line|CPtr +# +# NOTE: must carry novirtual, else TransPyC adds a vtable ptr (T*) but drops +# the parent GSListNode[T] Next field, misaligning the struct so field reads return garbage. +# ============================================================ +novirtual class Line(GSListNode[Line]): + Buf: str # line text (NUL-terminated) + Len: sizet # text length (excluding NUL) + + +# ============================================================ +# BasicBlock: basic block +# +# Holds name + instruction-line list + terminator flag. +# Extends GSListNode[BasicBlock] for typed Next: BasicBlock|CPtr +# Lines is a GSList[Line] pointer providing O(1) append. +# +# NOTE: must carry novirtual (same reason as Line). +# ============================================================ +novirtual class BasicBlock(GSListNode[BasicBlock]): + Name: str # block name (e.g. "entry"/"if.then") + Lines: GSList[Line]* # instruction-line list container (Head/Tail/Count) + IsTerminated: int # 1=terminator present (br/ret) + + +# ============================================================ +# Param: function parameter +# +# Extends GSListNode[Param] for typed Next: Param|CPtr +# Attrs holds parameter attribute text (e.g. "noundef"/"nocapture"/"readonly"), None=none. +# +# NOTE: must carry novirtual (same reason as Line). +# ============================================================ +novirtual class Param(GSListNode[Param]): + Ty: LLVMType* # parameter type + Name: str # parameter name (e.g. "%0"/"%a") + Attrs: str # parameter attribute text (None=none) + + +# ============================================================ +# Function: function +# +# Holds name + return type + parameter list + basic-block list. +# Extends GSListNode[Function] for typed Next: Function|CPtr +# Params/Blocks are GSList pointers providing O(1) append. +# Attrs holds function-level attribute text (e.g. "nounwind"/"noinline"), None=none. +# +# NOTE: must carry novirtual (same reason as Line). +# ============================================================ +novirtual class Function(GSListNode[Function]): + Name: str # function name + RetTy: LLVMType* # return type + Params: GSList[Param]* # parameter list container + Blocks: GSList[BasicBlock]* # basic-block list container + IsDeclared: int # 1=declaration only (declare), 0=definition (define) + IsVarArg: int # 1=variadic function (...) + Attrs: str # function attribute text (None=none) + AttrGroup: str # string attribute group content (e.g. '"section"=".text.startup"', None=none) + AttrGroupId: int # attribute group id (>0 emits #N on the function line and attributes #N at module end) + NameLen: sizet # function name length (computed once at creation, always == strlen(Name); appended last so existing field offsets stay) + DbgRef: str # DISubprogram ref (kept as-is; ignored by MLIR printing, e.g. "!3") + PeepDone: int # 1=local peephole already applied to this body (runs once; see FunctionPrint) + + +# ============================================================ +# Factory functions +# ============================================================ +# Line hardcoded size: Next(8) + Buf(8) + Len(8) = 24 bytes +define LINE_SIZE = 24 + +def new_line(pool: memhub.MemBuddy*, text: str) -> Line*: + """Create an instruction line, copying text into a newly allocated buffer""" + ptr: Line* = pool.alloc(LINE_SIZE) + if ptr is None: return None + string.memset(ptr, 0, LINE_SIZE) + if text is not None: + slen: sizet = string.strlen(text) + buf: str = pool.alloc(slen + 1) + if buf is not None: + string.strcpy(buf, text) + ptr.Buf = buf + ptr.Len = slen + return ptr + + +def _new_gslist(pool: memhub.MemBuddy*, list_size: sizet) -> rawptr: + """Allocate and zero-init a GSList container from pool (Head/Tail/Count all zero)""" + gsl: rawptr = pool.alloc(list_size) + if gsl is None: return None + string.memset(gsl, 0, list_size) + return gsl + + +# BasicBlock hardcoded size: Next(8) + Name(8) + Lines(8) + IsTerminated(4) = 32 bytes +define BLOCK_SIZE = 32 + +def new_basic_block(pool: memhub.MemBuddy*, name: str) -> BasicBlock*: + """Create a basic block""" + ptr: BasicBlock* = pool.alloc(BLOCK_SIZE) + if ptr is None: return None + string.memset(ptr, 0, BLOCK_SIZE) + ptr.Name = name + ptr.Lines = _new_gslist(pool, 24) # GSList: Head+Tail+Count + ptr.IsTerminated = 0 + return ptr + + +# Param hardcoded size: Next(8) + Ty(8) + Name(8) + Attrs(8) = 32 bytes +define PARAM_SIZE = 32 + +def new_param(pool: memhub.MemBuddy*, ty: LLVMType*, + name: str) -> Param*: + """Create a function parameter""" + ptr: Param* = pool.alloc(PARAM_SIZE) + if ptr is None: return None + string.memset(ptr, 0, PARAM_SIZE) + ptr.Ty = ty + ptr.Name = name + ptr.Attrs = None + return ptr + + +# Function hardcoded size: Next(8) + Name(8) + RetTy(8) + Params(8) + Blocks(8) + IsDeclared(4) + IsVarArg(4) + Attrs(8) + AttrGroup(8) + AttrGroupId(8) + NameLen(8) + DbgRef(8) + PeepDone(8) = 96 bytes (same layout as llvmlite) +define FUNC_SIZE = 96 + +def new_function(pool: memhub.MemBuddy*, name: str, + ret_ty: LLVMType*) -> Function*: + """Create a function""" + ptr: Function* = pool.alloc(FUNC_SIZE) + if ptr is None: return None + string.memset(ptr, 0, FUNC_SIZE) + ptr.Name = name + ptr.RetTy = ret_ty + ptr.Params = _new_gslist(pool, 24) # GSList: Head+Tail+Count + ptr.Blocks = _new_gslist(pool, 24) + ptr.IsDeclared = 0 + ptr.Attrs = None + ptr.AttrGroup = None + ptr.AttrGroupId = 0 + ptr.PeepDone = 0 + # NameLen computed once: Name never changes, so NameLen == strlen(Name) (avoids repeated strlen). + # name=None sets 0, matching new_line's null convention (explicit for fixed semantics, not allocator behavior). + if name is not None: + ptr.NameLen = string.strlen(name) + else: + ptr.NameLen = 0 + return ptr + + +# ============================================================ +# List operations (delegating to GSList.append) +# ============================================================ +def function_add_param(func: Function*, param: Param*): + """Append a parameter to the function's parameter list""" + if func is None or param is None: return + # NOTE: use a local variable + type parameter (see function_move_block_to_end): chaining + # func.Params.append() mis-resolves to LinkedNode.append and corrupts memory 48 bytes before the container. + params: GSList[Param]* = func.Params + if params is None: return + params.append(param) + + +def function_add_block(func: Function*, block: BasicBlock*): + """Append a basic block to the function's block list""" + if func is None or block is None: return + blocks: GSList[BasicBlock]* = func.Blocks + if blocks is None: return + blocks.append(block) + + +def function_move_block_to_end(func: Function*, block: BasicBlock*): + """Move block to the end of the function's block list (BB text order == control-flow order). + + Fixes non-monotonic SSA numbering (BBs are created in order but instructions are emitted in + control-flow order) that makes llc error; called at position_at_end so BB order matches position order. + NOTE: must use the typed GSList[BasicBlock] parameter, else TransPyC builds an incomplete struct + type and blocks.Head/Tail/Count field reads return garbage. + """ + if func is None or block is None: return + blocks: GSList[BasicBlock]* = func.Blocks + if blocks is None: return + + # Already Tail; nothing to move + if blocks.Tail is block: + return + + # Remove block from the list + if blocks.Head is block: + # block is Head; advance Head directly + blocks.Head = block.Next + else: + # Walk to find the predecessor + prev: BasicBlock* = blocks.Head + while prev is not None and prev.Next is not block: + prev = prev.Next + if prev is None: + # block not in list; append directly + blocks.append(block) + return + prev.Next = block.Next + + # block.Next handled by the removal logic; reset to None + block.Next = None + + # Append at the end (no blocks.append call to avoid double-incrementing Count) + if blocks.Tail is not None: + blocks.Tail.Next = block + blocks.Tail = block + + +def block_append_line(block: BasicBlock*, line: Line*): + """Append an instruction line to the basic block""" + if block is None or line is None: return + lines: GSList[Line]* = block.Lines + if lines is None: return + lines.append(line) + + +def block_set_terminated(block: BasicBlock*, val: int): + """Set the block terminator flag (cross-module call to avoid opaque chained field access)""" + if block is None: return + block.IsTerminated = val + + +def block_get_terminated(block: BasicBlock*) -> int: + """Get the block terminator flag (cross-module call to avoid opaque chained field access)""" + if block is None: + return 1 + return block.IsTerminated + + +def block_append_text(pool: memhub.MemBuddy*, block: BasicBlock*, + text: str): + """Create an instruction line and append it to the basic block""" + if block is None or text is None: return + line: Line* = new_line(pool, text) + if line is not None: + block_append_line(block, line) + + +def block_insert_text_before_terminator(pool: memhub.MemBuddy*, + block: BasicBlock*, + text: str): + """Create an instruction line and insert it before the block terminator. + + If the block is not terminated, append at the end; if terminated (br/ret), insert before + the terminator so alloca and similar instructions do not land after it. + """ + if block is None or text is None: return + if block.Lines is None: return + line: Line* = new_line(pool, text) + if line is None: return + # Block not terminated: append directly + if block.IsTerminated == 0: + block_append_line(block, line) + return + # Block terminated: insert before Tail (the terminator) + lines: GSList[Line]* = block.Lines + if lines.Head is None: + block_append_line(block, line) + return + # Single node: insert before Head + if lines.Head is lines.Tail: + line.Next = lines.Head + lines.Head = line + lines.Count += 1 + return + # Find the predecessor of Tail + prev: Line* = lines.Head + while prev is not None and prev.Next is not lines.Tail: + prev = prev.Next + if prev is None: + # No predecessor found; fall back to append + block_append_line(block, line) + return + # Insert line between prev and Tail + line.Next = lines.Tail + prev.Next = line + lines.Count += 1 + + +# ============================================================ +# Attribute setters +# ============================================================ +def function_set_attrs(func: Function*, attrs: str): + """Set function-level attribute text (e.g. "nounwind"/"noinline")""" + if func is None: return + func.Attrs = attrs + + +_g_attr_group_counter: int = 0 + +def function_set_attr_group(func: Function*, group: str): + """Set function string attribute group (e.g. '"section"=".text.startup"') and assign #N. + + LLVM string attributes (with a value, e.g. section) cannot be inlined on the signature + line; they must use a group: the function line emits #N and the group is emitted at the end. + """ + global _g_attr_group_counter + if func is None or group is None: + return + func.AttrGroup = group + _g_attr_group_counter += 1 + func.AttrGroupId = _g_attr_group_counter + + +def param_set_attrs(param: Param*, attrs: str): + """Set parameter attribute text (e.g. "noundef"/"nocapture"/"readonly")""" + if param is None: return + param.Attrs = attrs + + +def function_set_dbg(func: Function*, ref: str): + """Set the function-level debug ref (DISubprogram node ref, e.g. "!3"). + + Kept as-is: MLIR printing ignores it (FunctionPrint emits no !dbg), so this only + records the model without changing output. ref=None clears it. + """ + if func is None: return + func.DbgRef = ref + + +# ============================================================ +# _ll_name_needs_quote - whether a symbol name needs @"..." quoting in MLIR +# +# WARNING: the rule differs from LLVM (measured on mlir-opt 22.1.8): +# - A bare identifier's first char must be a letter or '_': leading digit, '.', '$' must be quoted. +# => VPC's {sha1}.{name} all start with a hex digit, so all need quotes; +# => the string-constant global @.str starts with '.', so it needs quotes too. +# - Later chars allow only [A-Za-z0-9_$] and '.'. +# - '-' is illegal in a bare MLIR name (LLVM allows it); measured error "expected '('". +# - '['/']' (generic specialization names) are likewise illegal. +# This predicate takes the conservative direction: when unsure, quote (@"..." is legal for any name). +# ============================================================ +def _ll_name_needs_quote(name: str) -> int: + """MLIR bare-name predicate: returns 1 (needs quotes) unless it matches [A-Za-z_][A-Za-z0-9_$.]*""" + if name is None: + return 0 + name_len: sizet = string.strlen(name) + if name_len == 0: + return 1 + c0: char = name[0] + if not ((c0 >= 'a' and c0 <= 'z') or (c0 >= 'A' and c0 <= 'Z') or c0 == '_'): + return 1 + i: sizet = 1 + while i < name_len: + ch: char = name[i] + if not ((ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or + (ch >= '0' and ch <= '9') or ch == '_' or ch == '$' or ch == '.'): + return 1 + i += 1 + return 0 + + +# ============================================================ +# _MlirSymRefAppend - emit a symbol reference (unified quoting rule) +# +# name does NOT include a leading '@'; an already-quoted form (first char '"') is emitted as-is. +# Used where a symbol appears as an operand (e.g. llvm.mlir.addressof), sharing the same +# predicate with _MlirFuncSig / call target / global name. +# ============================================================ +def _MlirSymRefAppend(out: str, outsz: sizet, name: str): + if name is None or name[0] == '\0': + MlirFail("MLIR render: empty symbol name") + if name[0] == '"': + _append_cstr(out, outsz, "@") + _append_cstr(out, outsz, name) + return + if _ll_name_needs_quote(name) != 0: + _append_cstr(out, outsz, "@\"") + _append_cstr(out, outsz, name) + _append_cstr(out, outsz, "\"") + return + _append_cstr(out, outsz, "@") + _append_cstr(out, outsz, name) + + +# ============================================================ +# RenumberSSA: renumber numeric SSA names within a function +# +# Fixes the non-monotonic SSA names in text output caused by build_alloca_at_entry +# inserting allocas before the entry block terminator (llc error: +# "instruction expected to be numbered '%N' or greater"). +# +# Example: entry originally %0-%6 + br; alloca_at_entry inserts %28/%41 before br, +# so text order is entry(%0..%6, %28, %41) -> if.then(%7..%27), +# placing %28 before %7, which llc rejects. +# +# Algorithm: +# 1. Scan all BB instruction lines, collect numeric SSA names %N in text-output order +# 2. Build an old->new map by first-occurrence order (incrementing from 0) +# 3. Replace %N in every instruction line text with the new number +# +# NOTE: +# - Only pure numeric SSA names (%0, %1, ...) are renumbered +# - Named SSA names (%entry, %.tmp, %self), block labels and global symbols @N are not +# - Only called for define (with a body); declare has no body to process +# - ViperC params are mostly named (%self/%env/%n), so no numeric param renumbering needed +# ============================================================ + +# Whether ch is a decimal digit +def _is_digit_ch(ch: char) -> int: + """Return 1 if ch is '0'-'9'. + + ch is a value type (char = i8); TPC has fixed is-None handling for value types + (always false, generating an i1 0), so no null deref occurs. + """ + if ch is None: + return 0 + if ch >= '0' and ch <= '9': + return 1 + return 0 + + +# ============================================================ +# Safe i32 array access (explicit byte offset) +# +# TransPyC's ptr[i] pointer indexing may be treated as byte indexing (i8*) instead of i32* +# indexing, corrupting memory. Must compute the byte offset explicitly: addr = base + idx * 4, +# then cast to i32*. See HandlesTranslator.py:358's uint64(ptr) + offset * sizeof usage. +# ============================================================ +def _map_get_i32(map_arr: str, idx: int) -> int: + """Read i32 array map_arr[idx] (explicit byte offset idx*4). + + Casts the byte offset to i32* via int*(uint64), then dereferences with defef. + TPC has fixed type-union casts (_HandleTypeUnionCastLlvm fallback), so this form works. + """ + if map_arr is None: + return -1 + byte_off: uint64 = uint64(map_arr) + idx * 4 + elem_ptr: int* = int*(byte_off) + return *elem_ptr + + +def _map_set_i32(map_arr: str, idx: int, val: int): + """Write map_arr[idx] = val (i32 array, explicit byte offset idx*4). + + Casts via int*(uint64) and assigns through defefas. + TPC has fixed type-union casts, so this form works. + """ + if map_arr is None: + return + byte_off: uint64 = uint64(map_arr) + idx * 4 + elem_ptr: int* = int*(byte_off) + *elem_ptr = val + + +def _scan_max_ssa_in_text(text: str, cur_max: int) -> int: + """Scan text for the largest numeric %N and return max(cur_max, found). + + Only matches '%' immediately followed by a digit; skips named SSA names (%entry etc.). + """ + result: int = cur_max + p: str = text + while p is not None and p[0] != '\0': + if p[0] == '%': + nxt: str = p + 1 + if nxt is not None and _is_digit_ch(nxt[0]) != 0: + num: int = string.atoi(nxt) + if num > result: + result = num + p = p + 1 + return result + + +def _find_max_ssa_number(func: Function*) -> int: + """Walk all BB instruction lines for the largest %N; returns -1 if no numeric SSA names""" + max_n: int = -1 + if func is None or func.Blocks is None: + return max_n + blocks: GSList[BasicBlock]* = func.Blocks + blk: BasicBlock* = blocks.Head + blk_steps: sizet = 0 + while blk is not None: + blk_steps += 1 + if blk_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa scan aborted)\n", func.Name) + stdlib.exit(1) + if blk.Lines is not None: + lines: GSList[Line]* = blk.Lines + line: Line* = lines.Head + line_steps: sizet = 0 + while line is not None: + line_steps += 1 + if line_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa scan aborted)\n", + func.Name, blk.Name) + stdlib.exit(1) + if line.Buf is not None: + max_n = _scan_max_ssa_in_text(line.Buf, max_n) + line = line.Next + blk = blk.Next + return max_n + + +def _assign_new_numbers(text: str, + map_arr: str, + max_n: int, next_new: int) -> int: + """Scan text; for each numeric %N, assign next_new if map_arr[N]==-1. + + Returns the updated next_new. Mapping is built in first-occurrence order. + """ + result: int = next_new + p: str = text + while p is not None and p[0] != '\0': + if p[0] == '%': + nxt: str = p + 1 + if nxt is not None and _is_digit_ch(nxt[0]) != 0: + num: int = string.atoi(nxt) + if num >= 0 and num <= max_n: + if _map_get_i32(map_arr, num) == -1: + _map_set_i32(map_arr, num, result) + result += 1 + p = p + 1 + return result + + +def _replace_ssa_numbers(text: str, + pool: memhub.MemBuddy*, + map_arr: str, + max_n: int) -> str: + """Build new text with each %N replaced by its mapped new number. + + Char-by-char scan: a numeric %N is looked up and substituted, others copied as-is. + Returns a newly allocated buffer (caller need not free the old Buf; pool manages it). + """ + src_len: sizet = string.strlen(text) + # New buffer: worst case each number grows in digits; allocate src_len*2 + 64 + new_size: sizet = src_len * 2 + 64 + new_buf: str = pool.alloc(new_size) + if new_buf is None: + return None + # Allocate the digit buffer once (reused in the loop) + num_buf: str = pool.alloc(16) + if num_buf is None: + new_buf[0] = '\0' + return new_buf + + dst_idx: sizet = 0 + src_p: str = text + while src_p is not None and src_p[0] != '\0': + if dst_idx + 16 >= new_size: + # Buffer too small; truncate + break + if src_p[0] == '%': + nxt: str = src_p + 1 + if nxt is not None and _is_digit_ch(nxt[0]) != 0: + num: int = string.atoi(nxt) + # Range-check before reading map_arr: when num > max_n the array holds only + # (max_n+1)*4 bytes, so a read-before-check would read out of bounds. + if num >= 0 and num <= max_n: + new_num: int = _map_get_i32(map_arr, num) + if new_num != -1: + # Write '%' + new_buf[dst_idx] = '%' + dst_idx += 1 + # Write the new number + num_buf[0] = '\0' + viperlib.snprintf(num_buf, 16, "%d", new_num) + k: sizet = 0 + while num_buf[k] != '\0' and dst_idx + 1 < new_size: + new_buf[dst_idx] = num_buf[k] + dst_idx += 1 + k += 1 + # Skip the old digit chars + src_p = src_p + 1 + while src_p is not None and _is_digit_ch(src_p[0]) != 0: + src_p = src_p + 1 + continue + # Ordinary char: copy directly + new_buf[dst_idx] = src_p[0] + dst_idx += 1 + src_p = src_p + 1 + + new_buf[dst_idx] = '\0' + return new_buf + + +def RenumberSSA(func: Function*, pool: memhub.MemBuddy*): + """Renumber all numeric SSA names in the function so text-output order is monotonic. + + Called before FunctionPrint serializes the body; only applies to define (IsDeclared==0) + functions with non-empty Blocks. + """ + if func is None or pool is None: + return + if func.IsDeclared == 1: + return # declare has no body + if func.Blocks is None: + return + + # 1. First pass: find the largest SSA number + max_n: int = _find_max_ssa_number(func) + if max_n < 0: + return # no numeric SSA names + + # 2. Allocate and init map_arr[0..max_n] = -1; stored as i8* (pool.alloc), accessed via + # _map_get_i32/_map_set_i32; memset to 0xFF (-1 per 4-byte i32) to avoid ptr[i] indexing issues. + map_bytes: sizet = (max_n + 1) * 4 # int = 4 bytes + map_arr: str = pool.alloc(map_bytes) + if map_arr is None: + return + string.memset(map_arr, 0xFF, map_bytes) # 0xFF * 4 = 0xFFFFFFFF = -1 + + # 3. Second pass: assign new numbers in text order + next_new: int = 0 + blocks: GSList[BasicBlock]* = func.Blocks + blk: BasicBlock* = blocks.Head + blk_steps: sizet = 0 + while blk is not None: + blk_steps += 1 + if blk_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa renumber aborted)\n", func.Name) + stdlib.exit(1) + if blk.Lines is not None: + lines: GSList[Line]* = blk.Lines + line: Line* = lines.Head + line_steps: sizet = 0 + while line is not None: + line_steps += 1 + if line_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa renumber aborted)\n", + func.Name, blk.Name) + stdlib.exit(1) + if line.Buf is not None: + next_new = _assign_new_numbers(line.Buf, map_arr, max_n, next_new) + line = line.Next + blk = blk.Next + + # 4. Check whether any renumbering is needed (old != new) + need_renumber: int = 0 + i: int = 0 + while i <= max_n: + v: int = _map_get_i32(map_arr, int(i)) + if v != -1 and v != i: + need_renumber = 1 + break + i += 1 + if need_renumber == 0: + return # already monotonic; no replacement needed + + # 5. Third pass: replace every instruction line text + blk = blocks.Head + blk_steps = 0 + while blk is not None: + blk_steps += 1 + if blk_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] cyclic list: func blocks (func name=%s, ssa replace aborted)\n", func.Name) + stdlib.exit(1) + if blk.Lines is not None: + lines: GSList[Line]* = blk.Lines + line: Line* = lines.Head + line_steps = 0 + while line is not None: + line_steps += 1 + if line_steps > IR_DUMP_CHAIN_LIMIT: + stdio.printf("[IR-DUMP] cyclic list: block lines (func name=%s, block=%s, ssa replace aborted)\n", + func.Name, blk.Name) + stdlib.exit(1) + if line.Buf is not None: + new_buf: str = _replace_ssa_numbers( + line.Buf, pool, map_arr, max_n) + if new_buf is not None: + line.Buf = new_buf + line.Len = string.strlen(new_buf) + line = line.Next + blk = blk.Next + + +# ============================================================ +# Local peephole optimization (text layer, before output, semantics-preserving) +# T1: ret 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 + st_ok: int = 0 + st_count: int = 0 + st_line_order: int = -1 + load_ltoks: str = pool.alloc(8 * 64) + load_cnt: int = 0 + load_entry_order: int64 = 0 + alloca_order: int = -1 + order: int = 0 + unsafe: int = 0 + # First pass: count this alloca's store/load/other refs and line order + blk_s = blk + while blk_s is not None: + l_s: Line* = None + if blk_s.Lines is not None: + scan_lines: GSList[Line]* = blk_s.Lines + l_s = scan_lines.Head + while l_s is not None: + sb: str = l_s.Buf + if sb is not None: + is_alloc_def: int = 0 + if sb[0] == '%': + if _peep_is_alloc_of(sb, a_name, a_name_len) != 0: + is_alloc_def = 1 + if is_alloc_def == 0 and _peep_last_ref_eq(sb, a_name, a_name_len) != 0: + if _peep_is_startwith(sb, "store ") != 0: + st_count += 1 + if st_count == 1: + vcomma: str = string.strstr(sb + 6, ", ") + if vcomma is not None: + vlen: sizet = sizet(uint64(vcomma) - uint64(sb + 6)) + if vlen > 0: + vbuf: str = pool.alloc(vlen + 1) + if vbuf is not None: + string.strncpy(vbuf, sb + 6, vlen) + vbuf[vlen] = '\0' + st_val = vbuf + st_ok = 1 + else: + unsafe = 1 + if blk_s == entry_blk: + st_line_order = order + elif string.strstr(sb, " = load ") is not None: + ls: str = string.strstr(sb, "%") + if ls is not None and load_cnt < 64: + lname_len: sizet = sizet( + uint64(string.strstr(sb, " = load ")) - uint64(ls)) + lname: str = pool.alloc(lname_len + 1) + if lname is not None: + string.strncpy(lname, ls + 1, lname_len - 1) + lname[lname_len - 1] = '\0' + slotp: str* = (str*)(uint64(load_ltoks) + load_cnt * 8) + slotp[0] = lname + load_cnt += 1 + if blk_s == entry_blk and st_line_order < 0: + load_entry_order += 1 + else: + # lname alloc failed: this load won't enter load_ltoks, but the + # delete phase still removes its def line by slot name, leaving a + # dangling use. Conservatively abandon T2 forwarding for this slot. + unsafe = 1 + else: + # Loads exceed record capacity (load_ltoks=8*64, cap 64) or the line has + # no '%' to parse a def name: unrecorded loads would still have their + # " = load " def lines deleted by slot name, leaving dangling uses + # (llc: "use of undefined value", e.g. strcmp short-circuit >64 branch). + # Only a fully recorded load set allows deleting all defs, so + # conservatively abandon the whole T2 forwarding for this slot (IR stays valid). + unsafe = 1 + else: + unsafe = 1 + if sb is not None: + order += 1 + l_s = l_s.Next + blk_s = blk_s.Next + # Criteria + safe_val: str = None + if unsafe == 0 and st_count == 1 and st_ok != 0 and st_line_order >= 0 \ + and load_cnt > 0 and load_entry_order == 0 and st_val is not None: + pct: str = string.strchr(st_val, '%') + if pct is None: + sp1: str = string.strchr(st_val, ' ') + if sp1 is not None and sp1[1] != 0: + safe_val = sp1 + 1 + else: + safe_val = st_val + else: + if string.strchr(pct, ' ') is None and string.strchr(pct, ',') is None: + safe_val = pct + if safe_val is not None: + # Second pass: replace load-name references + # safe_val is constant during this slot's replacement; strlen computed once per slot + safe_len: sizet = string.strlen(safe_val) + for li2 in range(int(load_cnt)): + slotsp: str* = (str*)(uint64(load_ltoks) + li2 * 8) + ltok: str = slotsp[0] + if ltok is not None: + # ltok is constant across the load name's function-wide scan; strlen computed once + ltok_len: sizet = string.strlen(ltok) + blk_r = blk + while blk_r is not None: + lr: Line* = None + if blk_r.Lines is not None: + repl_lines: GSList[Line]* = blk_r.Lines + lr = repl_lines.Head + while lr is not None: + if lr.Buf is not None: + nb2: str = _peep_replace_token(pool, lr.Buf, ltok, safe_val, + ltok_len, safe_len) + if nb2 is not None and nb2 is not lr.Buf: + lr.Buf = nb2 + lr = lr.Next + blk_r = blk_r.Next + # Delete: alloca def / the single store / all load lines + blk_d = blk + while blk_d is not None: + ld: Line* = None + if blk_d.Lines is not None: + kill_lines: GSList[Line]* = blk_d.Lines + ld = kill_lines.Head + while ld is not None: + db: str = ld.Buf + if db is not None: + kill: int = 0 + if db[0] == '%' and db[1] == a_name[0]: + if _peep_is_alloc_of(db, a_name, a_name_len) != 0: + kill = 1 + if kill == 0 and _peep_last_ref_eq(db, a_name, a_name_len) != 0: + if _peep_is_startwith(db, "store ") != 0 \ + or string.strstr(db, " = load ") is not None: + kill = 1 + if kill != 0: + ld.Buf = None + ld = ld.Next + blk_d = blk_d.Next + e_line = e_line.Next + + # ---------- T3: no-op bitcast removal ---------- + _peep_noop_bitcast(func, pool) + + +def _peep_noop_bitcast(func: Function*, pool: memhub.MemBuddy*): + """T3: remove no-op bitcast (bitcast %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 + +# 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, _MlirTablesValid, _MlirTablesPool + if pool is None: + MlirFail("MLIR render: memory pool is null") + if _MlirTablesValid != 0 and _MlirTablesPool == pool: + # Reuse the live buffers: reset the per-function logical state. Entries past the reset counts + # are unreachable (all lookups scan [0, count)), so no table body needs clearing; only the + # used-name bitmap must be zeroed because _MlirCollectUsedCNames only ever sets bits. + _MLIR_Pool = pool + _MLIR_Pass = 0 + _MLIR_ConstCount = 0 + _MLIR_ConstNameSeq = 0 + _MLIR_AliasCount = 0 + string.memset(_MLIR_UsedC, 0, _MLIR_ConstCap * 4) + return + _MLIR_Pool = pool + _MLIR_Pass = 0 + _MLIR_ConstCount = 0 + _MLIR_ConstNameSeq = 0 + _MLIR_AliasCount = 0 + _MLIR_ConstCap = MLIR_CONST_CAP + _MLIR_ConstTy = pool.alloc(_MLIR_ConstCap * 8) + _MLIR_ConstVal = pool.alloc(_MLIR_ConstCap * 8) + _MLIR_ConstName = pool.alloc(_MLIR_ConstCap * 8) + _MLIR_UsedC = pool.alloc(_MLIR_ConstCap * 4) + _MLIR_AliasCap = MLIR_ALIAS_CAP + _MLIR_AliasFrom = pool.alloc(_MLIR_AliasCap * 8) + _MLIR_AliasTo = pool.alloc(_MLIR_AliasCap * 8) + if _MLIR_ConstTy is None or _MLIR_ConstVal is None or _MLIR_ConstName is None \ + or _MLIR_UsedC is None or _MLIR_AliasFrom is None or _MLIR_AliasTo is None: + MlirFail("MLIR render: constant/alias table allocation failed") + string.memset(_MLIR_ConstTy, 0, _MLIR_ConstCap * 8) + string.memset(_MLIR_ConstVal, 0, _MLIR_ConstCap * 8) + string.memset(_MLIR_ConstName, 0, _MLIR_ConstCap * 8) + string.memset(_MLIR_UsedC, 0, _MLIR_ConstCap * 4) + string.memset(_MLIR_AliasFrom, 0, _MLIR_AliasCap * 8) + string.memset(_MLIR_AliasTo, 0, _MLIR_AliasCap * 8) + _SC_Op = pool.alloc(64) + _SC_Tb = pool.alloc(512) + _SC_Ty = pool.alloc(4096) + _SC_Ty2 = pool.alloc(4096) + _SC_V1 = pool.alloc(512) + _SC_V2 = pool.alloc(512) + _SC_V3 = pool.alloc(512) + _SC_Callee = pool.alloc(512) + _SC_Args = pool.alloc(2048) + _SC_ArgTs = pool.alloc(8192) + _SC_IdxV = pool.alloc(1024) + _SC_IdxT = pool.alloc(4096) + _SC_Mt = pool.alloc(4096) + _SC_Tmp = pool.alloc(8192) + _SC_Tmp2 = pool.alloc(4096) + _SC_P1 = pool.alloc(512) + _SC_P2 = pool.alloc(512) + _SC_B1 = pool.alloc(4096) + _SC_B2 = pool.alloc(4096) + _SC_Buf = pool.alloc(4096) + if _SC_Op is None or _SC_Ty is None or _SC_Mt is None or _SC_Tmp is None: + MlirFail("MLIR render: parse scratch allocation failed") + _SC_Op[0] = '\0' + _SC_Ty[0] = '\0' + _SC_Mt[0] = '\0' + _SC_Tmp[0] = '\0' + _MlirTablesPool = pool + _MlirTablesValid = 1 + + +# ============================================================ +# Constant table / alias table +# ============================================================ +def _MlirAliasLookup(name: str) -> str: + """Look up the alias table; returns None on miss""" + if name is None: + return None + for i in range(int(_MLIR_AliasCount)): + sp: str* = _mlir_slotp(_MLIR_AliasFrom, int(i)) + if sp[0] is not None and string.strcmp(sp[0], name) == 0: + st: str* = _mlir_slotp(_MLIR_AliasTo, int(i)) + return st[0] + return None + + +def _MlirAliasGrow(): + """扩容 alias 表:池内分配新块并拷贝旧内容,容量翻倍(旧块留在池里,不回收)。 + + 原实现把上限写死为 MLIR_ALIAS_CAP(512),超出直接 MlirFail 中断整次编译—— + 这是资源限制,不该让编译失败。大函数里 no-op bitcast(别名)条数会超过它。 + """ + global _MLIR_AliasFrom, _MLIR_AliasTo, _MLIR_AliasCap + old_cap: int = _MLIR_AliasCap + nb_from: bytes = _MLIR_Pool.alloc(old_cap * 16) + nb_to: bytes = _MLIR_Pool.alloc(old_cap * 16) + if nb_from is None or nb_to is None: + MlirFail("MLIR render: alias table growth allocation failed") + string.memcpy(nb_from, _MLIR_AliasFrom, old_cap * 8) + string.memcpy(nb_to, _MLIR_AliasTo, old_cap * 8) + string.memset(nb_from + old_cap * 8, 0, old_cap * 8) + string.memset(nb_to + old_cap * 8, 0, old_cap * 8) + _MLIR_AliasFrom = nb_from + _MLIR_AliasTo = nb_to + _MLIR_AliasCap = old_cap * 2 + + +def _MlirConstGrow(): + """扩容常量表 + 自动常量名空间(%c)+ 已用名表:池内分配翻倍新块并拷贝旧内容。 + + 原实现三处都钉死在 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: + MlirFail("MLIR render: float bit-pattern width mismatches type (f16/f32/f64/f128 need 4/8/16/32 hex digits respectively)") + return val + # Already decimal text: use as-is + return val + + +def _MlirConstFail(tytext: str, valtext: str): + """Unsupported constant form -> loud failure including the type and value. + + MlirFail gives no backtrace, so print (MLIR 类型, 常量文本) — otherwise an + "unsupported constant operand form" is unattributable (which pointer/aggregate constant, + and from which operand shape). Supports i/f literals, true/false, null/zeroinitializer and + `@sym`; anything else (e.g. a pointer/aggregate-typed literal or a constant expression) + lands here and must be lowered explicitly. + """ + tlen: sizet = 0 + vlen: sizet = 0 + if tytext is not None: tlen = string.strlen(tytext) + if valtext is not None: vlen = string.strlen(valtext) + bsize: sizet = tlen + vlen + 160 + buf: str = _MLIR_Pool.alloc(bsize) + if buf is None: + MlirFail("MLIR render: unsupported constant operand form") + viperlib.snprintf(buf, bsize, "MLIR render: unsupported constant operand form (type '%s', value '%s')", tytext, valtext) + MlirFail(buf) + + +def _MlirEmitOneConst(out: str, outsz: sizet, mt: str, val: str, nm: str): + """Emit one constant-materialization op""" + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, nm) + if val[0] == '@': + # Symbol reference (global/function address): use the unified quoting rule + # (LLVM and MLIR bare-name rules differ: MLIR disallows leading digit/'.' and '-') + _append_cstr(out, outsz, " = llvm.mlir.addressof ") + _MlirSymRefAppend(out, outsz, val + 1) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, mt) + elif string.strcmp(val, "null") == 0 or string.strcmp(val, "zeroinitializer") == 0: + _append_cstr(out, outsz, " = llvm.mlir.zero : ") + _append_cstr(out, outsz, mt) + else: + lit: str = val + if mt[0] == 'f': + lit = _MlirFloatLiteral(val, mt) + elif mt[0] != 'i': + _MlirConstFail(mt, val) + _append_cstr(out, outsz, " = llvm.mlir.constant(") + _append_cstr(out, outsz, lit) + # i1's `true`/`false` are MLIR bare boolean keywords, no type suffix allowed: + # `llvm.mlir.constant(false : i1)` makes mlir-opt error "expected ')'". + # Other scalars must be written `value : type` (otherwise treated as an untyped attr, mismatching the result type). + if string.strcmp(lit, "true") != 0 and string.strcmp(lit, "false") != 0: + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, mt) + _append_cstr(out, outsz, ") : ") + _append_cstr(out, outsz, mt) + _append_cstr(out, outsz, "\n") + + +def _MlirEmitConsts(out: str, outsz: sizet): + """Emit all constant-materialization ops at the entry block head in registration order""" + for i in range(int(_MLIR_ConstCount)): + st: str* = _mlir_slotp(_MLIR_ConstTy, int(i)) + sv: str* = _mlir_slotp(_MLIR_ConstVal, int(i)) + sn: str* = _mlir_slotp(_MLIR_ConstName, int(i)) + _MlirEmitOneConst(out, outsz, st[0], sv[0], sn[0]) + + +# ============================================================ +# Parsing primitives for intermediate instruction text +# ============================================================ +def _mlir_ws(p: str) -> str: + """Skip whitespace (space/tab/newline)""" + if p is None: + return None + while p[0] == ' ' or p[0] == '\t' or p[0] == '\n' or p[0] == '\r': + p = p + 1 + return p + + +def _mlir_delim(ch: int) -> int: + """Whether ch ends a token""" + if ch == 0 or ch == 32 or ch == 9 or ch == 10 or ch == 13: + return 1 + if ch == ',' or ch == '(' or ch == ')' or ch == '[' or ch == ']' \ + or ch == ':' or ch == '=': + return 1 + return 0 + + +def _mlir_read_value(p: str, out: str, outsz: sizet) -> str: + """Read a value token (%name / @"name" / %"name" / literal); returns the new cursor""" + p = _mlir_ws(p) + n: sizet = 0 + if p[0] == '\0': + out[0] = '\0' + return p + if p[0] == '%' or p[0] == '@': + if n + 1 < outsz: + out[n] = p[0] + n += 1 + p = p + 1 + if p[0] == '"': + if n + 1 < outsz: + out[n] = '"' + n += 1 + p = p + 1 + while p[0] != '\0' and p[0] != '"': + if n + 1 < outsz: + out[n] = p[0] + n += 1 + p = p + 1 + if p[0] == '"': + if n + 1 < outsz: + out[n] = '"' + n += 1 + p = p + 1 + out[n] = '\0' + return p + while p[0] != '\0' and _mlir_delim(p[0]) == 0: + if n + 1 < outsz: + out[n] = p[0] + n += 1 + p = p + 1 + out[n] = '\0' + return p + + +def _mlir_read_type(p: str, out: str, outsz: sizet, nofunc: int = 0) -> str: + """Read a type token (MLIR form !llvm... or scalar iN/fN, also LLVM aggregate forms). + + Bracket balancing covers `[] {} () <>`: MLIR's !llvm.array<...>/!llvm.struct<(...)> contain + spaces and commas and must be read whole by depth counting; bracketless types (!llvm.ptr) + end at a top-level delimiter. + + With nofunc=1, does NOT absorb the following ` (...)` function-type suffix: in a call's return + type position, `(...)` (LLVM syntax) is the callee's parameter type list, not part of the return + type, so it must be left to _MlirCall's variadic branch (see the comment there). + """ + p = _mlir_ws(p) + n: sizet = 0 + if p[0] == '\0': + out[0] = '\0' + return p + c0: int = p[0] + if c0 == '!' or c0 == '[' or c0 == '{' or c0 == '(': + d: int = 0 + while p[0] != '\0': + ch: int = p[0] + if ch == '[' or ch == '{' or ch == '(' or ch == '<': + d += 1 + elif ch == ']' or ch == '}' or ch == ')' or ch == '>': + if n + 1 < outsz: + out[n] = char(ch) + n += 1 + p = p + 1 + d -= 1 + if d <= 0: + break + continue + elif d == 0 and (ch == ' ' or ch == ',' or ch == '\t' + or ch == '\n' or ch == '\r'): + # Top-level delimiter: bracketless types (e.g. !llvm.ptr) end here + break + if n + 1 < outsz: + out[n] = char(ch) + n += 1 + p = p + 1 + while p[0] == '*': + if n + 1 < outsz: + out[n] = '*' + n += 1 + p = p + 1 + out[n] = '\0' + return p + if c0 == '%': + p = _mlir_read_value(p, out, outsz) + n = string.strlen(out) + while p[0] == '*': + if n + 1 < outsz: + out[n] = '*' + n += 1 + p = p + 1 + out[n] = '\0' + return p + # Ordinary word (i32 / i8* / void / double ...) + while p[0] != '\0' and _mlir_delim(p[0]) == 0: + if n + 1 < outsz: + out[n] = p[0] + n += 1 + p = p + 1 + # Function-type suffix " ( ... )" (not absorbed when nofunc=1, see the docstring) + q: str = _mlir_ws(p) + if nofunc == 0 and q[0] == '(': + if n + 1 < outsz: + out[n] = ' ' + n += 1 + p = q + d2: int = 0 + while p[0] != '\0': + ch2: int = p[0] + if ch2 == '(': + d2 += 1 + elif ch2 == ')': + if n + 1 < outsz: + out[n] = char(ch2) + n += 1 + p = p + 1 + d2 -= 1 + if d2 <= 0: + break + continue + if n + 1 < outsz: + out[n] = char(ch2) + n += 1 + p = p + 1 + while p[0] == '*': + if n + 1 < outsz: + out[n] = '*' + n += 1 + p = p + 1 + out[n] = '\0' + return p + + +def _mlir_in_set(ob: str, setstr: str) -> int: + """Whether opcode belongs to a "|a|b|c|"-form set""" + tb: str = _SC_Tb + tb[0] = '\0' + string.strcat(tb, "|") + string.strcat(tb, ob) + string.strcat(tb, "|") + if string.strstr(setstr, tb) is not None: + return 1 + return 0 + + +def _MlirBlkRef(t: str) -> str: + """Block reference text: strip the leading '%' (MLIR blocks use ^name)""" + if t is None: + return "" + if t[0] == '%': + return t + 1 + return t + + +def _MlirFindBlock(func: Function*, name: str) -> BasicBlock*: + """Find a basic block by name (without %) within the function""" + if func is None or func.Blocks is None or name is None: + return None + blocks: GSList[BasicBlock]* = func.Blocks + blk: BasicBlock* = blocks.Head + steps: int = 0 + while blk is not None: + steps += 1 + if steps > IR_DUMP_CHAIN_LIMIT: + MlirFail("MLIR render: block chain may be cyclic") + if blk.Name is not None and string.strcmp(blk.Name, name) == 0: + return blk + blk = blk.Next + return None + + +def _MlirIsPhiLine(text: str) -> int: + """Whether the line is a phi line""" + if text is None: + return 0 + if string.strstr(text, " = phi ") is not None: + return 1 + return 0 + + +# ============================================================ +# Operand rendering (with constant materialization) +# ============================================================ +def _MlirOperand(val: str, tyllvm: str, out: str, outsz: sizet) -> str: + """Render one operand and return its final text. + + SSA value -> %name after alias resolution; + non-SSA (literal/symbol ref) -> materialized %cN (registered during the scan pass). + """ + if val is None or val[0] == '\0': + MlirFail("MLIR render: empty operand") + if val[0] == '%': + r: str = _MlirResolve(val) + _append_cstr(out, outsz, r) + return r + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, tyllvm, _MLIR_Pool) + nm: str = _MlirConstGet(mt, val) + _append_cstr(out, outsz, nm) + return nm + + +# ============================================================ +# phi parsing +# ============================================================ +def _MlirPhiScan(text: str): + """Scan a phi line: register all incoming constants (scan pass only)""" + p: str = _mlir_ws(text) + tb: str = _SC_Buf + p = _mlir_read_value(p, tb, 512) + p = _mlir_ws(p) + if p[0] == '=': + p = _mlir_ws(p + 1) + ob: str = _SC_Op + p = _mlir_read_value(p, ob, 64) + if string.strcmp(ob, "phi") != 0: + return + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + g: int = 0 + while p[0] != '\0': + g += 1 + if g > 512: + MlirFail("MLIR render: too many phi incoming edges") + p = _mlir_ws(p) + if p[0] != '[': + break + p = _mlir_ws(p + 1) + vb: str = _SC_P1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + bb: str = _SC_P2 + p = _mlir_read_value(p, bb, 512) + p = _mlir_ws(p) + if p[0] == ']': + p = p + 1 + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + # Register this incoming value (constants/symbol refs need materialization) + _MlirOperand(vb, tyb, None, 0) + + +def _MlirPhiParam(text: str) -> int: + """Take a phi's result name and type as block params; on success writes _SC_B1(name)/_SC_B2(type) and returns 1""" + p: str = _mlir_ws(text) + nm: str = _SC_B1 + p = _mlir_read_value(p, nm, 512) + p = _mlir_ws(p) + if p[0] != '=': + return 0 + p = _mlir_ws(p + 1) + ob: str = _SC_Op + p = _mlir_read_value(p, ob, 64) + if string.strcmp(ob, "phi") != 0: + return 0 + tyb: str = _SC_B2 + _mlir_read_type(p, tyb, 512) + return 1 + + +def _MlirPhiFindIncoming(text: str, src: BasicBlock*) -> int: + """Find the phi incoming edge matching the source block; on success writes _SC_B1(value)/_SC_B2(type) and returns 1""" + p: str = _mlir_ws(text) + tb: str = _SC_Buf + p = _mlir_read_value(p, tb, 512) + p = _mlir_ws(p) + if p[0] == '=': + p = _mlir_ws(p + 1) + ob: str = _SC_Op + p = _mlir_read_value(p, ob, 64) + if string.strcmp(ob, "phi") != 0: + return 0 + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + srcn: str = None + if src is not None: + srcn = src.Name + g: int = 0 + while p[0] != '\0': + g += 1 + if g > 512: + MlirFail("MLIR render: too many phi incoming edges") + p = _mlir_ws(p) + if p[0] != '[': + break + p = _mlir_ws(p + 1) + vb: str = _SC_P1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + bb: str = _SC_P2 + p = _mlir_read_value(p, bb, 512) + p = _mlir_ws(p) + if p[0] == ']': + p = p + 1 + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + if srcn is not None: + bn: str = bb + if bn[0] == '%': + bn = bn + 1 + if string.strcmp(bn, srcn) == 0: + string.strcpy(_SC_B1, vb) + string.strcpy(_SC_B2, tyb) + return 1 + return 0 + + +# ============================================================ +# Block parameters / block arguments +# ============================================================ +def _MlirBlockHasPhi(blk: BasicBlock*) -> int: + """Whether the block head has a phi (i.e. the block carries block params)""" + if blk is None or blk.Lines is None: + return 0 + lines: GSList[Line]* = blk.Lines + ln: Line* = lines.Head + while ln is not None: + if ln.Buf is not None: + if _MlirIsPhiLine(ln.Buf) != 0: + return 1 + return 0 + ln = ln.Next + return 0 + + +def _MlirBlockOperands(out: str, outsz: sizet, + target: BasicBlock*, src: BasicBlock*): + """Emit target's block arguments when branching to it: (%v : ty, ...). + + Incoming edges are matched to phis by source block; no match -> loud error (never silently drop args). + """ + if target is None or src is None: + return + if _MlirBlockHasPhi(target) == 0: + return + tmp: str = _SC_Tmp + tmp[0] = '\0' + lines: GSList[Line]* = target.Lines + ln: Line* = lines.Head + cnt: int = 0 + while ln is not None: + if ln.Buf is None: + ln = ln.Next + continue + if _MlirIsPhiLine(ln.Buf) == 0: + break + if _MlirPhiFindIncoming(ln.Buf, src) == 0: + MlirFail("MLIR render: target block has phi but no incoming edge matches the source block") + if cnt > 0: + _append_cstr(tmp, 8192, ", ") + _MlirOperand(_SC_B1, _SC_B2, tmp, 4096) + _append_cstr(tmp, 8192, " : ") + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, _SC_B2, _MLIR_Pool) + _append_cstr(tmp, 8192, mt) + cnt += 1 + ln = ln.Next + if cnt == 0: + MlirFail("MLIR render: target block has phi but no block arguments were generated") + _append_cstr(out, outsz, "(") + _append_cstr(out, outsz, tmp) + _append_cstr(out, outsz, ")") + + +def _MlirEmitBlockHeader(out: str, outsz: sizet, blk: BasicBlock*): + """Emit a non-entry block header: ^name(%p: ty, ...):""" + _append_cstr(out, outsz, "^") + if blk.Name is not None: + _append_cstr(out, outsz, blk.Name) + if _MlirBlockHasPhi(blk) != 0: + tmp: str = _SC_Tmp + tmp[0] = '\0' + lines: GSList[Line]* = blk.Lines + ln: Line* = lines.Head + cnt: int = 0 + while ln is not None: + if ln.Buf is None: + ln = ln.Next + continue + if _MlirIsPhiLine(ln.Buf) == 0: + break + if _MlirPhiParam(ln.Buf) != 0: + if cnt > 0: + _append_cstr(tmp, 8192, ", ") + _append_cstr(tmp, 8192, _SC_B1) + _append_cstr(tmp, 8192, ": ") + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, _SC_B2, _MLIR_Pool) + _append_cstr(tmp, 8192, mt) + cnt += 1 + ln = ln.Next + if cnt == 0: + MlirFail("MLIR render: failed to parse leading phi") + _append_cstr(out, outsz, "(") + _append_cstr(out, outsz, tmp) + _append_cstr(out, outsz, ")") + _append_cstr(out, outsz, ":\n") + + +def _MlirEmitBlockLines(out: str, outsz: sizet, blk: BasicBlock*): + """Emit in-block instructions (skip leading phis, which became block params)""" + if blk is None or blk.Lines is None: + return + lines: GSList[Line]* = blk.Lines + ln: Line* = lines.Head + skipping: int = 1 + steps: int = 0 + while ln is not None: + steps += 1 + if steps > IR_DUMP_CHAIN_LIMIT: + MlirFail("MLIR render: block instruction chain may be cyclic") + if ln.Buf is not None: + if skipping != 0 and _MlirIsPhiLine(ln.Buf) != 0: + ln = ln.Next + continue + skipping = 0 + _MlirRenderLine(ln.Buf, out, outsz) + ln = ln.Next + + +def _MlirCheckEntryNoPhi(blk: BasicBlock*): + """entry block must not have phis (no predecessor, cannot become block params)""" + if _MlirBlockHasPhi(blk) != 0: + MlirFail("MLIR render: entry block must not have phi (MLIR entry block takes no block arguments)") + + +# ============================================================ +# Per-instruction MLIR rendering +# ============================================================ +def _MlirEmitRes(out: str, outsz: sizet, resname: str): + """Emit the " [%N = ]" prefix""" + _append_cstr(out, outsz, " ") + if resname is not None: + _append_cstr(out, outsz, resname) + _append_cstr(out, outsz, " = ") + + +def _MlirBinop(p: str, resname: str, ob: str, out: str, outsz: sizet): + """Integer/float binary op: llvm. %a, %b : ty""" + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + lb: str = _SC_V1 + p = _mlir_read_value(p, lb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + rb: str = _SC_V2 + _mlir_read_value(p, rb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.") + _append_cstr(out, outsz, ob) + _append_cstr(out, outsz, " ") + _MlirOperand(lb, tyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(rb, tyb, out, outsz) + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, tyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, mt) + _append_cstr(out, outsz, "\n") + + +def _MlirFneg(p: str, resname: str, out: str, outsz: sizet): + """fneg: llvm.fneg %a : ty""" + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + vb: str = _SC_V1 + _mlir_read_value(p, vb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.fneg ") + _MlirOperand(vb, tyb, out, outsz) + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, tyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, mt) + _append_cstr(out, outsz, "\n") + + +def _MlirCmp(p: str, resname: str, ob: str, out: str, outsz: sizet): + """icmp/fcmp: llvm.icmp "sgt" %a, %b : ty + + The intermediate text is LLVM syntax `icmp , ` -- predicate before type, + so the read order must be predicate -> type -> operands. + Do not read the type first: the predicate (e.g. `sgt`) would be passed as type text, triggering + "cannot convert to MLIR type text" and aborting. + """ + pb: str = _SC_P1 + p = _mlir_read_value(p, pb, 512) + p = _mlir_ws(p) + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + lb: str = _SC_V1 + p = _mlir_read_value(p, lb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + rb: str = _SC_V2 + _mlir_read_value(p, rb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.") + _append_cstr(out, outsz, ob) + _append_cstr(out, outsz, " \"") + _append_cstr(out, outsz, pb) + _append_cstr(out, outsz, "\" ") + _MlirOperand(lb, tyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(rb, tyb, out, outsz) + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, tyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, mt) + _append_cstr(out, outsz, "\n") + + +def _MlirAlloca(p: str, resname: str, out: str, outsz: sizet): + """alloca: llvm.alloca %cN x ty {alignment = N : i64} : (i32) -> !llvm.ptr + + The MLIR form of the alignment attribute was determined by measuring mlir-opt 22.1.8 + (do NOT rely on memory): `{alignment = 16 : i64}` -> exit 0 (adopted); + `align(16)` / `, align 16` (LLVM syntax) -> exit 1 (expected ':'); `{alignment = 16 : i32}` + -> exit 1 (requires i64). So the intermediate `, align N` must be rewritten to `{alignment = N : i64}`. + + Alignment accepts only a decimal constant that is a power of two: mlir-opt does not check + power-of-two (`{alignment = 3 : i64}` exits 0), but non-powers are illegal in LLVM semantics, so this + layer gates it: non-constant/non-power/out-of-range (> 2^30) always errors loudly. + """ + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + p = _mlir_ws(p) + aligntxt: str = _SC_P2 + aligntxt[0] = '\0' + if p[0] == ',': + # ", align N" (the intermediate-text form from build_alloca_aligned) + p = _mlir_ws(p + 1) + if string.strncmp(p, "align", 5) != 0: + MlirFail("MLIR render: alloca only supports the align attribute (unknown attribute)") + p = _mlir_ws(p + 5) + nd: int = 0 + while p[0] >= '0' and p[0] <= '9': + if nd + 1 >= 512: + MlirFail("MLIR render: alloca alignment value too long (out of range)") + aligntxt[nd] = p[0] + nd += 1 + p = p + 1 + aligntxt[nd] = '\0' + p = _mlir_ws(p) + if p[0] != '\0': + MlirFail("MLIR render: extra token after alloca align") + if nd == 0: + MlirFail("MLIR render: alloca alignment must be a constant (decimal constant, not SSA)") + if nd > 10: + MlirFail("MLIR render: alloca alignment out of range (must be a power of 2 and <= 2^30)") + av: int = string.atoi(aligntxt) + if av <= 0 or av > 1073741824: + MlirFail("MLIR render: alloca alignment out of range (must be a power of 2 and <= 2^30)") + if (av & (av - 1)) != 0: + MlirFail("MLIR render: alloca alignment must be a power of 2") + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, tyb, _MLIR_Pool) + cnt: str = _MlirConstGet("i32", "1") + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.alloca ") + _append_cstr(out, outsz, cnt) + _append_cstr(out, outsz, " x ") + _append_cstr(out, outsz, mt) + if aligntxt[0] != '\0': + _append_cstr(out, outsz, " {alignment = ") + _append_cstr(out, outsz, aligntxt) + _append_cstr(out, outsz, " : i64}") + _append_cstr(out, outsz, " : (i32) -> !llvm.ptr\n") + + +def _MlirLoad(p: str, resname: str, out: str, outsz: sizet): + """load: llvm.load %p : !llvm.ptr -> ty""" + p = _mlir_ws(p) + vol: int = 0 + if string.strncmp(p, "volatile ", 9) == 0: + vol = 1 + p = _mlir_ws(p + 9) + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ptyb: str = _SC_Ty2 + p = _mlir_read_type(p, ptyb, 512) + p = _mlir_ws(p) + pb: str = _SC_V1 + _mlir_read_value(p, pb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.load ") + if vol != 0: + _append_cstr(out, outsz, "volatile ") + _MlirOperand(pb, ptyb, out, outsz) + tmt: str = _SC_Mt + tmt[0] = '\0' + MlirTypeText(tmt, 4096, tyb, _MLIR_Pool) + pmt: str = _SC_Tmp2 + pmt[0] = '\0' + MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, pmt) + _append_cstr(out, outsz, " -> ") + _append_cstr(out, outsz, tmt) + _append_cstr(out, outsz, "\n") + + +def _MlirStore(p: str, out: str, outsz: sizet): + """store: llvm.store %v, %p : ty, !llvm.ptr""" + p = _mlir_ws(p) + vol: int = 0 + if string.strncmp(p, "volatile ", 9) == 0: + vol = 1 + p = _mlir_ws(p + 9) + vtyb: str = _SC_Ty + p = _mlir_read_type(p, vtyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ptyb: str = _SC_Ty2 + p = _mlir_read_type(p, ptyb, 512) + p = _mlir_ws(p) + pb: str = _SC_V2 + _mlir_read_value(p, pb, 512) + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.store ") + if vol != 0: + _append_cstr(out, outsz, "volatile ") + _MlirOperand(vb, vtyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(pb, ptyb, out, outsz) + vmt: str = _SC_Mt + vmt[0] = '\0' + MlirTypeText(vmt, 1024, vtyb, _MLIR_Pool) + pmt: str = _SC_Tmp2 + pmt[0] = '\0' + MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, vmt) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, pmt) + _append_cstr(out, outsz, "\n") + + +def _MlirBr(p: str, out: str, outsz: sizet): + """br: in LLVM syntax the conditional branch opcode is also `br`, dispatched by whether the first token is `label`. + + br label %x -> llvm.br ^x + br i1 %c, label %a, label %b -> llvm.cond_br %c, ^a, ^b + + Note: build_cond_br emits intermediate text `br i1 ...` (NOT `cond_br`), so the conditional + form must be forwarded here to _MlirCondBr; otherwise it is taken as unconditional and the + type `i1` is mistaken for a block name (producing `llvm.br ^i1` and dropping the condition). + """ + p = _mlir_ws(p) + if string.strncmp(p, "label ", 6) != 0: + # Conditional branch: ` , label %a, label %b` + _MlirCondBr(p, out, outsz) + return + p = _mlir_ws(p + 6) + tb: str = _SC_V1 + _mlir_read_value(p, tb, 512) + br: str = _MlirBlkRef(tb) + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.br ^") + _append_cstr(out, outsz, br) + _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, br), _MLIR_SrcBlk) + _append_cstr(out, outsz, "\n") + + +def _MlirCondBr(p: str, out: str, outsz: sizet): + """cond_br: llvm.cond_br %c, ^then(%v : ty), ^else(%v : ty)""" + ctyb: str = _SC_Ty + p = _mlir_read_type(p, ctyb, 512) + p = _mlir_ws(p) + cb: str = _SC_V1 + p = _mlir_read_value(p, cb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + if string.strncmp(p, "label ", 6) == 0: + p = _mlir_ws(p + 6) + tb: str = _SC_V2 + p = _mlir_read_value(p, tb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + if string.strncmp(p, "label ", 6) == 0: + p = _mlir_ws(p + 6) + eb: str = _SC_V3 + _mlir_read_value(p, eb, 512) + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.cond_br ") + _MlirOperand(cb, ctyb, out, outsz) + tr: str = _MlirBlkRef(tb) + _append_cstr(out, outsz, ", ^") + _append_cstr(out, outsz, tr) + _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, tr), _MLIR_SrcBlk) + er: str = _MlirBlkRef(eb) + _append_cstr(out, outsz, ", ^") + _append_cstr(out, outsz, er) + _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, er), _MLIR_SrcBlk) + _append_cstr(out, outsz, "\n") + + +def _MlirRet(p: str, out: str, outsz: sizet): + """ret: llvm.return %v : ty (void omits operand and type)""" + p = _mlir_ws(p) + if string.strncmp(p, "void", 4) == 0 and (p[4] == '\0' or p[4] == ' '): + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.return\n") + return + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + vb: str = _SC_V1 + _mlir_read_value(p, vb, 512) + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.return ") + _MlirOperand(vb, tyb, out, outsz) + mt: str = _SC_Mt + mt[0] = '\0' + MlirTypeText(mt, 4096, tyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, mt) + _append_cstr(out, outsz, "\n") + + +def _MlirUnreachable(out: str, outsz: sizet): + """unreachable: llvm.unreachable""" + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.unreachable\n") + + +def _MlirCall(p: str, resname: str, out: str, outsz: sizet): + """call: llvm.call @f(%a) : (i32) -> i32 / indirect llvm.call %fp(%a) : !llvm.ptr, (i32) -> i32 + + Variadic calls (intermediate text `call (...) @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, 512) + fmt: str = _SC_IdxT + fmt[0] = '\0' + MlirTypeText(fmt, 4096, ftb, _MLIR_Pool) + _append_cstr(vftys, 8192, fmt) + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + calb: str = _SC_Callee + p = _mlir_read_value(p, calb, 512) + p = _mlir_ws(p) + if p[0] != '(': + MlirFail("MLIR render: call missing argument list") + p = p + 1 + args: str = _SC_Args + argts: str = _SC_ArgTs + args[0] = '\0' + argts[0] = '\0' + nargs: int = 0 + g: int = 0 + while p[0] != '\0': + g += 1 + if g > 256: + MlirFail("MLIR render: too many call arguments") + p = _mlir_ws(p) + if p[0] == ')': + break + atyb: str = _SC_Ty2 + p = _mlir_read_type(p, atyb, 512) + p = _mlir_ws(p) + avb: str = _SC_V1 + p = _mlir_read_value(p, avb, 512) + if nargs > 0: + _append_cstr(args, 2048, ", ") + _append_cstr(argts, 8192, ", ") + _MlirOperand(avb, atyb, args, 2048) + amt: str = _SC_Tmp2 + amt[0] = '\0' + MlirTypeText(amt, 4096, atyb, _MLIR_Pool) + _append_cstr(argts, 8192, amt) + nargs += 1 + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + rmt: str = _SC_Mt + rmt[0] = '\0' + MlirTypeText(rmt, 1024, rtyb, _MLIR_Pool) + # LLVM intrinsics (bare symbol @llvm.xxx, not llvm.mlir.*) must use the generic op + # `llvm.call_intrinsic "llvm.xxx"(...) : (...) -> ...`: mlir-translate mangles the intrinsic + # name by operand types (e.g. llvm.va_start -> llvm.va_start.p0). A plain llvm.call would emit + # an unmangled @llvm.va_start and LLVM verification errors + # "Intrinsic name not mangled correctly for type arguments!". + if calb[0] == '@' and string.strncmp(calb + 1, "llvm.", 5) == 0 \ + and string.strncmp(calb + 1, "llvm.mlir.", 10) != 0: + if string.strcmp(rmt, "none") == 0: + _MlirEmitRes(out, outsz, None) + else: + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.call_intrinsic \"") + _append_cstr(out, outsz, calb + 1) + _append_cstr(out, outsz, "\"(") + _append_cstr(out, outsz, args) + _append_cstr(out, outsz, ") : (") + _append_cstr(out, outsz, argts) + _append_cstr(out, outsz, ") -> ") + if string.strcmp(rmt, "none") == 0: + _append_cstr(out, outsz, "()") + else: + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, "\n") + return + if string.strcmp(rmt, "none") == 0: + _MlirEmitRes(out, outsz, None) + else: + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.call ") + # callee 也必须过 alias 解析:no-op bitcast(含函数指针 bitcast)被省略后,间接调用点 + # 必须改写成别名目标,否则 %N 悬空 → mlir-opt 报 use of undeclared SSA value + # / definition has type 'i8' but previously used here with type '!llvm.ptr'。 + calb_r: str = _MlirResolve(calb) + if calb_r is None: calb_r = calb + _append_cstr(out, outsz, calb_r) + _append_cstr(out, outsz, "(") + _append_cstr(out, outsz, args) + _append_cstr(out, outsz, ")") + if variadic != 0: + # Variadic call: vararg(the callee's full MLIR function type) (includes `...`) + _append_cstr(out, outsz, " vararg(!llvm.func<") + if string.strcmp(rmt, "none") == 0: + # Void-returning variadic function type: write `void`, not `none` + _append_cstr(out, outsz, "void") + else: + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, " (") + _append_cstr(out, outsz, vftys) + # Close three levels in order: the param list `)`, `!llvm.func<`'s `>`, `vararg(`'s `)` + # (omitting vararg's `)` made mlir-opt 22.1.8 report "expected ')'" verbatim) + _append_cstr(out, outsz, ")>)") + _append_cstr(out, outsz, " : ") + if calb_r[0] != '@': + # Indirect call: the callee is a !llvm.ptr operand + # (用解析后的名字判断:别名解析可能把 callee 指到一个 @符号,那就成了直接调用) + _append_cstr(out, outsz, "!llvm.ptr, ") + _append_cstr(out, outsz, "(") + _append_cstr(out, outsz, argts) + _append_cstr(out, outsz, ") -> ") + if string.strcmp(rmt, "none") == 0: + _append_cstr(out, outsz, "()") + else: + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, "\n") + + +def _MlirAsm(p: str, resname: str, rtyb: str, out: str, outsz: sizet): + """Inline asm: llvm.inline_asm [has_side_effects] [asm_dialect = intel] "...", "..." %a, %b : (i32, i32) -> i32 + + Intermediate text (LLVM syntax) has two sources: + builders' build_inline_asm -> `call 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, 512) + p = _mlir_ws(p) + avb: str = _SC_V1 + p = _mlir_read_value(p, avb, 512) + if nops > 0: + _append_cstr(ops, 8192, ", ") + _append_cstr(tys, 1024, ", ") + _MlirOperand(avb, atyb, ops, 2048) + amt: str = _SC_Mt + amt[0] = '\0' + MlirTypeText(amt, 4096, atyb, _MLIR_Pool) + _append_cstr(tys, 1024, amt) + nops += 1 + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + if p[0] == ')': + p = p + 1 + p = _mlir_ws(p) + if p[0] != '\0': + MlirFail("MLIR render: extra token after inline_asm argument list") + rmt: str = _SC_Buf + rmt[0] = '\0' + MlirTypeText(rmt, 1024, rtyb, _MLIR_Pool) + if string.strcmp(rmt, "none") == 0: + _MlirEmitRes(out, outsz, None) + else: + if resname is None: + MlirFail("MLIR render: inline_asm has a return value but no result name (%N = call ...)") + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.inline_asm") + if side != 0: + _append_cstr(out, outsz, " has_side_effects") + if intel != 0: + _append_cstr(out, outsz, " asm_dialect = intel") + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, asmb) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, conb) + if nops > 0: + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, ops) + _append_cstr(out, outsz, " : (") + _append_cstr(out, outsz, tys) + _append_cstr(out, outsz, ") -> ") + if string.strcmp(rmt, "none") == 0: + _append_cstr(out, outsz, "()") + else: + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, "\n") + + +def _MlirCast(p: str, resname: str, ob: str, out: str, outsz: sizet): + """Cast instruction: llvm.zext %v : i32 to i64""" + ftyb: str = _SC_Ty + p = _mlir_read_type(p, ftyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if string.strncmp(p, "to ", 3) == 0: + p = _mlir_ws(p + 3) + ttyb: str = _SC_Ty2 + _mlir_read_type(p, ttyb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.") + _append_cstr(out, outsz, ob) + _append_cstr(out, outsz, " ") + _MlirOperand(vb, ftyb, out, outsz) + fmt: str = _SC_Mt + fmt[0] = '\0' + MlirTypeText(fmt, 4096, ftyb, _MLIR_Pool) + tmt: str = _SC_Tmp2 + tmt[0] = '\0' + MlirTypeText(tmt, 4096, ttyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, fmt) + _append_cstr(out, outsz, " to ") + _append_cstr(out, outsz, tmt) + _append_cstr(out, outsz, "\n") + + +def _MlirBitcast(p: str, resname: str, out: str, outsz: sizet): + """bitcast: ptr->ptr is treated as a no-op (records an alias); otherwise llvm.bitcast %v : T to U""" + ftyb: str = _SC_Ty + p = _mlir_read_type(p, ftyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if string.strncmp(p, "to ", 3) == 0: + p = _mlir_ws(p + 3) + ttyb: str = _SC_Ty2 + _mlir_read_type(p, ttyb, 512) + if MlirTypeTextIsPointer(ftyb) != 0 and MlirTypeTextIsPointer(ttyb) != 0: + # With opaque pointers, a ptr->ptr bitcast is meaningless -> no-op: + # register the result name as an alias of the source operand (later refs auto-rewritten), emit nothing + if resname is None: + MlirFail("MLIR render: no-op bitcast missing result name") + opnd: str = _MlirOperand(vb, ftyb, None, 0) + _MlirAliasAdd(resname, opnd) + return + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.bitcast ") + _MlirOperand(vb, ftyb, out, outsz) + fmt: str = _SC_Mt + fmt[0] = '\0' + MlirTypeText(fmt, 4096, ftyb, _MLIR_Pool) + tmt: str = _SC_Tmp2 + tmt[0] = '\0' + MlirTypeText(tmt, 4096, ttyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, fmt) + _append_cstr(out, outsz, " to ") + _append_cstr(out, outsz, tmt) + _append_cstr(out, outsz, "\n") + + +def _MlirSelect(p: str, resname: str, out: str, outsz: sizet): + """select: llvm.select %c, %a, %b : i1, i32""" + ctyb: str = _SC_Ty + p = _mlir_read_type(p, ctyb, 512) + p = _mlir_ws(p) + cb: str = _SC_V1 + p = _mlir_read_value(p, cb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ttyb: str = _SC_Ty2 + p = _mlir_read_type(p, ttyb, 512) + p = _mlir_ws(p) + tb: str = _SC_V2 + p = _mlir_read_value(p, tb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ftyb: str = _SC_Buf + p = _mlir_read_type(p, ftyb, 512) + p = _mlir_ws(p) + fb: str = _SC_V3 + _mlir_read_value(p, fb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.select ") + _MlirOperand(cb, ctyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(tb, ttyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(fb, ttyb, out, outsz) + cmt: str = _SC_Mt + cmt[0] = '\0' + MlirTypeText(cmt, 4096, ctyb, _MLIR_Pool) + vmt: str = _SC_Tmp2 + vmt[0] = '\0' + MlirTypeText(vmt, 1024, ttyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, cmt) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, vmt) + _append_cstr(out, outsz, "\n") + + +def _MlirGep(p: str, resname: str, out: str, outsz: sizet): + """getelementptr: llvm.getelementptr %p[i0, i1] : (!llvm.ptr, t0, t1) -> !llvm.ptr, base""" + baseb: str = _SC_Ty2 + p = _mlir_read_type(p, baseb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ptyb: str = _SC_Buf + p = _mlir_read_type(p, ptyb, 512) + p = _mlir_ws(p) + pb: str = _SC_V3 + p = _mlir_read_value(p, pb, 512) + idxv: str = _SC_IdxV + idxt: str = _SC_IdxT + idxv[0] = '\0' + idxt[0] = '\0' + p = _mlir_ws(p) + nidx: int = 0 + tmpv: str = _SC_Tmp + tmpv[0] = '\0' + g: int = 0 + while p[0] == ',': + g += 1 + if g > 32: + MlirFail("MLIR render: too many GEP indices") + p = _mlir_ws(p + 1) + ityb: str = _SC_Ty + p = _mlir_read_type(p, ityb, 512) + p = _mlir_ws(p) + ivb: str = _SC_V1 + p = _mlir_read_value(p, ivb, 512) + # Constant indices must be written as integer literals (e.g. `[0, 1]`), not materialized + # as SSA operands: MLIR's llvm.getelementptr requires struct-field indices to be constant, + # else the verifier errors 'expected index N indexing a struct to be constant' + # (operand form only populates rawConstantIndices' dynamic slot). Only non-constant (SSA, + # starting with '%') indices are materialized and take a type slot; constants do not. + if nidx > 0: + _append_cstr(idxv, 1024, ", ") + if ivb[0] == '%': + _MlirOperand(ivb, ityb, tmpv, 4096) + _append_cstr(idxv, 1024, tmpv) + tmpv[0] = '\0' + imt: str = _SC_Mt + imt[0] = '\0' + MlirTypeText(imt, 4096, ityb, _MLIR_Pool) + if idxt[0] != '\0': + _append_cstr(idxt, 4096, ", ") + _append_cstr(idxt, 4096, imt) + else: + _append_cstr(idxv, 1024, ivb) + nidx += 1 + p = _mlir_ws(p) + if nidx == 0: + MlirFail("MLIR render: GEP missing index") + bmt: str = _SC_Tmp2 + bmt[0] = '\0' + MlirTypeText(bmt, 4096, baseb, _MLIR_Pool) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.getelementptr ") + _MlirOperand(pb, ptyb, out, outsz) + _append_cstr(out, outsz, "[") + _append_cstr(out, outsz, idxv) + _append_cstr(out, outsz, "] : (!llvm.ptr") + # One type per index + _MlirAppendCommaTypes(out, outsz, idxt) + _append_cstr(out, outsz, ") -> !llvm.ptr, ") + _append_cstr(out, outsz, bmt) + _append_cstr(out, outsz, "\n") + + +def _MlirAppendCommaTypes(out: str, outsz: sizet, tylist: str): + """Append ", t0, t1" to out (tylist is a comma-separated type string)""" + if tylist is None or tylist[0] == '\0': + return + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, tylist) + + +def _MlirSwitch(p: str, out: str, outsz: sizet): + """switch: llvm.switch %v : i32, ^default [0: ^case0, 1: ^case1] + + The form was determined by measuring mlir-opt 22.1.8 (the design-doc form + `llvm.switch %v, ^default [0: ^case0] : i32` is REJECTED: "expected ':'"): + `llvm.switch %v : i32, ^default [0: ^c0, 1: ^c1]` -> exit 0 (adopted) + `llvm.switch %v : i32, ^default(%r : i32) [ ... ]` -> exit 0 (default block args) + `llvm.switch %v : i32, ^default [0: ^c0(%r : i32)]` -> exit 0 (case block args) + `llvm.switch %v : i64, ^default [4294967296: ^c0]` -> exit 0 (case value follows value type) + `llvm.switch %v : i32, ^default [-1: ^c0]` -> exit 0 + `llvm.switch %v : i32, ^default []` -> exit 0 (empty case table) + i.e. the value type follows the value (`%v : i32,`); block args are written exactly like + br/cond_br, so _MlirBlockOperands' phi->block-arg facility is reused as-is => target blocks may + carry phis (a missing incoming edge errors loudly, never silently dropping args). + Case values must still be constants: in MLIR they are attributes (case_values), not SSA values. + """ + tyb: str = _SC_Ty + p = _mlir_read_type(p, tyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + if string.strncmp(p, "label ", 6) == 0: + p = _mlir_ws(p + 6) + db: str = _SC_V2 + p = _mlir_read_value(p, db, 512) + p = _mlir_ws(p) + if p[0] == '[': + p = p + 1 + # Materialize the value operand and its MLIR type text first: _MlirBlockOperands inside the + # case loop reuses _SC_Ty/_SC_Buf/_SC_P1/_SC_P2 scratch, so capture into a separate buffer first. + vop: str = _SC_IdxT + vop[0] = '\0' + _MlirOperand(vb, tyb, vop, 1024) + vmt: str = _SC_IdxV + vmt[0] = '\0' + MlirTypeText(vmt, 1024, tyb, _MLIR_Pool) + cases: str = _SC_Tmp2 + cases[0] = '\0' + ncase: int = 0 + g: int = 0 + while p[0] != '\0': + g += 1 + if g > 1024: + MlirFail("MLIR render: too many switch branches") + p = _mlir_ws(p) + if p[0] == ']' or p[0] == '\0': + break + ctb: str = _SC_P1 + p = _mlir_read_type(p, ctb, 512) + p = _mlir_ws(p) + cvb: str = _SC_P2 + p = _mlir_read_value(p, cvb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + if string.strncmp(p, "label ", 6) == 0: + p = _mlir_ws(p + 6) + cb: str = _SC_Ty2 + p = _mlir_read_value(p, cb, 512) + if cvb[0] == '%': + MlirFail("MLIR render: switch case value must be a constant (SSA value not supported)") + cbr: str = _MlirBlkRef(cb) + if ncase > 0: + _append_cstr(cases, 4096, ", ") + _append_cstr(cases, 4096, cvb) + _append_cstr(cases, 4096, ": ^") + _append_cstr(cases, 4096, cbr) + # Target block has phis => append block arguments (same facility as br/cond_br) + _MlirBlockOperands(cases, 1024, _MlirFindBlock(_MLIR_Func, cbr), _MLIR_SrcBlk) + ncase += 1 + p = _mlir_ws(p) + if p[0] == ',': + p = p + 1 + dr: str = _MlirBlkRef(db) + _MlirEmitRes(out, outsz, None) + _append_cstr(out, outsz, "llvm.switch ") + _append_cstr(out, outsz, vop) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, vmt) + _append_cstr(out, outsz, ", ^") + _append_cstr(out, outsz, dr) + _MlirBlockOperands(out, outsz, _MlirFindBlock(_MLIR_Func, dr), _MLIR_SrcBlk) + _append_cstr(out, outsz, " [") + _append_cstr(out, outsz, cases) + _append_cstr(out, outsz, "]\n") + + +def _MlirAtomicRmw(p: str, resname: str, out: str, outsz: sizet): + """atomicrmw: llvm.atomicrmw add %p, %v monotonic : !llvm.ptr, i32""" + aop: str = _SC_P1 + p = _mlir_read_value(p, aop, 512) + p = _mlir_ws(p) + ptyb: str = _SC_Ty + p = _mlir_read_type(p, ptyb, 512) + p = _mlir_ws(p) + pb: str = _SC_V1 + p = _mlir_read_value(p, pb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + vtyb: str = _SC_Ty2 + p = _mlir_read_type(p, vtyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V2 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + ordb: str = _SC_P2 + _mlir_read_value(p, ordb, 512) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.atomicrmw ") + _append_cstr(out, outsz, aop) + _append_cstr(out, outsz, " ") + _MlirOperand(pb, ptyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(vb, vtyb, out, outsz) + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, ordb) + pmt: str = _SC_Mt + pmt[0] = '\0' + MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) + vmt: str = _SC_Tmp2 + vmt[0] = '\0' + MlirTypeText(vmt, 1024, vtyb, _MLIR_Pool) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, pmt) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, vmt) + _append_cstr(out, outsz, "\n") + + +def _MlirCmpXchg(p: str, resname: str, resname2: str, + out: str, outsz: sizet): + """cmpxchg: MLIR single result struct{T, i1} + two extractvalues for old value / success bit. + + Intermediate text (LLVM syntax, two forms: builder single result / two-result prefix) + %N = cmpxchg , , + %a, %b = cmpxchg ... + MLIR form (mlir-opt 22.1.8 measured exit 0; cmpxchg is a single result in MLIR, yielding + struct{T, i1}, not two SSA values): + %N = llvm.cmpxchg %p, %c, %n acq_rel monotonic : !llvm.ptr, i32 + %_N_v = llvm.extractvalue %N[0] : !llvm.struct<(i32, i1)> ; old value (compared value) + %_N_ok = llvm.extractvalue %N[1] : !llvm.struct<(i32, i1)> ; i1 success bit + The two extractvalue result names are deterministically derived from the struct result name + (_MlirCxResName, e.g. `%0` -> `%_0_v`/`%_0_ok`). + Bindings: `%N` (single result) / `%a` (two-result first name) -> old value `[0]`; + two-result second name `%b` -> success bit `[1]`. Bindings use the same alias facility as + bitcast no-op (_MlirAliasAdd); later references are auto-rewritten by _MlirResolve to the + extractvalue result names. This preserves builder-side build_cmpxchg's "T-typed Value" (old value) semantics. + """ + ptyb: str = _SC_Ty + p = _mlir_read_type(p, ptyb, 512) + p = _mlir_ws(p) + pb: str = _SC_V1 + p = _mlir_read_value(p, pb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ctyb: str = _SC_Ty2 + p = _mlir_read_type(p, ctyb, 512) + p = _mlir_ws(p) + cb: str = _SC_V2 + p = _mlir_read_value(p, cb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + ntyb: str = _SC_B1 + p = _mlir_read_type(p, ntyb, 512) + p = _mlir_ws(p) + nb: str = _SC_B2 + p = _mlir_read_value(p, nb, 512) + p = _mlir_ws(p) + succ: str = _SC_P1 + p = _mlir_read_value(p, succ, 512) + p = _mlir_ws(p) + fail: str = _SC_P2 + _mlir_read_value(p, fail, 512) + if resname is None: + MlirFail("MLIR render: cmpxchg missing result name") + # The two extractvalue result names derive deterministically from the struct name (identical across both passes) + onm: str = _MlirCxResName(resname, "_v") + snm: str = _MlirCxResName(resname, "_ok") + _MlirAliasAdd(resname, onm) + if resname2 is not None: + _MlirAliasAdd(resname2, snm) + # MLIR type text (use a non-_SC_Mt buffer: _MlirOperand occupies _SC_Mt for literals) + pmt: str = _SC_Tmp2 + pmt[0] = '\0' + MlirTypeText(pmt, 4096, ptyb, _MLIR_Pool) + vmt: str = _SC_IdxV + vmt[0] = '\0' + MlirTypeText(vmt, 1024, ctyb, _MLIR_Pool) + stmt: str = _SC_IdxT + stmt[0] = '\0' + _append_cstr(stmt, 4096, "!llvm.struct<(") + _append_cstr(stmt, 4096, vmt) + _append_cstr(stmt, 4096, ", i1)>") + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.cmpxchg ") + _MlirOperand(pb, ptyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(cb, ctyb, out, outsz) + _append_cstr(out, outsz, ", ") + _MlirOperand(nb, ntyb, out, outsz) + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, succ) + _append_cstr(out, outsz, " ") + _append_cstr(out, outsz, fail) + _append_cstr(out, outsz, " : ") + _append_cstr(out, outsz, pmt) + _append_cstr(out, outsz, ", ") + _append_cstr(out, outsz, vmt) + _append_cstr(out, outsz, "\n") + _MlirEmitRes(out, outsz, onm) + _append_cstr(out, outsz, "llvm.extractvalue ") + _append_cstr(out, outsz, resname) + _append_cstr(out, outsz, "[0] : ") + _append_cstr(out, outsz, stmt) + _append_cstr(out, outsz, "\n") + _MlirEmitRes(out, outsz, snm) + _append_cstr(out, outsz, "llvm.extractvalue ") + _append_cstr(out, outsz, resname) + _append_cstr(out, outsz, "[1] : ") + _append_cstr(out, outsz, stmt) + _append_cstr(out, outsz, "\n") + + +def _MlirVaArg(p: str, resname: str, out: str, outsz: sizet): + """va_arg: use the generic op syntax (deliberate choice, see below). + + Intermediate text (LLVM syntax): %N = va_arg , + MLIR text: %N = "llvm.va_arg"(%ap) : (!llvm.ptr) -> i32 + + Why generic form is required (mlir-opt 22.1.8 measured): the custom assembly syntax + `llvm.va_arg %ap, i32` requires an explicit va_list type (in LLVM it is `ptr`), which cannot + be expressed with opaque pointers -- mlir-opt errors "expected builtin.function". So the + generic op form is used, described only by operand/result types. Down-level verification + (mlir-translate --mlir-to-llvmir) yields `%2 = va_arg ptr %0, i32`, isomorphic to LLVM's va_arg. + """ + vtyb: str = _SC_Ty + p = _mlir_read_type(p, vtyb, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + p = _mlir_ws(p) + if p[0] == ',': + p = _mlir_ws(p + 1) + atyb: str = _SC_Ty2 + _mlir_read_type(p, atyb, 512) + if resname is None: + MlirFail("MLIR render: va_arg missing result name") + # Operand/result types (_MlirOperand only uses _SC_Mt, so take separate buffers here) + omt: str = _SC_Tmp2 + omt[0] = '\0' + MlirTypeText(omt, 4096, vtyb, _MLIR_Pool) + rmt: str = _SC_IdxV + rmt[0] = '\0' + MlirTypeText(rmt, 1024, atyb, _MLIR_Pool) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "\"llvm.va_arg\"(") + _MlirOperand(vb, vtyb, out, outsz) + _append_cstr(out, outsz, ") : (") + _append_cstr(out, outsz, omt) + _append_cstr(out, outsz, ") -> ") + _append_cstr(out, outsz, rmt) + _append_cstr(out, outsz, "\n") + + +def _MlirExtractValue(p: str, resname: str, out: str, outsz: sizet): + """extractvalue: llvm.extractvalue %v[i0, i1] : + + 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, 512) + p = _mlir_ws(p) + vb: str = _SC_V1 + p = _mlir_read_value(p, vb, 512) + idxs: str = _SC_IdxV + idxs[0] = '\0' + nidx: int = 0 + g: int = 0 + # Index list: comma-separated in LLVM syntax (`, i0, i1`); read each and rewrite. + # Loop bound matches other parse loops in this file (bounded; exceeding errors loudly). + while g < 64: + p = _mlir_ws(p) + if p[0] != ',': + break + g += 1 + if g > 32: + MlirFail("MLIR render: too many extractvalue indices") + p = _mlir_ws(p + 1) + ib: str = _SC_IdxT + p = _mlir_read_value(p, ib, 512) + if ib[0] == '\0': + MlirFail("MLIR render: extractvalue missing index") + if ib[0] == '%': + MlirFail("MLIR render: extractvalue index must be a constant (SSA index not supported)") + if nidx > 0: + _append_cstr(idxs, 1024, ", ") + _append_cstr(idxs, 1024, ib) + nidx += 1 + if nidx == 0: + MlirFail("MLIR render: extractvalue missing index") + if p[0] != '\0': + MlirFail("MLIR render: extra token after extractvalue index") + # Aggregate type text goes into a separate buffer first (_MlirOperand only uses _SC_Mt) + amt: str = _SC_Tmp2 + amt[0] = '\0' + MlirTypeText(amt, 4096, atyb, _MLIR_Pool) + _MlirEmitRes(out, outsz, resname) + _append_cstr(out, outsz, "llvm.extractvalue ") + _MlirOperand(vb, atyb, out, outsz) + _append_cstr(out, outsz, "[") + _append_cstr(out, outsz, idxs) + _append_cstr(out, outsz, "] : ") + _append_cstr(out, outsz, amt) + _append_cstr(out, outsz, "\n") + + +# ============================================================ +# Instruction line -> MLIR dispatch +# ============================================================ +def _MlirRenderLine(text: str, out: str, outsz: sizet): + """Parse one intermediate instruction text and render it to MLIR (out=None: register/validate only)""" + global _MLIR_Pass, _MLIR_CurLine + if text is None: + return + # Stash the "current line" at entry: printed on type-text conversion failure (both passes + # use the same line, so pass1 failures are also locatable). text points to Line.Buf, live for the whole render. + _MLIR_CurLine = text + MlirSetContextLine(_MLIR_CurLine) + p: str = _mlir_ws(text) + if p[0] == '\0': + return + resname: str = None + resname2: str = None + if p[0] == '%': + tb: str = _SC_Tb + q: str = _mlir_read_value(p, tb, 512) + q2: str = _mlir_ws(q) + if q2[0] == '=': + resname = _mlir_dup(tb) + p = _mlir_ws(q2 + 1) + elif q2[0] == ',': + # Two-result prefix `%a, %b = ` (cmpxchg only): MLIR's cmpxchg is a single + # struct{T, i1} result; the two names each bind to one extractvalue (see _MlirCmpXchg). + resname = _mlir_dup(tb) + q3: str = _mlir_ws(q2 + 1) + tb2: str = _SC_V3 + q3 = _mlir_read_value(q3, tb2, 512) + q4: str = _mlir_ws(q3) + if q4[0] != '=': + MlirFail("MLIR render: double-result prefix (%a, %b =) missing '='") + resname2 = _mlir_dup(tb2) + p = _mlir_ws(q4 + 1) + ob: str = _SC_Op + p = _mlir_read_value(p, ob, 64) + if ob[0] == '\0': + MlirFail("MLIR render: empty instruction") + if resname2 is not None and string.strcmp(ob, "cmpxchg") != 0: + MlirFail("MLIR render: double-result prefix (%a, %b =) only supports cmpxchg") + if _mlir_in_set(ob, "|add|sub|mul|sdiv|udiv|srem|urem|and|or|xor|shl|lshr|ashr|") != 0: + _MlirBinop(p, resname, ob, out, outsz) + return + if _mlir_in_set(ob, "|fadd|fsub|fmul|fdiv|frem|") != 0: + _MlirBinop(p, resname, ob, out, outsz) + return + if string.strcmp(ob, "fneg") == 0: + _MlirFneg(p, resname, out, outsz) + return + if string.strcmp(ob, "icmp") == 0: + _MlirCmp(p, resname, ob, out, outsz) + return + if string.strcmp(ob, "fcmp") == 0: + _MlirCmp(p, resname, ob, out, outsz) + return + if string.strcmp(ob, "alloca") == 0: + _MlirAlloca(p, resname, out, outsz) + return + if string.strcmp(ob, "load") == 0: + _MlirLoad(p, resname, out, outsz) + return + if string.strcmp(ob, "store") == 0: + _MlirStore(p, out, outsz) + return + if string.strcmp(ob, "br") == 0: + _MlirBr(p, out, outsz) + return + if string.strcmp(ob, "ret") == 0: + _MlirRet(p, out, outsz) + return + if string.strcmp(ob, "unreachable") == 0: + _MlirUnreachable(out, outsz) + return + if string.strcmp(ob, "call") == 0: + _MlirCall(p, resname, out, outsz) + return + if string.strcmp(ob, "select") == 0: + _MlirSelect(p, resname, out, outsz) + return + if string.strcmp(ob, "phi") == 0: + if out is not None: + # Leading phis are skipped in _MlirEmitBlockLines; reaching here means phi is not at block head + MlirFail("MLIR render: phi is not at block head (v1 only supports leading consecutive phis -> block arguments)") + _MlirPhiScan(text) + return + if string.strcmp(ob, "getelementptr") == 0: + _MlirGep(p, resname, out, outsz) + return + if string.strcmp(ob, "bitcast") == 0: + _MlirBitcast(p, resname, out, outsz) + return + if _mlir_in_set(ob, "|sext|trunc|zext|ptrtoint|inttoptr|fpext|fptrunc|fptosi|sitofp|fptoui|uitofp|") != 0: + _MlirCast(p, resname, ob, out, outsz) + return + if string.strcmp(ob, "switch") == 0: + _MlirSwitch(p, out, outsz) + return + if string.strcmp(ob, "atomicrmw") == 0: + _MlirAtomicRmw(p, resname, out, outsz) + return + if string.strcmp(ob, "cmpxchg") == 0: + _MlirCmpXchg(p, resname, resname2, out, outsz) + return + if string.strcmp(ob, "va_arg") == 0: + _MlirVaArg(p, resname, out, outsz) + return + if string.strcmp(ob, "extractvalue") == 0: + # Real source: VPC's multi-result inline-asm result extraction (see _MlirExtractValue). + # Note: other unsupported opcodes like `insertvalue` never occur in the corpus and fall + # through to the loud fallback error ("unknown instruction opcode"). + _MlirExtractValue(p, resname, out, outsz) + return + # Note: inline asm's opcode in intermediate text is "call" (call asm ...); _MlirCall + # recognizes it and forwards to _MlirAsm (llvm.inline_asm). + MlirFail("MLIR render: unknown instruction opcode") + + +# ============================================================ +# Function signature and body +# ============================================================ +def _MlirFuncSig(buf: str, size: sizet, + func: Function*, pool: memhub.MemBuddy*): + """llvm.func @name(%a: i32, %b: i32) -> i32 (void return omits -> ty)""" + _append_cstr(buf, size, "llvm.func @") + if func.Name is not None: + if _ll_name_needs_quote(func.Name) != 0: + _append_cstr(buf, size, "\"") + _append_cstr(buf, size, func.Name) + _append_cstr(buf, size, "\"") + else: + _append_cstr(buf, size, func.Name) + _append_cstr(buf, size, "(") + params: GSList[Param]* = func.Params + cur: Param* = None + if params is not None: + cur = params.Head + first: int = 1 + cur_steps: int = 0 + while cur is not None: + cur_steps += 1 + if cur_steps > IR_DUMP_CHAIN_LIMIT: + MlirFail("MLIR render: parameter chain may be cyclic") + if first == 0: + _append_cstr(buf, size, ", ") + if cur.Name is not None: + if cur.Name[0] != '%': + _append_cstr(buf, size, "%") + _append_cstr(buf, size, cur.Name) + _append_cstr(buf, size, ": ") + ty_buf: str = _SC_Ty + ty_buf[0] = '\0' + TypePrint(ty_buf, 4096, cur.Ty, pool) + _append_cstr(buf, size, ty_buf) + cur = cur.Next + first = 0 + # Variadic function: append `...` at the end of the parameter list (mlir-opt 22.1.8 measured exit 0: + # `llvm.func @f(%p: !llvm.ptr, ...) -> i32` / with no fixed params `llvm.func @f(...) -> i32`) + if func.IsVarArg == 1: + if params is not None and params.Head is not None: + _append_cstr(buf, size, ", ...") + else: + _append_cstr(buf, size, "...") + _append_cstr(buf, size, ")") + # Return type (Void omitted) + is_void: int = 0 + match func.RetTy: + case LLVMType.Void(): + is_void = 1 + case _: + is_void = 0 + if is_void == 0: + ret_buf: str = _SC_Ty2 + ret_buf[0] = '\0' + TypePrint(ret_buf, 4096, func.RetTy, pool) + _append_cstr(buf, size, " -> ") + _append_cstr(buf, size, ret_buf) + + +def FunctionPrint(buf: str, size: sizet, + func: Function*, pool: memhub.MemBuddy*): + """Serialize function func as MLIR (llvm dialect) text APPENDED to buf. + + NOTE: like the llvmlite version, this appends (so the module printer can concatenate + functions); callers wanting single-function text must set buf[0]='\\0' themselves. + """ + global _MLIR_Pass, _MLIR_Func, _MLIR_SrcBlk, _MLIR_CurLine, _MlirOutTrunc + if func is None or buf is None or size == 0: + return + _MlirOutTrunc = 0 + + # Local peephole first (ret zeroinitializer->0/null, entry-slot store->load forwarding, + # no-op bitcast removal): peepholes match by name and delete lines by setting Buf=None. + # They act on the builders' intermediate instruction text and are orthogonal to MLIR rendering; + # RenumberSSA internally skips declare functions (IsDeclared==1). + # Run at most ONCE per body: the module is printed several times (STUB/TEXT/FULL, see PrintModule) + # and the T2 forwarding is NOT idempotent — a second pass over the already-optimized text can + # drop an entry alloca whose use was only reachable through a line the first pass had removed, + # leaving dangling SSA names (mlir-opt: "op using value defined outside the region"). + if func.IsDeclared == 0 and G_PeepEnable != 0 and func.PeepDone == 0: + _peephole_opt(func, pool) + func.PeepDone = 1 + RenumberSSA(func, pool) + + _MlirTablesInit(pool) + _MLIR_Func = func + # Record the function's own SSA names of the form c0/c1/... so the constant namer never picks a + # colliding %c (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 diff --git a/mlirlite/__init__.vp b/mlirlite/__init__.vp index 27fc8f7..7dc9899 100644 --- a/mlirlite/__init__.vp +++ b/mlirlite/__init__.vp @@ -1,462 +1,462 @@ -from stdint import * -import memhub -import string -import viperlib - -# ============================================================ -# mlirlite -- an MLIR (llvm dialect) text emission library, isomorphic to llvmlite name for name -# -# Design principles: -# * The internal model (LLVMType/Value/Function/BasicBlock/IRBuilder/Module...) is -# **isomorphic to includes/llvmlite name for name and signature for signature**, so on the TPV side it suffices to rename -# `llvmlite.` to `mlirlite.` in order to compile; -# * The difference lies only in the "printing" stage: TypePrint / ValuePrint / FunctionPrint / -# LLVMModulePrint emit MLIR text, while the builders still generate the intermediate instruction text as before; -# * Names keep the LLVMxxx form (isomorphism first). See README.md for semantic differences. -# ============================================================ - -# Import submodules -from .__types import ( - LLVMType, ParamNode, LLVMTypeCore, - Int1, Int8, Int16, Int32, Int64, Ptr, Func, Void, Array, Struct, - Half, Float, Double, FP128, Label, - new_param_node, param_list_append, - paramnode_get_ty, paramnode_get_next, paramnode_set_ty, paramnode_set_next, - TypePrint, PrintStructDefinition, get_struct_name, - get_struct_fields, get_struct_fcount, DeepCopyType, -) -from .__values import ( - Value, new_value, ConstInt, ConstNull, ConstZero, ConstFloat, SSAValue, ValuePrint, - value_get_ty, value_get_next, - value_set_ty, value_set_next, value_set_name, value_set_isconst, -) -from .__function import ( - Line, BasicBlock, Param, Function, - new_line, new_basic_block, new_param, new_function, - function_add_param, function_add_block, - block_append_line, block_append_text, - function_set_attrs, param_set_attrs, - function_set_attr_group, - function_set_dbg, - function_get_name, function_get_name_len, function_get_ret_ty, function_get_params, - function_get_param_head, function_get_next, function_is_declared, - param_get_name, param_get_ty, param_get_next, - FunctionPrint, G_PeepEnable, - DebugSetEnabled, DebugIsEnabled, DebugNoteFunc, DebugClearFunc, - DebugNoteLine, DebugResetFunc, DebugTakeText, -) -from .__module import ( - LLVMModule, GlobalVariable, NamedTypeNode, DINode, DINamedMeta, - new_module, new_global_variable, - module_add_function, module_add_global, module_add_named_type, - module_set_named_type_packed, - module_set_target, module_set_datalayout, - module_add_di_node, module_add_named_metadata, - global_set_constant, global_set_linkage, global_set_unnamed_addr, global_set_section, - LLVMModulePrint, OUTPUT_FULL, OUTPUT_STUB, OUTPUT_TEXT, - module_ensure_opaque_for_type, -) -from .__builder import ( - IRBuilder, new_builder, position_at_end, - builder_cur_block_is_terminated, - builder_set_debug_location, builder_clear_debug_location, - build_alloca, build_alloca_aligned, build_alloca_at_entry, build_load, build_store, - build_store_at_entry, - build_add, build_sub, build_mul, build_sdiv, build_udiv, build_srem, build_urem, - build_and, build_or, build_xor, build_shl, build_lshr, build_ashr, - build_fadd, build_fsub, build_fmul, build_fdiv, build_frem, build_fneg, - build_icmp, build_fcmp, - build_br, build_cond_br, build_ret, build_ret_void, - build_call, build_call_indirect, build_bitcast, build_sext, build_trunc, build_zext, - build_ptrtoint, build_inttoptr, - build_fpext, build_fptrunc, build_fp2si, build_si2fp, build_fp2ui, build_ui2fp, - build_gep, build_gep_array, build_gep_struct, - build_phi, build_switch, build_select, build_unreachable, - build_atomicrmw, build_cmpxchg, build_inline_asm, build_va_arg, - PhiIncoming, new_phi_incoming, SwitchCase, new_switch_case, - ICMP_EQ, ICMP_NE, ICMP_SGT, ICMP_SGE, ICMP_SLT, ICMP_SLE, - ICMP_UGT, ICMP_UGE, ICMP_ULT, ICMP_ULE, - FCMP_FALSE, FCMP_OEQ, FCMP_OGT, FCMP_OGE, FCMP_OLT, FCMP_OLE, - FCMP_ONE, FCMP_ORD, FCMP_UNO, FCMP_UEQ, FCMP_UGT, FCMP_UGE, - FCMP_ULT, FCMP_ULE, FCMP_UNE, FCMP_TRUE, - ATOMIC_XCHG, ATOMIC_ADD, ATOMIC_SUB, ATOMIC_AND, ATOMIC_NAND, - ATOMIC_OR, ATOMIC_XOR, ATOMIC_MAX, ATOMIC_MIN, ATOMIC_UMAX, ATOMIC_UMIN, - ATOMIC_ORDER_NOTATOMIC, ATOMIC_ORDER_UNORDERED, ATOMIC_ORDER_MONOTONIC, - ATOMIC_ORDER_ACQUIRE, ATOMIC_ORDER_RELEASE, ATOMIC_ORDER_ACQ_REL, - ATOMIC_ORDER_SEQ_CST, -) -from .__verify import ( - VerifyResult, NameEntry, - verify_function, verify_module, - VERIFY_OK, VERIFY_ERR_NO_TERMINATOR, VERIFY_ERR_DUPLICATE_DEF, - VERIFY_ERR_UNDEFINED_USE, VERIFY_ERR_PHI_NOT_FIRST, - _new_verify_result, -) - - -# ============================================================ -# Convenience factory: string copy -# ============================================================ -def _str_dup(pool: memhub.MemBuddy*, src: str) -> str: - """Allocate from pool and copy a C string.""" - if src is None: return None - slen: sizet = string.strlen(src) - buf: str = pool.alloc(slen + 1) - if buf is None: return None - string.strcpy(buf, src) - return buf - - -# ============================================================ -# Convenience factory: create a named basic block -# ============================================================ -def create_block(pool: memhub.MemBuddy*, func: Function*, - name: str) -> BasicBlock*: - """Create a basic block and add it to the function.""" - # Block-name/param conflict guard: block label "entry:" is value %entry, so a param %entry - # collides; on collision append .1/.2/... (strip a leading % from Param.Name before comparing). - use_name: str = name - if func is not None and func.Params is not None and name is not None \ - and name[0] != '\0': - cb_len: sizet = string.strlen(name) + 16 - cb_buf: str = pool.alloc(cb_len) - if cb_buf is not None: - cb_k: int = 1 - cb_collide: int = 1 - while cb_collide != 0: - cb_collide = 0 - cb_p: Param* = function_get_param_head(func) - while cb_p is not None: - cb_pnm: str = param_get_name(cb_p) - if cb_pnm is not None: - cb_pv: str = cb_pnm - if cb_pv[0] == '%': - cb_pv = cb_pv + 1 - if string.strcmp(cb_pv, use_name) == 0: - cb_collide = 1 - break - cb_p = param_get_next(cb_p) - if cb_collide != 0: - viperlib.snprintf(cb_buf, cb_len, "%s.%d", name, cb_k) - use_name = cb_buf - cb_k += 1 - name_dup: str = _str_dup(pool, use_name) - blk: BasicBlock* = new_basic_block(pool, name_dup) - if blk is not None: - function_add_block(func, blk) - return blk - - -# ============================================================ -# Convenience factory: create an integer constant value -# ============================================================ -def const_int(pool: memhub.MemBuddy*, bits: int, - val: int64) -> Value*: - """Create an integer constant value (auto-creates type + name text). - bits: bit width (1/8/16/32/64); val: integer value. - """ - # create type - ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if ty is None: return None - string.memset(ty, 0, LLVMType.__sizeof__()) - *ty = *LLVMType.Int(bits) - - # create name text - name: str = pool.alloc(32) - if name is None: return None - # LLVM i64 literals exclude INT64_MIN: represent it as the unsigned bit pattern - # 9223372036854775808 (accepted by LLVM), else opt rejects "-9223372036854775808". - if bits == 64 and val == -9223372036854775807 - 1: - viperlib.snprintf(name, 32, "9223372036854775808") - else: - viperlib.snprintf(name, 32, "%lld", val) - - return ConstInt(pool, ty, val, name) - - -def const_int32(pool: memhub.MemBuddy*, val: int64) -> Value*: - """Convenience: create an i32 integer constant.""" - return const_int(pool, 32, val) - - -def const_int64(pool: memhub.MemBuddy*, val: int64) -> Value*: - """Convenience: create an i64 integer constant.""" - return const_int(pool, 64, val) - - -# ============================================================ -# Convenience factory: create a function and add it to the module -# ============================================================ -def create_function(pool: memhub.MemBuddy*, mod: LLVMModule*, - name: str, ret_ty: LLVMType*) -> Function*: - """Create a function and add it to the module.""" - name_dup: str = _str_dup(pool, name) - func: Function* = new_function(pool, name_dup, ret_ty) - if func is not None: - module_add_function(mod, func) - return func - - -def add_param(pool: memhub.MemBuddy*, func: Function*, - ty: LLVMType*, name: str) -> Param*: - """Create a parameter and add it to the function.""" - name_dup: str = _str_dup(pool, name) - param: Param* = new_param(pool, ty, name_dup) - if param is not None: - function_add_param(func, param) - return param - - -# ============================================================ -# Convenience factory: create a function declaration (declare) -# ============================================================ -def create_declare(pool: memhub.MemBuddy*, mod: LLVMModule*, - name: str, ret_ty: LLVMType*) -> Function*: - """Create a function declaration (declare, no body) and add it to the module. - Unlike create_function, it emits only: declare @() - """ - name_dup: str = _str_dup(pool, name) - func: Function* = new_function(pool, name_dup, ret_ty) - if func is not None: - func.IsDeclared = 1 - module_add_function(mod, func) - return func - - -# ============================================================ -# Convenience factory: create a string-constant global variable -# -# The initializer is stored in gv.Initializer as llvmlite-shaped LLVM syntax text (c"contents\00"); -# the MLIR render stage (__module.vp/_MlirGlobalInit) converts it to the attribute form "contents". -# ============================================================ -def create_global_string(pool: memhub.MemBuddy*, mod: LLVMModule*, - name: str, text: str) -> GlobalVariable*: - """Create a string-constant global variable and add it to the module. - name: global name (e.g. ".str"); text: C string without trailing NUL (added automatically). - Returns: GlobalVariable pointer of type [len+1 x i8]. - """ - if text is None: return None - slen: sizet = string.strlen(text) - # 显式收窄:LLVMType.Array 的 Count 是 int(i32),而 slen 是 sizet(u64); - # 这里按显式转换(非隐式窄化)取 i32(字符串长度远超 2^31 不现实)。 - count: int = int(slen + 1) # includes the trailing NUL - - # create i8 type - i8_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if i8_ty is None: return None - string.memset(i8_ty, 0, LLVMType.__sizeof__()) - *i8_ty = *LLVMType.Int(8) - - # create [count x i8] array type - arr_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if arr_ty is None: return None - string.memset(arr_ty, 0, LLVMType.__sizeof__()) - *arr_ty = *LLVMType.Array(i8_ty, count) - - # create global variable - name_dup: str = _str_dup(pool, name) - gv: GlobalVariable* = new_global_variable(pool, name_dup, arr_ty) - if gv is None: return None - - # set attributes: private + unnamed_addr + constant - global_set_linkage(gv, "private") - global_set_unnamed_addr(gv, 1) - global_set_constant(gv, 1) - - # Build initializer text c"contents\00": LLVM IR string literal c"" with " \ and - # non-printables escaped as \xx; buffer sized for worst case (4 bytes/char) + c"..." + \00 + " - init_buf: str = pool.alloc(slen * 4 + 8) - if init_buf is None: return None - pos: sizet = 0 - init_buf[pos] = 'c' - pos += 1 - init_buf[pos] = '"' - pos += 1 - for i in range(int(slen)): - ch: uint8 = text[i] - if ch == 0x22 or ch == 0x5C or ch < 0x20 or ch >= 0x7F: - # escape: " -> \22, \ -> \5c, non-printable -> \xx - init_buf[pos] = '\\' - pos += 1 - hi: int = (ch >> 4) & 0x0F - lo: int = ch & 0x0F - if hi < 10: - init_buf[pos] = char(0x30 + hi) - else: - init_buf[pos] = char(0x61 + hi - 10) - pos += 1 - if lo < 10: - init_buf[pos] = char(0x30 + lo) - else: - init_buf[pos] = char(0x61 + lo - 10) - pos += 1 - else: - init_buf[pos] = ch - pos += 1 - # trailing NUL byte - init_buf[pos] = '\\' - pos += 1 - init_buf[pos] = '0' - pos += 1 - init_buf[pos] = '0' - pos += 1 - init_buf[pos] = '"' - pos += 1 - init_buf[pos] = '\0' - gv.Initializer = init_buf - - module_add_global(mod, gv) - return gv - - -# ============================================================ -# Convenience factory: create an integer global with an initializer -# ============================================================ -def create_global_int(pool: memhub.MemBuddy*, mod: LLVMModule*, - name: str, bits: int, val: int64) -> GlobalVariable*: - """Create an integer global variable and add it to the module. - MLIR output: llvm.mlir.global @( : i) : i - """ - # create integer type - ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) - if ty is None: return None - string.memset(ty, 0, LLVMType.__sizeof__()) - *ty = *LLVMType.Int(bits) - - name_dup: str = _str_dup(pool, name) - gv: GlobalVariable* = new_global_variable(pool, name_dup, ty) - if gv is None: return None - - # build initializer text "42" (the type is printed separately by the global render stage) - init_buf: str = pool.alloc(48) - if init_buf is None: return None - viperlib.snprintf(init_buf, 48, "%lld", val) - gv.Initializer = init_buf - - module_add_global(mod, gv) - return gv - - -# ============================================================ -# LLVMModuleCore: module/function/value creation core; wraps pool to avoid passing it repeatedly -# -# Usage: -# mcore: rawptr[LLVMModuleCore] = LLVMModuleCore(pool) -# mod: rawptr[LLVMModule] = mcore.NewModule("test") -# func: rawptr[Function] = mcore.CreateFunction(mod, "add", ty_i32) -# val: rawptr[Value] = mcore.ConstInt32(42) -# ============================================================ -novirtual class LLVMModuleCore: - """Module/function/value creation core; wraps pool so callers need not pass it.""" - Pool: memhub.MemBuddy* - - def __init__(self, pool: memhub.MemBuddy*): - self.Pool = pool - - # ---- module operations ---- - def NewModule(self, name: str) -> LLVMModule*: - return new_module(self.Pool, name) - - def CreateFunction(self, mod: LLVMModule*, name: str, - ret_ty: LLVMType*) -> Function*: - return create_function(self.Pool, mod, name, ret_ty) - - def CreateDeclare(self, mod: LLVMModule*, name: str, - ret_ty: LLVMType*) -> Function*: - """Create a function declaration (declare, no body).""" - return create_declare(self.Pool, mod, name, ret_ty) - - def AddParam(self, func: Function*, ty: LLVMType*, - name: str) -> Param*: - return add_param(self.Pool, func, ty, name) - - def FunctionSetAttrs(self, func: Function*, attrs: str): - """Set the function-level attribute text.""" - function_set_attrs(func, attrs) - - def ParamSetAttrs(self, param: Param*, attrs: str): - """Set the parameter attribute text.""" - param_set_attrs(param, attrs) - - def CreateBlock(self, func: Function*, - name: str) -> BasicBlock*: - return create_block(self.Pool, func, name) - - def NewBuilder(self, func: Function*) -> IRBuilder*: - return new_builder(self.Pool, func) - - # ---- globals ---- - def NewGlobal(self, mod: LLVMModule*, name: str, - ty: LLVMType*) -> GlobalVariable*: - """Create a global variable and add it to the module.""" - name_dup: str = _str_dup(self.Pool, name) - gv: GlobalVariable* = new_global_variable(self.Pool, name_dup, ty) - if gv is not None: - module_add_global(mod, gv) - return gv - - def CreateGlobalString(self, mod: LLVMModule*, name: str, - text: str) -> GlobalVariable*: - """Create a string-constant global variable.""" - return create_global_string(self.Pool, mod, name, text) - - def CreateGlobalInt(self, mod: LLVMModule*, name: str, - bits: int, val: int64) -> GlobalVariable*: - """Create an integer global with an initializer.""" - return create_global_int(self.Pool, mod, name, bits, val) - - def GlobalSetConstant(self, gv: GlobalVariable*, is_const: int): - global_set_constant(gv, is_const) - - def GlobalSetLinkage(self, gv: GlobalVariable*, linkage: str): - global_set_linkage(gv, linkage) - - def GlobalSetUnnamedAddr(self, gv: GlobalVariable*, level: int): - global_set_unnamed_addr(gv, level) - - def GlobalSetSection(self, gv: GlobalVariable*, section: str): - global_set_section(gv, section) - - # ---- value creation ---- - def ConstInt(self, bits: int, val: int64) -> Value*: - return const_int(self.Pool, bits, val) - - def ConstInt32(self, val: int64) -> Value*: - return const_int32(self.Pool, val) - - def ConstInt64(self, val: int64) -> Value*: - return const_int64(self.Pool, val) - - def ConstFloat(self, ty: LLVMType*, val: double) -> Value*: - return ConstFloat(self.Pool, ty, val) - - def SSAValue(self, ty: LLVMType*, name: str) -> Value*: - return SSAValue(self.Pool, ty, name) - - def ConstNull(self, ty: LLVMType*, name: str) -> Value*: - return ConstNull(self.Pool, ty, name) - - # ---- printing ---- - def PrintValue(self, buf: str, size: sizet, val: Value*): - ValuePrint(buf, size, val, self.Pool) - - def PrintFunction(self, buf: str, size: sizet, func: Function*): - FunctionPrint(buf, size, func, self.Pool) - - def PrintModule(self, buf: str, size: sizet, mod: LLVMModule*): - LLVMModulePrint(buf, size, mod, self.Pool, OUTPUT_FULL) - - # ---- SSA verification ---- - def NewVerifyResult(self, buf_size: sizet) -> VerifyResult*: - """Create a verify-result object (buf_size = error message buffer size).""" - return _new_verify_result(self.Pool, buf_size) - - def VerifyFunction(self, func: Function*, - result: VerifyResult*) -> int: - """Verify SSA correctness of one function; returns error count (0 = pass).""" - return verify_function(self.Pool, func, result) - - def VerifyModule(self, mod: LLVMModule*, - result: VerifyResult*) -> int: - """Verify SSA correctness of all functions in the module; returns error count (0 = pass).""" - return verify_module(self.Pool, mod, result) +from stdint import * +import memhub +import string +import viperlib + +# ============================================================ +# mlirlite -- an MLIR (llvm dialect) text emission library, isomorphic to llvmlite name for name +# +# Design principles: +# * The internal model (LLVMType/Value/Function/BasicBlock/IRBuilder/Module...) is +# **isomorphic to includes/llvmlite name for name and signature for signature**, so on the VPC side it suffices to rename +# `llvmlite.` to `mlirlite.` in order to compile; +# * The difference lies only in the "printing" stage: TypePrint / ValuePrint / FunctionPrint / +# LLVMModulePrint emit MLIR text, while the builders still generate the intermediate instruction text as before; +# * Names keep the LLVMxxx form (isomorphism first). See README.md for semantic differences. +# ============================================================ + +# Import submodules +from .__types import ( + LLVMType, ParamNode, LLVMTypeCore, + Int1, Int8, Int16, Int32, Int64, Ptr, Func, Void, Array, Struct, + Half, Float, Double, FP128, Label, + new_param_node, param_list_append, + paramnode_get_ty, paramnode_get_next, paramnode_set_ty, paramnode_set_next, + TypePrint, PrintStructDefinition, get_struct_name, + get_struct_fields, get_struct_fcount, DeepCopyType, +) +from .__values import ( + Value, new_value, ConstInt, ConstNull, ConstZero, ConstFloat, SSAValue, ValuePrint, + value_get_ty, value_get_next, + value_set_ty, value_set_next, value_set_name, value_set_isconst, +) +from .__function import ( + Line, BasicBlock, Param, Function, + new_line, new_basic_block, new_param, new_function, + function_add_param, function_add_block, + block_append_line, block_append_text, + function_set_attrs, param_set_attrs, + function_set_attr_group, + function_set_dbg, + function_get_name, function_get_name_len, function_get_ret_ty, function_get_params, + function_get_param_head, function_get_next, function_is_declared, + param_get_name, param_get_ty, param_get_next, + FunctionPrint, G_PeepEnable, + DebugSetEnabled, DebugIsEnabled, DebugNoteFunc, DebugClearFunc, + DebugNoteLine, DebugResetFunc, DebugTakeText, +) +from .__module import ( + LLVMModule, GlobalVariable, NamedTypeNode, DINode, DINamedMeta, + new_module, new_global_variable, + module_add_function, module_add_global, module_add_named_type, + module_set_named_type_packed, + module_set_target, module_set_datalayout, + module_add_di_node, module_add_named_metadata, + global_set_constant, global_set_linkage, global_set_unnamed_addr, global_set_section, + LLVMModulePrint, OUTPUT_FULL, OUTPUT_STUB, OUTPUT_TEXT, + module_ensure_opaque_for_type, +) +from .__builder import ( + IRBuilder, new_builder, position_at_end, + builder_cur_block_is_terminated, + builder_set_debug_location, builder_clear_debug_location, + build_alloca, build_alloca_aligned, build_alloca_at_entry, build_load, build_store, + build_store_at_entry, + build_add, build_sub, build_mul, build_sdiv, build_udiv, build_srem, build_urem, + build_and, build_or, build_xor, build_shl, build_lshr, build_ashr, + build_fadd, build_fsub, build_fmul, build_fdiv, build_frem, build_fneg, + build_icmp, build_fcmp, + build_br, build_cond_br, build_ret, build_ret_void, + build_call, build_call_indirect, build_bitcast, build_sext, build_trunc, build_zext, + build_ptrtoint, build_inttoptr, + build_fpext, build_fptrunc, build_fp2si, build_si2fp, build_fp2ui, build_ui2fp, + build_gep, build_gep_array, build_gep_struct, + build_phi, build_switch, build_select, build_unreachable, + build_atomicrmw, build_cmpxchg, build_inline_asm, build_va_arg, + PhiIncoming, new_phi_incoming, SwitchCase, new_switch_case, + ICMP_EQ, ICMP_NE, ICMP_SGT, ICMP_SGE, ICMP_SLT, ICMP_SLE, + ICMP_UGT, ICMP_UGE, ICMP_ULT, ICMP_ULE, + FCMP_FALSE, FCMP_OEQ, FCMP_OGT, FCMP_OGE, FCMP_OLT, FCMP_OLE, + FCMP_ONE, FCMP_ORD, FCMP_UNO, FCMP_UEQ, FCMP_UGT, FCMP_UGE, + FCMP_ULT, FCMP_ULE, FCMP_UNE, FCMP_TRUE, + ATOMIC_XCHG, ATOMIC_ADD, ATOMIC_SUB, ATOMIC_AND, ATOMIC_NAND, + ATOMIC_OR, ATOMIC_XOR, ATOMIC_MAX, ATOMIC_MIN, ATOMIC_UMAX, ATOMIC_UMIN, + ATOMIC_ORDER_NOTATOMIC, ATOMIC_ORDER_UNORDERED, ATOMIC_ORDER_MONOTONIC, + ATOMIC_ORDER_ACQUIRE, ATOMIC_ORDER_RELEASE, ATOMIC_ORDER_ACQ_REL, + ATOMIC_ORDER_SEQ_CST, +) +from .__verify import ( + VerifyResult, NameEntry, + verify_function, verify_module, + VERIFY_OK, VERIFY_ERR_NO_TERMINATOR, VERIFY_ERR_DUPLICATE_DEF, + VERIFY_ERR_UNDEFINED_USE, VERIFY_ERR_PHI_NOT_FIRST, + _new_verify_result, +) + + +# ============================================================ +# Convenience factory: string copy +# ============================================================ +def _str_dup(pool: memhub.MemBuddy*, src: str) -> str: + """Allocate from pool and copy a C string.""" + if src is None: return None + slen: sizet = string.strlen(src) + buf: str = pool.alloc(slen + 1) + if buf is None: return None + string.strcpy(buf, src) + return buf + + +# ============================================================ +# Convenience factory: create a named basic block +# ============================================================ +def create_block(pool: memhub.MemBuddy*, func: Function*, + name: str) -> BasicBlock*: + """Create a basic block and add it to the function.""" + # Block-name/param conflict guard: block label "entry:" is value %entry, so a param %entry + # collides; on collision append .1/.2/... (strip a leading % from Param.Name before comparing). + use_name: str = name + if func is not None and func.Params is not None and name is not None \ + and name[0] != '\0': + cb_len: sizet = string.strlen(name) + 16 + cb_buf: str = pool.alloc(cb_len) + if cb_buf is not None: + cb_k: int = 1 + cb_collide: int = 1 + while cb_collide != 0: + cb_collide = 0 + cb_p: Param* = function_get_param_head(func) + while cb_p is not None: + cb_pnm: str = param_get_name(cb_p) + if cb_pnm is not None: + cb_pv: str = cb_pnm + if cb_pv[0] == '%': + cb_pv = cb_pv + 1 + if string.strcmp(cb_pv, use_name) == 0: + cb_collide = 1 + break + cb_p = param_get_next(cb_p) + if cb_collide != 0: + viperlib.snprintf(cb_buf, cb_len, "%s.%d", name, cb_k) + use_name = cb_buf + cb_k += 1 + name_dup: str = _str_dup(pool, use_name) + blk: BasicBlock* = new_basic_block(pool, name_dup) + if blk is not None: + function_add_block(func, blk) + return blk + + +# ============================================================ +# Convenience factory: create an integer constant value +# ============================================================ +def const_int(pool: memhub.MemBuddy*, bits: int, + val: int64) -> Value*: + """Create an integer constant value (auto-creates type + name text). + bits: bit width (1/8/16/32/64); val: integer value. + """ + # create type + ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if ty is None: return None + string.memset(ty, 0, LLVMType.__sizeof__()) + *ty = *LLVMType.Int(bits) + + # create name text + name: str = pool.alloc(32) + if name is None: return None + # LLVM i64 literals exclude INT64_MIN: represent it as the unsigned bit pattern + # 9223372036854775808 (accepted by LLVM), else opt rejects "-9223372036854775808". + if bits == 64 and val == -9223372036854775807 - 1: + viperlib.snprintf(name, 32, "9223372036854775808") + else: + viperlib.snprintf(name, 32, "%lld", val) + + return ConstInt(pool, ty, val, name) + + +def const_int32(pool: memhub.MemBuddy*, val: int64) -> Value*: + """Convenience: create an i32 integer constant.""" + return const_int(pool, 32, val) + + +def const_int64(pool: memhub.MemBuddy*, val: int64) -> Value*: + """Convenience: create an i64 integer constant.""" + return const_int(pool, 64, val) + + +# ============================================================ +# Convenience factory: create a function and add it to the module +# ============================================================ +def create_function(pool: memhub.MemBuddy*, mod: LLVMModule*, + name: str, ret_ty: LLVMType*) -> Function*: + """Create a function and add it to the module.""" + name_dup: str = _str_dup(pool, name) + func: Function* = new_function(pool, name_dup, ret_ty) + if func is not None: + module_add_function(mod, func) + return func + + +def add_param(pool: memhub.MemBuddy*, func: Function*, + ty: LLVMType*, name: str) -> Param*: + """Create a parameter and add it to the function.""" + name_dup: str = _str_dup(pool, name) + param: Param* = new_param(pool, ty, name_dup) + if param is not None: + function_add_param(func, param) + return param + + +# ============================================================ +# Convenience factory: create a function declaration (declare) +# ============================================================ +def create_declare(pool: memhub.MemBuddy*, mod: LLVMModule*, + name: str, ret_ty: LLVMType*) -> Function*: + """Create a function declaration (declare, no body) and add it to the module. + Unlike create_function, it emits only: declare @() + """ + name_dup: str = _str_dup(pool, name) + func: Function* = new_function(pool, name_dup, ret_ty) + if func is not None: + func.IsDeclared = 1 + module_add_function(mod, func) + return func + + +# ============================================================ +# Convenience factory: create a string-constant global variable +# +# The initializer is stored in gv.Initializer as llvmlite-shaped LLVM syntax text (c"contents\00"); +# the MLIR render stage (__module.vp/_MlirGlobalInit) converts it to the attribute form "contents". +# ============================================================ +def create_global_string(pool: memhub.MemBuddy*, mod: LLVMModule*, + name: str, text: str) -> GlobalVariable*: + """Create a string-constant global variable and add it to the module. + name: global name (e.g. ".str"); text: C string without trailing NUL (added automatically). + Returns: GlobalVariable pointer of type [len+1 x i8]. + """ + if text is None: return None + slen: sizet = string.strlen(text) + # 显式收窄:LLVMType.Array 的 Count 是 int(i32),而 slen 是 sizet(u64); + # 这里按显式转换(非隐式窄化)取 i32(字符串长度远超 2^31 不现实)。 + count: int = int(slen + 1) # includes the trailing NUL + + # create i8 type + i8_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if i8_ty is None: return None + string.memset(i8_ty, 0, LLVMType.__sizeof__()) + *i8_ty = *LLVMType.Int(8) + + # create [count x i8] array type + arr_ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if arr_ty is None: return None + string.memset(arr_ty, 0, LLVMType.__sizeof__()) + *arr_ty = *LLVMType.Array(i8_ty, count) + + # create global variable + name_dup: str = _str_dup(pool, name) + gv: GlobalVariable* = new_global_variable(pool, name_dup, arr_ty) + if gv is None: return None + + # set attributes: private + unnamed_addr + constant + global_set_linkage(gv, "private") + global_set_unnamed_addr(gv, 1) + global_set_constant(gv, 1) + + # Build initializer text c"contents\00": LLVM IR string literal c"" with " \ and + # non-printables escaped as \xx; buffer sized for worst case (4 bytes/char) + c"..." + \00 + " + init_buf: str = pool.alloc(slen * 4 + 8) + if init_buf is None: return None + pos: sizet = 0 + init_buf[pos] = 'c' + pos += 1 + init_buf[pos] = '"' + pos += 1 + for i in range(int(slen)): + ch: uint8 = text[i] + if ch == 0x22 or ch == 0x5C or ch < 0x20 or ch >= 0x7F: + # escape: " -> \22, \ -> \5c, non-printable -> \xx + init_buf[pos] = '\\' + pos += 1 + hi: int = (ch >> 4) & 0x0F + lo: int = ch & 0x0F + if hi < 10: + init_buf[pos] = char(0x30 + hi) + else: + init_buf[pos] = char(0x61 + hi - 10) + pos += 1 + if lo < 10: + init_buf[pos] = char(0x30 + lo) + else: + init_buf[pos] = char(0x61 + lo - 10) + pos += 1 + else: + init_buf[pos] = ch + pos += 1 + # trailing NUL byte + init_buf[pos] = '\\' + pos += 1 + init_buf[pos] = '0' + pos += 1 + init_buf[pos] = '0' + pos += 1 + init_buf[pos] = '"' + pos += 1 + init_buf[pos] = '\0' + gv.Initializer = init_buf + + module_add_global(mod, gv) + return gv + + +# ============================================================ +# Convenience factory: create an integer global with an initializer +# ============================================================ +def create_global_int(pool: memhub.MemBuddy*, mod: LLVMModule*, + name: str, bits: int, val: int64) -> GlobalVariable*: + """Create an integer global variable and add it to the module. + MLIR output: llvm.mlir.global @( : i) : i + """ + # create integer type + ty: LLVMType* = pool.alloc(LLVMType.__sizeof__()) + if ty is None: return None + string.memset(ty, 0, LLVMType.__sizeof__()) + *ty = *LLVMType.Int(bits) + + name_dup: str = _str_dup(pool, name) + gv: GlobalVariable* = new_global_variable(pool, name_dup, ty) + if gv is None: return None + + # build initializer text "42" (the type is printed separately by the global render stage) + init_buf: str = pool.alloc(48) + if init_buf is None: return None + viperlib.snprintf(init_buf, 48, "%lld", val) + gv.Initializer = init_buf + + module_add_global(mod, gv) + return gv + + +# ============================================================ +# LLVMModuleCore: module/function/value creation core; wraps pool to avoid passing it repeatedly +# +# Usage: +# mcore: rawptr[LLVMModuleCore] = LLVMModuleCore(pool) +# mod: rawptr[LLVMModule] = mcore.NewModule("test") +# func: rawptr[Function] = mcore.CreateFunction(mod, "add", ty_i32) +# val: rawptr[Value] = mcore.ConstInt32(42) +# ============================================================ +novirtual class LLVMModuleCore: + """Module/function/value creation core; wraps pool so callers need not pass it.""" + Pool: memhub.MemBuddy* + + def __init__(self, pool: memhub.MemBuddy*): + self.Pool = pool + + # ---- module operations ---- + def NewModule(self, name: str) -> LLVMModule*: + return new_module(self.Pool, name) + + def CreateFunction(self, mod: LLVMModule*, name: str, + ret_ty: LLVMType*) -> Function*: + return create_function(self.Pool, mod, name, ret_ty) + + def CreateDeclare(self, mod: LLVMModule*, name: str, + ret_ty: LLVMType*) -> Function*: + """Create a function declaration (declare, no body).""" + return create_declare(self.Pool, mod, name, ret_ty) + + def AddParam(self, func: Function*, ty: LLVMType*, + name: str) -> Param*: + return add_param(self.Pool, func, ty, name) + + def FunctionSetAttrs(self, func: Function*, attrs: str): + """Set the function-level attribute text.""" + function_set_attrs(func, attrs) + + def ParamSetAttrs(self, param: Param*, attrs: str): + """Set the parameter attribute text.""" + param_set_attrs(param, attrs) + + def CreateBlock(self, func: Function*, + name: str) -> BasicBlock*: + return create_block(self.Pool, func, name) + + def NewBuilder(self, func: Function*) -> IRBuilder*: + return new_builder(self.Pool, func) + + # ---- globals ---- + def NewGlobal(self, mod: LLVMModule*, name: str, + ty: LLVMType*) -> GlobalVariable*: + """Create a global variable and add it to the module.""" + name_dup: str = _str_dup(self.Pool, name) + gv: GlobalVariable* = new_global_variable(self.Pool, name_dup, ty) + if gv is not None: + module_add_global(mod, gv) + return gv + + def CreateGlobalString(self, mod: LLVMModule*, name: str, + text: str) -> GlobalVariable*: + """Create a string-constant global variable.""" + return create_global_string(self.Pool, mod, name, text) + + def CreateGlobalInt(self, mod: LLVMModule*, name: str, + bits: int, val: int64) -> GlobalVariable*: + """Create an integer global with an initializer.""" + return create_global_int(self.Pool, mod, name, bits, val) + + def GlobalSetConstant(self, gv: GlobalVariable*, is_const: int): + global_set_constant(gv, is_const) + + def GlobalSetLinkage(self, gv: GlobalVariable*, linkage: str): + global_set_linkage(gv, linkage) + + def GlobalSetUnnamedAddr(self, gv: GlobalVariable*, level: int): + global_set_unnamed_addr(gv, level) + + def GlobalSetSection(self, gv: GlobalVariable*, section: str): + global_set_section(gv, section) + + # ---- value creation ---- + def ConstInt(self, bits: int, val: int64) -> Value*: + return const_int(self.Pool, bits, val) + + def ConstInt32(self, val: int64) -> Value*: + return const_int32(self.Pool, val) + + def ConstInt64(self, val: int64) -> Value*: + return const_int64(self.Pool, val) + + def ConstFloat(self, ty: LLVMType*, val: double) -> Value*: + return ConstFloat(self.Pool, ty, val) + + def SSAValue(self, ty: LLVMType*, name: str) -> Value*: + return SSAValue(self.Pool, ty, name) + + def ConstNull(self, ty: LLVMType*, name: str) -> Value*: + return ConstNull(self.Pool, ty, name) + + # ---- printing ---- + def PrintValue(self, buf: str, size: sizet, val: Value*): + ValuePrint(buf, size, val, self.Pool) + + def PrintFunction(self, buf: str, size: sizet, func: Function*): + FunctionPrint(buf, size, func, self.Pool) + + def PrintModule(self, buf: str, size: sizet, mod: LLVMModule*): + LLVMModulePrint(buf, size, mod, self.Pool, OUTPUT_FULL) + + # ---- SSA verification ---- + def NewVerifyResult(self, buf_size: sizet) -> VerifyResult*: + """Create a verify-result object (buf_size = error message buffer size).""" + return _new_verify_result(self.Pool, buf_size) + + def VerifyFunction(self, func: Function*, + result: VerifyResult*) -> int: + """Verify SSA correctness of one function; returns error count (0 = pass).""" + return verify_function(self.Pool, func, result) + + def VerifyModule(self, mod: LLVMModule*, + result: VerifyResult*) -> int: + """Verify SSA correctness of all functions in the module; returns error count (0 = pass).""" + return verify_module(self.Pool, mod, result) diff --git a/mlirlite/__module.vp b/mlirlite/__module.vp index 168ae4a..a8c88eb 100644 --- a/mlirlite/__module.vp +++ b/mlirlite/__module.vp @@ -1,1640 +1,1640 @@ -import t, c -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 "! = !{ }"; 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} { }. - 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 "! = !{ }".""" - 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: " : " - _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. - TPV 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 _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") - p: str = src + 1 - _append_cstr(buf, size, "[") - n: int = 0 - g: int = 0 - while p[0] != '\0': - g += 1 - if g > 4096: - 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: - tb: str = pool.alloc(512) - if tb is None: - MlirFail("MLIR render: array element type buffer allocation failed") - p = _mlir_read_type(p, tb, 512) - p = _mlir_ws(p) - vb: str = pool.alloc(512) - if vb is None: - MlirFail("MLIR render: array element value buffer allocation failed") - p = _mlir_read_value(p, vb, 512) - _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: str = pool.alloc(4096) - if mtb is None: - MlirFail("MLIR render: array element MLIR type buffer allocation failed") - 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) - r1: str = pool.alloc_tagged(512, memhub.DETSITE_MODULE_987) - r2: str = pool.alloc_tagged(512, memhub.DETSITE_MODULE_988) - nm: str = pool.alloc_tagged(512, memhub.DETSITE_MODULE_989) - vl: str = pool.alloc_tagged(512, 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 : ` + 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, 512) - 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, 512) - 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, 512) - 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 > 4096: - 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 > 65536: - 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). - - TPV 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 ` `. 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 ` `: 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, 512) - 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, 512) - _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, 512) - _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, 512) - _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: - 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: - 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 : .""" - 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( : ) : .""" - 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 +import t, c +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 "! = !{ }"; 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} { }. + 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 "! = !{ }".""" + 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: " : " + _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 _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") + p: str = src + 1 + _append_cstr(buf, size, "[") + n: int = 0 + g: int = 0 + while p[0] != '\0': + g += 1 + if g > 4096: + 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: + tb: str = pool.alloc(512) + if tb is None: + MlirFail("MLIR render: array element type buffer allocation failed") + p = _mlir_read_type(p, tb, 512) + p = _mlir_ws(p) + vb: str = pool.alloc(512) + if vb is None: + MlirFail("MLIR render: array element value buffer allocation failed") + p = _mlir_read_value(p, vb, 512) + _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: str = pool.alloc(4096) + if mtb is None: + MlirFail("MLIR render: array element MLIR type buffer allocation failed") + 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) + r1: str = pool.alloc_tagged(512, memhub.DETSITE_MODULE_987) + r2: str = pool.alloc_tagged(512, memhub.DETSITE_MODULE_988) + nm: str = pool.alloc_tagged(512, memhub.DETSITE_MODULE_989) + vl: str = pool.alloc_tagged(512, 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 : ` + 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, 512) + 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, 512) + 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, 512) + 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 > 4096: + 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 > 65536: + 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 ` `. 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 ` `: 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, 512) + 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, 512) + _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, 512) + _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, 512) + _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: + 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: + 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 : .""" + 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( : ) : .""" + 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 \ No newline at end of file diff --git a/viperlib.vp b/viperlib.vp index a63b616..a9e6970 100644 --- a/viperlib.vp +++ b/viperlib.vp @@ -1,1383 +1,1383 @@ -import viperio -from stdarg import arg - - -define TEMP_BUF_SIZE = 68 - -def get_digit_count(num: uint, base: int) -> int: - count: int = 0 - if num == 0: return 1 - while num > 0: - count += 1 - # 显式收窄口径:目标 num 是无符号、base 是有符号,按新规则必须显式声明用无符号语义 - # (base 恒为 2/10/16 等正整数;若传负值,uint(base) 会得到其无符号位型,语义与旧隐式行为一致)。 - num //= uint(base) - return count - -# Return type must be int, matching the CRT `sprintf` and includes/stdio.vp; -# return vsnprintf's write length directly (consistent with the CRT). -export def sprintf(buf: str, rule: str, *args) -> int: - if buf is None or rule is None: return 0 - return vsnprintf(buf, 0x7FFFFFFF, rule, args) - -def vsprintf(buf: str, rule: str, *args) -> int: - if buf is None or rule is None: return 0 - return vsnprintf(buf, 0x7FFFFFFF, rule, args) - -export def snprintf(buf: str, size: sizet, fmt: str, *args) -> int: - if fmt is None: return 0 - if buf is None or size == 0: return vsnprintf(None, 0, fmt, args) - ret: int = vsnprintf(buf, size, fmt, args) - return ret - -# Check float special values (IEEE 754): 0=normal, 1=+inf, 2=-inf, 3=nan. -# Do NOT read the bit pattern via `*&fbits = *&fv`: the assignment is silently dropped (fbits stays 0), so this function would always return 0 and inf would spin forever in the float formatting loops. Use pure float semantics instead (NaN: fv != fv; ±inf: fv - fv is NaN, finite gives 0.0). -def _check_special_float(fv: double) -> int: - if fv != fv: return 3 - diff: double = fv - fv - if diff != diff: - if fv < 0.0: return 2 - return 1 - return 0 - -# The 4th parameter MUST stay named `args`: the printf wrappers forward their own va_list pointer -# here (vprintf style), and the `arg()` sugar resolves the forwarded va_list by that name. -export def vsnprintf(buf: bytes, size: sizet, fmt: str, args: str) -> int: - if fmt is None: return 0 - # Length detection mode: when buf is None, only calculate the formatted length, without actually writing - # _step=0 makes ptr not advance, repeatedly writing the same dummy byte (harmless), write_count accumulates correctly - count_mode: int = 0 - _step: int = 1 - _dummy: char = '\0' - if buf is None or size == 0: - count_mode = 1 - _step = 0 - buf = &_dummy - size = 0x7FFFFFFF - ptr: str = buf - write_count: int = 0 - temp: [68]char - temp[0] = '\t' - while *fmt != 0 and write_count < size - 1: - if *fmt != 37: - ptr[0] = *fmt - write_count += 1 - ptr += _step - fmt += 1 - else: - fmt += 1 - if *fmt == 0: break - width: int = 0 - pad_zero: int = 0 - left_align: int = 0 - show_sign: int = 0 - space_sign: int = 0 - alt_form: int = 0 - precision: int = -1 - is_long: int = 0 - is_long_long: int = 0 - is_size: int = 0 - is_short: int = 0 - is_char: int = 0 - while True: - if *fmt == 45: - left_align = 1 - fmt += 1 - elif *fmt == 48: - pad_zero = 1 - fmt += 1 - elif *fmt == 43: - show_sign = 1 - fmt += 1 - elif *fmt == 32: - space_sign = 1 - fmt += 1 - elif *fmt == 35: - alt_form = 1 - fmt += 1 - else: break - if show_sign: space_sign = 0 - while *fmt >= 48 and *fmt <= 57: - width = width * 10 + (*fmt - 48) - fmt += 1 - if *fmt == 46: - fmt += 1 - precision = 0 - while *fmt >= 48 and *fmt <= 57: - precision = precision * 10 + (*fmt - 48) - fmt += 1 - if left_align: pad_zero = 0 - if *fmt == 108: - is_long = 1 - fmt += 1 - if *fmt == 108: - is_long_long = 1 - is_long = 0 - fmt += 1 - elif *fmt == 104: - is_short = 1 - fmt += 1 - if *fmt == 104: - is_char = 1 - is_short = 0 - fmt += 1 - elif *fmt == 122: - is_size = 1 - fmt += 1 - elif *fmt == 106: - is_long_long = 1 - fmt += 1 - elif *fmt == 116: - is_long = 1 - fmt += 1 - if *fmt == 99: - if write_count < size - 1: - ptr_i: int = arg(int) - ptr[0] = char(ptr_i) - write_count += 1 - ptr += _step - else: - ptr_i: int = arg(int) - elif *fmt == 115: - s: str = arg(rawptr) - if s is None: s = "(None)" - slen: int = 0 - sc: str = s - while *sc != 0: - slen += 1 - sc += 1 - if not left_align and width > slen: - pad_count: int = width - slen - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - while *s != 0 and write_count < size - 1: - ptr[0] = *s - write_count += 1 - ptr += _step - s += 1 - if left_align and width > slen: - pad_count = width - slen - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 100 or *fmt == 105: - if is_long_long or is_size or is_long: - num_l: int64 = arg(int64) - else: - num_l: int64 = int64(arg(int)) - if is_char: - num_l = int64(char(num_l)) - elif is_short: - num_l = int64(short(num_l)) - sign: int = 0 - if num_l < 0: - sign = 1 - if num_l == (-9223372036854775807 - 1): - u_num: uint64 = uint64(0x8000000000000000) - else: - u_num: uint64 = uint64(-num_l) - else: - u_num: uint64 = uint64(num_l) - if show_sign: sign = 2 - elif space_sign: sign = 3 - i: int = 0 - while True: - if i >= TEMP_BUF_SIZE - 1: break - temp[i] = char(u_num % 10 + 48) - i += 1 - u_num //= 10 - if u_num <= 0: break - if sign == 1 and i < TEMP_BUF_SIZE - 1: - temp[i] = 45 - i += 1 - elif sign == 2 and i < TEMP_BUF_SIZE - 1: - temp[i] = 43 - i += 1 - elif sign == 3 and i < TEMP_BUF_SIZE - 1: - temp[i] = 32 - i += 1 - if not left_align and not pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 117: - if is_long or is_long_long or is_size: - u_num_l: uint64 = arg(uint64) - else: - u_num_l: uint64 = uint64(uint(arg())) - i = 0 - while True: - if i >= TEMP_BUF_SIZE - 1: break - temp[i] = char(u_num_l % 10 + 48) - i += 1 - u_num_l //= 10 - if u_num_l <= 0: break - if not left_align and not pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 120: - if is_long or is_long_long or is_size: - u_num_l: uint64 = arg(uint64) - else: - u_num_l: uint64 = uint64(uint(arg())) - is_nonzero: int = 0 - if u_num_l != 0: - is_nonzero = 1 - i = 0 - while True: - if i >= TEMP_BUF_SIZE - 1: break - digit: int = int(u_num_l % 16) - if digit < 10: - temp[i] = char(digit + 48) - else: - temp[i] = char(digit - 10 + 97) - u_num_l //= 16 - i += 1 - if u_num_l <= 0: break - digit_count: int = i - prefix_len: int = 0 - if alt_form and is_nonzero: - prefix_len = 2 - effective_len: int = digit_count + prefix_len - if not left_align and not pad_zero and width > effective_len: - pad_count = width - effective_len - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if prefix_len > 0: - if write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - if write_count < size - 1: - ptr[0] = 120 - write_count += 1 - ptr += _step - if pad_zero and width > effective_len: - pad_count = width - effective_len - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > effective_len: - pad_count = width - effective_len - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 88: - if is_long or is_long_long or is_size: - u_num_l: uint64 = arg() - else: - u_num_l: uint64 = uint64(uint(arg())) - is_nonzero = 0 - if u_num_l != 0: - is_nonzero = 1 - i = 0 - while True: - if i >= TEMP_BUF_SIZE - 1: break - digit = int(u_num_l % 16) - if digit < 10: - temp[i] = char(digit + 48) - else: - temp[i] = char(digit - 10 + 65) - u_num_l //= 16 - i += 1 - if u_num_l <= 0: break - digit_count = i - prefix_len = 0 - if alt_form and is_nonzero: - prefix_len = 2 - effective_len = digit_count + prefix_len - if not left_align and not pad_zero and width > effective_len: - pad_count = width - effective_len - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if prefix_len > 0: - if write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - if write_count < size - 1: - ptr[0] = 88 - write_count += 1 - ptr += _step - if pad_zero and width > effective_len: - pad_count = width - effective_len - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > effective_len: - pad_count = width - effective_len - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 111: - if is_long or is_long_long or is_size: - u_num_l: uint64 = arg(uint64) - else: - u_num_l: uint64 = uint64(uint(arg())) - if is_char: - u_num_l = uint64(char(u_num_l)) - elif is_short: - u_num_l = uint64(short(u_num_l)) - i = 0 - if u_num_l == 0 and not alt_form: - temp[0] = 48 - i = 1 - else: - while True: - if i >= TEMP_BUF_SIZE - 1: break - temp[i] = char(u_num_l % 8 + 48) - i += 1 - u_num_l //= 8 - if u_num_l <= 0: break - if alt_form and i < TEMP_BUF_SIZE - 1: - temp[i] = 48 - i += 1 - if not left_align and not pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 98: - if is_long or is_long_long or is_size: - u_num_l: uint64 = arg(uint64) - else: - u_num_l: uint64 = uint64(uint(arg())) - if is_char: - u_num_l = uint64(char(u_num_l)) - elif is_short: - u_num_l = uint64(short(u_num_l)) - i = 0 - if u_num_l == 0: - temp[0] = 48 - i = 1 - else: - while True: - if i >= TEMP_BUF_SIZE - 1: break - temp[i] = char(u_num_l % 2 + 48) - i += 1 - u_num_l //= 2 - if u_num_l <= 0: break - if alt_form and i < TEMP_BUF_SIZE - 1: - temp[i] = 98 - i += 1 - if not left_align and not pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 112: - ptr_val: uint64 = uint64(arg(rawptr)) - if ptr_val == 0: - temp[0] = 40 - temp[1] = 110 - temp[2] = 105 - temp[3] = 108 - temp[4] = 41 - i = 5 - else: - u_num_l = ptr_val - i = 0 - while True: - if i >= TEMP_BUF_SIZE - 1: break - digit = int(u_num_l % 16) - if digit < 10: - temp[i] = char(digit + 48) - else: - temp[i] = char(digit - 10 + 97) - u_num_l //= 16 - i += 1 - if u_num_l <= 0: break - if i < TEMP_BUF_SIZE - 1: - temp[i] = 120 - i += 1 - if i < TEMP_BUF_SIZE - 1: - temp[i] = 48 - i += 1 - if not left_align and not pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while i > 0 and write_count < size - 1: - i -= 1 - ptr[0] = temp[i] - write_count += 1 - ptr += _step - if left_align and width > i: - pad_count = width - i - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 102 or *fmt == 70: - f_upper: int = 1 if *fmt == 70 else 0 - if precision < 0: precision = 6 - fv: double = arg(double) - f_sp: int = _check_special_float(fv) - if f_sp != 0: - f_sgn_len: int = 0 - if f_sp == 2: - f_sgn_len = 1 - elif f_sp == 1 and show_sign: - f_sgn_len = 1 - elif f_sp == 1 and space_sign: - f_sgn_len = 1 - f_total_len: int = f_sgn_len + 3 - if not left_align and width > f_total_len: - f_pad: int = width - f_total_len - while f_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - f_pad -= 1 - if f_sp == 2: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - elif f_sp == 1 and show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif f_sp == 1 and space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - fi_c: int = 0 - while fi_c < 3 and write_count < size - 1: - fc: int = 0 - if f_sp == 3: - # nan - if fi_c == 0: - fc = 110 - elif fi_c == 1: - fc = 97 - else: - fc = 110 - else: - # inf (f_sp==1 +inf or f_sp==2 -inf) - if fi_c == 0: - fc = 105 - elif fi_c == 1: - fc = 110 - else: - fc = 102 - if f_upper and fc >= 97 and fc <= 122: - fc -= 32 - ptr[0] = char(fc) - write_count += 1 - ptr += _step - fi_c += 1 - if left_align and width > f_total_len: - f_pad = width - f_total_len - while f_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - f_pad -= 1 - fmt += 1 - continue - if fv < 0.0: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - fv = 0.0 - fv - elif show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - di: int = 0 - work: double = fv - if work < 1.0: - temp[0] = 48 - di = 1 - else: - scale: double = 1.0 - # Upper-bound guard: inf makes `work >= scale*10.0` always true; finite values need at most 308 iterations - scale_guard: int = 0 - while work >= scale * 10.0 and scale_guard < 400: - scale *= 10.0 - scale_guard += 1 - while scale >= 1.0 and di < TEMP_BUF_SIZE - 1: - d: int = int(work / scale) - if d > 9: d = 9 - temp[di] = char(d + 48) - di += 1 - work = work - double(d) * scale - scale /= 10.0 - frac_val: double = work - for p in range(int(precision)): - frac_val *= 10.0 - frac_part: uint64 = uint64(frac_val + 0.5) - f_total: int = di + (precision + 1 if precision > 0 else 0) - if not left_align and not pad_zero and width > f_total: - pad_count = width - f_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > f_total: - pad_count = width - f_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - while di > 0 and write_count < size - 1: - di -= 1 - ptr[0] = temp[di] - write_count += 1 - ptr += _step - if precision > 0: - if write_count < size - 1: - ptr[0] = 46 - write_count += 1 - ptr += _step - fi: int = 0 - while fi < precision and fi < TEMP_BUF_SIZE - 1: - temp[fi] = char(frac_part % 10 + 48) - fi += 1 - frac_part //= 10 - while fi < precision and fi < TEMP_BUF_SIZE - 1: - temp[fi] = 48 - fi += 1 - while fi > 0 and write_count < size - 1: - fi -= 1 - ptr[0] = temp[fi] - write_count += 1 - ptr += _step - elif alt_form: - if write_count < size - 1: - ptr[0] = 46 - write_count += 1 - ptr += _step - if left_align and width > f_total: - pad_count = width - f_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 101 or *fmt == 69: - if precision < 0: precision = 6 - fv: double = arg(double) - e_upper: int = 1 if *fmt == 69 else 0 - e_sp: int = _check_special_float(fv) - if e_sp != 0: - e_sgn_len: int = 0 - if e_sp == 2: - e_sgn_len = 1 - elif e_sp == 1 and show_sign: - e_sgn_len = 1 - elif e_sp == 1 and space_sign: - e_sgn_len = 1 - e_total_len: int = e_sgn_len + 3 - if not left_align and width > e_total_len: - e_pad: int = width - e_total_len - while e_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - e_pad -= 1 - if e_sp == 2: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - elif e_sp == 1 and show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif e_sp == 1 and space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - ei_c: int = 0 - while ei_c < 3 and write_count < size - 1: - ec: int = 0 - if e_sp == 3: - # nan - if ei_c == 0: - ec = 110 - elif ei_c == 1: - ec = 97 - else: - ec = 110 - else: - # inf (e_sp==1 +inf or e_sp==2 -inf) - if ei_c == 0: - ec = 105 - elif ei_c == 1: - ec = 110 - else: - ec = 102 - if e_upper and ec >= 97 and ec <= 122: - ec -= 32 - ptr[0] = char(ec) - write_count += 1 - ptr += _step - ei_c += 1 - if left_align and width > e_total_len: - e_pad = width - e_total_len - while e_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - e_pad -= 1 - fmt += 1 - continue - if fv < 0.0: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - fv = 0.0 - fv - elif show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - exp10: int = 0 - if fv != 0.0: - # Upper-bound guard: inf makes the two loops below always true (inf/10==inf, inf*10==inf) - exp_guard: int = 0 - while fv >= 10.0 and exp_guard < 400: - fv /= 10.0 - exp10 += 1 - exp_guard += 1 - exp_guard = 0 - while fv < 1.0 and exp_guard < 400: - fv *= 10.0 - exp10 -= 1 - exp_guard += 1 - int_digit: int = int(fv) - if int_digit > 9: int_digit = 9 - if int_digit < 0: int_digit = 0 - frac_v: double = fv - double(int_digit) - p: int = 0 - while p < precision: - frac_v *= 10.0 - p += 1 - frac_p: uint64 = uint64(frac_v + 0.5) - carry: uint64 = 1 - for p in range(int(precision)): - carry *= 10 - if frac_p >= carry and carry > 0: - frac_p = 0 - int_digit += 1 - if int_digit > 9: - int_digit = 1 - exp10 += 1 - e_total: int = 1 + (precision + 1 if precision > 0 else 0) + 4 - if not left_align and not pad_zero and width > e_total: - pad_count = width - e_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > e_total: - pad_count = width - e_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - if write_count < size - 1: - ptr[0] = char(int_digit + 48) - write_count += 1 - ptr += _step - if precision > 0: - if write_count < size - 1: - ptr[0] = 46 - write_count += 1 - ptr += _step - fi: int = 0 - while fi < precision and fi < TEMP_BUF_SIZE - 1: - temp[fi] = char(frac_p % 10 + 48) - fi += 1 - frac_p //= 10 - while fi < precision and fi < TEMP_BUF_SIZE - 1: - temp[fi] = 48 - fi += 1 - while fi > 0 and write_count < size - 1: - fi -= 1 - ptr[0] = temp[fi] - write_count += 1 - ptr += _step - elif alt_form: - if write_count < size - 1: - ptr[0] = 46 - write_count += 1 - ptr += _step - if write_count < size - 1: - ptr[0] = char(101 if e_upper == 0 else 69) - write_count += 1 - ptr += _step - if write_count < size - 1: - ptr[0] = char(45 if exp10 < 0 else 43) - write_count += 1 - ptr += _step - exp_abs: uint64 = uint64(exp10) if exp10 >= 0 else uint64(0 - exp10) - ei: int = 0 - if exp_abs == 0: - temp[0] = 48 - temp[1] = 48 - ei = 2 - else: - while exp_abs > 0 and ei < TEMP_BUF_SIZE - 1: - temp[ei] = char(exp_abs % 10 + 48) - ei += 1 - exp_abs //= 10 - while ei < 2: - temp[ei] = 48 - ei += 1 - while ei > 0 and write_count < size - 1: - ei -= 1 - ptr[0] = temp[ei] - write_count += 1 - ptr += _step - if left_align and width > e_total: - pad_count = width - e_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 103 or *fmt == 71: - if precision < 0: precision = 6 - if precision == 0: precision = 1 - fv: double = arg(double) - g_upper: int = 1 if *fmt == 71 else 0 - g_sp: int = _check_special_float(fv) - if g_sp != 0: - g_sgn_len: int = 0 - if g_sp == 2: - g_sgn_len = 1 - elif g_sp == 1 and show_sign: - g_sgn_len = 1 - elif g_sp == 1 and space_sign: - g_sgn_len = 1 - g_total_len: int = g_sgn_len + 3 - if not left_align and width > g_total_len: - g_pad: int = width - g_total_len - while g_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - g_pad -= 1 - if g_sp == 2: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - elif g_sp == 1 and show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif g_sp == 1 and space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - gi_c: int = 0 - while gi_c < 3 and write_count < size - 1: - gc: int = 0 - if g_sp == 3: - # nan - if gi_c == 0: - gc = 110 - elif gi_c == 1: - gc = 97 - else: - gc = 110 - else: - # inf (g_sp==1 +inf or g_sp==2 -inf) - if gi_c == 0: - gc = 105 - elif gi_c == 1: - gc = 110 - else: - gc = 102 - if g_upper and gc >= 97 and gc <= 122: - gc -= 32 - ptr[0] = char(gc) - write_count += 1 - ptr += _step - gi_c += 1 - if left_align and width > g_total_len: - g_pad = width - g_total_len - while g_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - g_pad -= 1 - fmt += 1 - continue - if fv < 0.0: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - fv = 0.0 - fv - elif show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - fv_orig: double = fv - exp10: int = 0 - if fv != 0.0: - # Upper-bound guard: this loop is where it hangs (inf makes fv>=10.0 always true, inf/10==inf) - exp_guard: int = 0 - while fv >= 10.0 and exp_guard < 400: - fv /= 10.0 - exp10 += 1 - exp_guard += 1 - exp_guard = 0 - while fv < 1.0 and exp_guard < 400: - fv *= 10.0 - exp10 -= 1 - exp_guard += 1 - g_use_e: int = 0 - if exp10 < -4 or exp10 >= precision: - g_use_e = 1 - g_len: int = 0 - if g_use_e: - int_digit: int = int(fv) - if int_digit > 9: int_digit = 9 - if int_digit < 0: int_digit = 0 - frac_v: double = fv - double(int_digit) - prec_e: int = precision - 1 - p: int = 0 - while p < prec_e: - frac_v *= 10.0 - p += 1 - frac_p: uint64 = uint64(frac_v + 0.5) - carry: uint64 = 1 - for p in range(int(prec_e)): - carry *= 10 - if frac_p >= carry and carry > 0: - frac_p = 0 - int_digit += 1 - if int_digit > 9: - int_digit = 1 - exp10 += 1 - temp[0] = char(int_digit + 48) - g_len = 1 - if prec_e > 0: - temp[1] = 46 - g_len = 2 - # Store fraction digits in reverse order (ones place at g_len+prec_e-1) so temp is already forward-ordered. - # The original swap-reverse loop fails in TPV output (digits not reversed, e.g. 456->654); reverse storage matches %f's output and avoids the three-variable swap. - fi: int = 0 - fp_inv: uint64 = frac_p - fp_idx: int = g_len + prec_e - 1 - while fi < prec_e and fp_idx < TEMP_BUF_SIZE - 1: - temp[fp_idx] = char(fp_inv % 10 + 48) - fp_idx -= 1 - fp_inv //= 10 - fi += 1 - g_len += prec_e - else: - prec_f: int = precision - 1 - exp10 - if prec_f < 0: prec_f = 0 - work: double = fv_orig - if work < 1.0: - temp[0] = 48 - g_len = 1 - else: - scale: double = 1.0 - # Upper-bound guard: inf makes `work >= scale*10.0` always true; finite values need at most 308 iterations - scale_guard: int = 0 - while work >= scale * 10.0 and scale_guard < 400: - scale *= 10.0 - scale_guard += 1 - while scale >= 1.0 and g_len < TEMP_BUF_SIZE - 1: - d: int = int(work / scale) - if d > 9: d = 9 - temp[g_len] = char(d + 48) - g_len += 1 - work = work - double(d) * scale - scale /= 10.0 - if prec_f > 0 and g_len < TEMP_BUF_SIZE - 1: - temp[g_len] = 46 - g_len += 1 - frac_val: double = work - for p in range(int(prec_f)): - frac_val *= 10.0 - frac_part: uint64 = uint64(frac_val + 0.5) - # Store decimal digits in reverse order (ones place goes to g_len prec_f-1), temp is directly in forward order. - # The original swap-reverse loop doesn't work in TPV compiled output (decimal digits aren't reversed, e.g., 456->654), - # Change to reverse storage, consistent with %f's reverse output, avoiding three-variable swapping. - fi = 0 - fp_inv: uint64 = frac_part - fp_idx: int = g_len + prec_f - 1 - while fi < prec_f and fp_idx < TEMP_BUF_SIZE - 1: - temp[fp_idx] = char(fp_inv % 10 + 48) - fp_idx -= 1 - fp_inv //= 10 - fi += 1 - g_len += prec_f - if not alt_form and g_len > 0: - while g_len > 0: - if temp[g_len - 1] == 48: - g_len -= 1 - else: - break - if g_len > 0 and temp[g_len - 1] == 46: - g_len -= 1 - g_total: int = g_len - if g_use_e: g_total = g_len + 4 - if not left_align and not pad_zero and width > g_total: - pad_count = width - g_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - if pad_zero and width > g_total: - pad_count = width - g_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - pad_count -= 1 - gi: int = 0 - while gi < g_len and write_count < size - 1: - ptr[0] = temp[gi] - write_count += 1 - ptr += _step - gi += 1 - if g_use_e: - if write_count < size - 1: - ptr[0] = char(101 if g_upper == 0 else 69) - write_count += 1 - ptr += _step - if write_count < size - 1: - ptr[0] = char(45 if exp10 < 0 else 43) - write_count += 1 - ptr += _step - exp_abs: uint64 = uint64(exp10) if exp10 >= 0 else uint64(0 - exp10) - ei: int = 0 - if exp_abs == 0: - temp[0] = 48 - temp[1] = 48 - ei = 2 - else: - while exp_abs > 0 and ei < TEMP_BUF_SIZE - 1: - temp[ei] = char(exp_abs % 10 + 48) - ei += 1 - exp_abs //= 10 - while ei < 2: - temp[ei] = 48 - ei += 1 - while ei > 0 and write_count < size - 1: - ei -= 1 - ptr[0] = temp[ei] - write_count += 1 - ptr += _step - if left_align and width > g_total: - pad_count = width - g_total - while pad_count > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - pad_count -= 1 - elif *fmt == 97 or *fmt == 65: - a_upper: int = 1 if *fmt == 65 else 0 - fv: double = arg(double) - a_sp: int = _check_special_float(fv) - if a_sp != 0: - a_sgn_len: int = 0 - if a_sp == 2: - a_sgn_len = 1 - elif a_sp == 1 and show_sign: - a_sgn_len = 1 - elif a_sp == 1 and space_sign: - a_sgn_len = 1 - a_total_len: int = a_sgn_len + 3 - if not left_align and width > a_total_len: - a_pad: int = width - a_total_len - while a_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - a_pad -= 1 - if a_sp == 2: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - elif a_sp == 1 and show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif a_sp == 1 and space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - ai_c: int = 0 - while ai_c < 3 and write_count < size - 1: - ac: int = 0 - if a_sp == 3: - # nan - if ai_c == 0: - ac = 110 - elif ai_c == 1: - ac = 97 - else: - ac = 110 - else: - # inf (a_sp==1 +inf or a_sp==2 -inf) - if ai_c == 0: - ac = 105 - elif ai_c == 1: - ac = 110 - else: - ac = 102 - if a_upper and ac >= 97 and ac <= 122: - ac -= 32 - ptr[0] = char(ac) - write_count += 1 - ptr += _step - ai_c += 1 - if left_align and width > a_total_len: - a_pad = width - a_total_len - while a_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - a_pad -= 1 - fmt += 1 - continue - fbits: uint64 = 0 - *&fbits = *&fv - a_neg: int = 1 if (fbits >> 63) != 0 else 0 - a_exp: int = int((fbits >> 52) & 0x7FF) - a_frac: uint64 = fbits & 0x000FFFFFFFFFFFFF - a_leading: int = 0 - a_binexp: int = 0 - if a_exp == 0: - a_leading = 0 - if a_frac == 0: - a_binexp = 0 - else: - a_binexp = -1022 - else: - a_leading = 1 - a_binexp = a_exp - 1023 - a_prec: int = precision - if a_prec < 0: - a_prec = 13 - if a_prec > 50: - a_prec = 50 - a_hi: int = 0 - while a_hi < 13 and a_hi < a_prec: - a_digit: int = int((a_frac >> (48 - 4 * a_hi)) & 0xF) - if a_digit < 10: - temp[a_hi] = char(a_digit + 48) - else: - if a_upper: - temp[a_hi] = char(a_digit - 10 + 65) - else: - temp[a_hi] = char(a_digit - 10 + 97) - a_hi += 1 - while a_hi < a_prec: - temp[a_hi] = 48 - a_hi += 1 - a_hd_count: int = a_prec - if precision < 0: - while a_hd_count > 0 and temp[a_hd_count - 1] == 48: - a_hd_count -= 1 - a_has_dot: int = 0 - if a_hd_count > 0: - a_has_dot = 1 - elif alt_form: - a_has_dot = 1 - a_eb: int = a_binexp - a_esign: int = 0 - if a_binexp < 0: - a_esign = 1 - a_eb = -a_binexp - a_ei: int = 0 - if a_eb == 0: - temp[60] = 48 - a_ei = 1 - else: - while a_eb > 0 and a_ei < 7: - temp[60 + a_ei] = char(a_eb % 10 + 48) - a_ei += 1 - a_eb //= 10 - a_ecount: int = a_ei - a_sign_len: int = 0 - if a_neg: - a_sign_len = 1 - elif show_sign: - a_sign_len = 1 - elif space_sign: - a_sign_len = 1 - a_content_len: int = a_sign_len + 2 + 1 + a_has_dot + a_hd_count + 1 + 1 + a_ecount - if not left_align and not pad_zero and width > a_content_len: - a_pad = width - a_content_len - while a_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - a_pad -= 1 - if a_neg: - if write_count < size - 1: - ptr[0] = 45 - write_count += 1 - ptr += _step - elif show_sign: - if write_count < size - 1: - ptr[0] = 43 - write_count += 1 - ptr += _step - elif space_sign: - if write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - if pad_zero and width > a_content_len: - a_pad = width - a_content_len - while a_pad > 0 and write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - a_pad -= 1 - if write_count < size - 1: - ptr[0] = 48 - write_count += 1 - ptr += _step - if write_count < size - 1: - if a_upper: - ptr[0] = 88 - else: - ptr[0] = 120 - write_count += 1 - ptr += _step - if write_count < size - 1: - ptr[0] = char(a_leading + 48) - write_count += 1 - ptr += _step - if a_has_dot: - if write_count < size - 1: - ptr[0] = 46 - write_count += 1 - ptr += _step - a_hj: int = 0 - while a_hj < a_hd_count and write_count < size - 1: - ptr[0] = temp[a_hj] - write_count += 1 - ptr += _step - a_hj += 1 - if write_count < size - 1: - if a_upper: - ptr[0] = 80 - else: - ptr[0] = 112 - write_count += 1 - ptr += _step - if write_count < size - 1: - if a_esign: - ptr[0] = 45 - else: - ptr[0] = 43 - write_count += 1 - ptr += _step - a_ek: int = a_ecount - while a_ek > 0 and write_count < size - 1: - a_ek -= 1 - ptr[0] = temp[60 + a_ek] - write_count += 1 - ptr += _step - if left_align and width > a_content_len: - a_pad = width - a_content_len - while a_pad > 0 and write_count < size - 1: - ptr[0] = 32 - write_count += 1 - ptr += _step - a_pad -= 1 - elif *fmt == 110: - if is_long_long or is_long: - np: int64* = arg(int64*) - if np != None: - np[0] = int64(write_count) - elif is_short: - sp: short* = arg(short*) - if sp != None: - sp[0] = short(write_count) - elif is_char: - cp: str = arg(str) - if cp != None: - cp[0] = char(write_count) - else: - ip: int* = arg(int*) - if ip != None: - ip[0] = write_count - elif *fmt == 37: - if write_count < size - 1: - ptr[0] = 37 - write_count += 1 - ptr += _step - else: - if write_count < size - 1: - ptr[0] = *fmt - write_count += 1 - ptr += _step - fmt += 1 - ptr[0] = 0 - return write_count +import viperio +from stdarg import arg + + +define TEMP_BUF_SIZE = 68 + +def get_digit_count(num: uint, base: int) -> int: + count: int = 0 + if num == 0: return 1 + while num > 0: + count += 1 + # 显式收窄口径:目标 num 是无符号、base 是有符号,按新规则必须显式声明用无符号语义 + # (base 恒为 2/10/16 等正整数;若传负值,uint(base) 会得到其无符号位型,语义与旧隐式行为一致)。 + num //= uint(base) + return count + +# Return type must be int, matching the CRT `sprintf` and includes/stdio.vp; +# return vsnprintf's write length directly (consistent with the CRT). +export def sprintf(buf: str, rule: str, *args) -> int: + if buf is None or rule is None: return 0 + return vsnprintf(buf, 0x7FFFFFFF, rule, args) + +def vsprintf(buf: str, rule: str, *args) -> int: + if buf is None or rule is None: return 0 + return vsnprintf(buf, 0x7FFFFFFF, rule, args) + +export def snprintf(buf: str, size: sizet, fmt: str, *args) -> int: + if fmt is None: return 0 + if buf is None or size == 0: return vsnprintf(None, 0, fmt, args) + ret: int = vsnprintf(buf, size, fmt, args) + return ret + +# Check float special values (IEEE 754): 0=normal, 1=+inf, 2=-inf, 3=nan. +# Do NOT read the bit pattern via `*&fbits = *&fv`: the assignment is silently dropped (fbits stays 0), so this function would always return 0 and inf would spin forever in the float formatting loops. Use pure float semantics instead (NaN: fv != fv; ±inf: fv - fv is NaN, finite gives 0.0). +def _check_special_float(fv: double) -> int: + if fv != fv: return 3 + diff: double = fv - fv + if diff != diff: + if fv < 0.0: return 2 + return 1 + return 0 + +# The 4th parameter MUST stay named `args`: the printf wrappers forward their own va_list pointer +# here (vprintf style), and the `arg()` sugar resolves the forwarded va_list by that name. +export def vsnprintf(buf: bytes, size: sizet, fmt: str, args: str) -> int: + if fmt is None: return 0 + # Length detection mode: when buf is None, only calculate the formatted length, without actually writing + # _step=0 makes ptr not advance, repeatedly writing the same dummy byte (harmless), write_count accumulates correctly + count_mode: int = 0 + _step: int = 1 + _dummy: char = '\0' + if buf is None or size == 0: + count_mode = 1 + _step = 0 + buf = &_dummy + size = 0x7FFFFFFF + ptr: str = buf + write_count: int = 0 + temp: [68]char + temp[0] = '\t' + while *fmt != 0 and write_count < size - 1: + if *fmt != 37: + ptr[0] = *fmt + write_count += 1 + ptr += _step + fmt += 1 + else: + fmt += 1 + if *fmt == 0: break + width: int = 0 + pad_zero: int = 0 + left_align: int = 0 + show_sign: int = 0 + space_sign: int = 0 + alt_form: int = 0 + precision: int = -1 + is_long: int = 0 + is_long_long: int = 0 + is_size: int = 0 + is_short: int = 0 + is_char: int = 0 + while True: + if *fmt == 45: + left_align = 1 + fmt += 1 + elif *fmt == 48: + pad_zero = 1 + fmt += 1 + elif *fmt == 43: + show_sign = 1 + fmt += 1 + elif *fmt == 32: + space_sign = 1 + fmt += 1 + elif *fmt == 35: + alt_form = 1 + fmt += 1 + else: break + if show_sign: space_sign = 0 + while *fmt >= 48 and *fmt <= 57: + width = width * 10 + (*fmt - 48) + fmt += 1 + if *fmt == 46: + fmt += 1 + precision = 0 + while *fmt >= 48 and *fmt <= 57: + precision = precision * 10 + (*fmt - 48) + fmt += 1 + if left_align: pad_zero = 0 + if *fmt == 108: + is_long = 1 + fmt += 1 + if *fmt == 108: + is_long_long = 1 + is_long = 0 + fmt += 1 + elif *fmt == 104: + is_short = 1 + fmt += 1 + if *fmt == 104: + is_char = 1 + is_short = 0 + fmt += 1 + elif *fmt == 122: + is_size = 1 + fmt += 1 + elif *fmt == 106: + is_long_long = 1 + fmt += 1 + elif *fmt == 116: + is_long = 1 + fmt += 1 + if *fmt == 99: + if write_count < size - 1: + ptr_i: int = arg(int) + ptr[0] = char(ptr_i) + write_count += 1 + ptr += _step + else: + ptr_i: int = arg(int) + elif *fmt == 115: + s: str = arg(rawptr) + if s is None: s = "(None)" + slen: int = 0 + sc: str = s + while *sc != 0: + slen += 1 + sc += 1 + if not left_align and width > slen: + pad_count: int = width - slen + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + while *s != 0 and write_count < size - 1: + ptr[0] = *s + write_count += 1 + ptr += _step + s += 1 + if left_align and width > slen: + pad_count = width - slen + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 100 or *fmt == 105: + if is_long_long or is_size or is_long: + num_l: int64 = arg(int64) + else: + num_l: int64 = int64(arg(int)) + if is_char: + num_l = int64(char(num_l)) + elif is_short: + num_l = int64(short(num_l)) + sign: int = 0 + if num_l < 0: + sign = 1 + if num_l == (-9223372036854775807 - 1): + u_num: uint64 = uint64(0x8000000000000000) + else: + u_num: uint64 = uint64(-num_l) + else: + u_num: uint64 = uint64(num_l) + if show_sign: sign = 2 + elif space_sign: sign = 3 + i: int = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num % 10 + 48) + i += 1 + u_num //= 10 + if u_num <= 0: break + if sign == 1 and i < TEMP_BUF_SIZE - 1: + temp[i] = 45 + i += 1 + elif sign == 2 and i < TEMP_BUF_SIZE - 1: + temp[i] = 43 + i += 1 + elif sign == 3 and i < TEMP_BUF_SIZE - 1: + temp[i] = 32 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 117: + if is_long or is_long_long or is_size: + u_num_l: uint64 = arg(uint64) + else: + u_num_l: uint64 = uint64(uint(arg())) + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num_l % 10 + 48) + i += 1 + u_num_l //= 10 + if u_num_l <= 0: break + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 120: + if is_long or is_long_long or is_size: + u_num_l: uint64 = arg(uint64) + else: + u_num_l: uint64 = uint64(uint(arg())) + is_nonzero: int = 0 + if u_num_l != 0: + is_nonzero = 1 + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + digit: int = int(u_num_l % 16) + if digit < 10: + temp[i] = char(digit + 48) + else: + temp[i] = char(digit - 10 + 97) + u_num_l //= 16 + i += 1 + if u_num_l <= 0: break + digit_count: int = i + prefix_len: int = 0 + if alt_form and is_nonzero: + prefix_len = 2 + effective_len: int = digit_count + prefix_len + if not left_align and not pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if prefix_len > 0: + if write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 120 + write_count += 1 + ptr += _step + if pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 88: + if is_long or is_long_long or is_size: + u_num_l: uint64 = arg() + else: + u_num_l: uint64 = uint64(uint(arg())) + is_nonzero = 0 + if u_num_l != 0: + is_nonzero = 1 + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + digit = int(u_num_l % 16) + if digit < 10: + temp[i] = char(digit + 48) + else: + temp[i] = char(digit - 10 + 65) + u_num_l //= 16 + i += 1 + if u_num_l <= 0: break + digit_count = i + prefix_len = 0 + if alt_form and is_nonzero: + prefix_len = 2 + effective_len = digit_count + prefix_len + if not left_align and not pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if prefix_len > 0: + if write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = 88 + write_count += 1 + ptr += _step + if pad_zero and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > effective_len: + pad_count = width - effective_len + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 111: + if is_long or is_long_long or is_size: + u_num_l: uint64 = arg(uint64) + else: + u_num_l: uint64 = uint64(uint(arg())) + if is_char: + u_num_l = uint64(char(u_num_l)) + elif is_short: + u_num_l = uint64(short(u_num_l)) + i = 0 + if u_num_l == 0 and not alt_form: + temp[0] = 48 + i = 1 + else: + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num_l % 8 + 48) + i += 1 + u_num_l //= 8 + if u_num_l <= 0: break + if alt_form and i < TEMP_BUF_SIZE - 1: + temp[i] = 48 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 98: + if is_long or is_long_long or is_size: + u_num_l: uint64 = arg(uint64) + else: + u_num_l: uint64 = uint64(uint(arg())) + if is_char: + u_num_l = uint64(char(u_num_l)) + elif is_short: + u_num_l = uint64(short(u_num_l)) + i = 0 + if u_num_l == 0: + temp[0] = 48 + i = 1 + else: + while True: + if i >= TEMP_BUF_SIZE - 1: break + temp[i] = char(u_num_l % 2 + 48) + i += 1 + u_num_l //= 2 + if u_num_l <= 0: break + if alt_form and i < TEMP_BUF_SIZE - 1: + temp[i] = 98 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 112: + ptr_val: uint64 = uint64(arg(rawptr)) + if ptr_val == 0: + temp[0] = 40 + temp[1] = 110 + temp[2] = 105 + temp[3] = 108 + temp[4] = 41 + i = 5 + else: + u_num_l = ptr_val + i = 0 + while True: + if i >= TEMP_BUF_SIZE - 1: break + digit = int(u_num_l % 16) + if digit < 10: + temp[i] = char(digit + 48) + else: + temp[i] = char(digit - 10 + 97) + u_num_l //= 16 + i += 1 + if u_num_l <= 0: break + if i < TEMP_BUF_SIZE - 1: + temp[i] = 120 + i += 1 + if i < TEMP_BUF_SIZE - 1: + temp[i] = 48 + i += 1 + if not left_align and not pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while i > 0 and write_count < size - 1: + i -= 1 + ptr[0] = temp[i] + write_count += 1 + ptr += _step + if left_align and width > i: + pad_count = width - i + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 102 or *fmt == 70: + f_upper: int = 1 if *fmt == 70 else 0 + if precision < 0: precision = 6 + fv: double = arg(double) + f_sp: int = _check_special_float(fv) + if f_sp != 0: + f_sgn_len: int = 0 + if f_sp == 2: + f_sgn_len = 1 + elif f_sp == 1 and show_sign: + f_sgn_len = 1 + elif f_sp == 1 and space_sign: + f_sgn_len = 1 + f_total_len: int = f_sgn_len + 3 + if not left_align and width > f_total_len: + f_pad: int = width - f_total_len + while f_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + f_pad -= 1 + if f_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif f_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif f_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + fi_c: int = 0 + while fi_c < 3 and write_count < size - 1: + fc: int = 0 + if f_sp == 3: + # nan + if fi_c == 0: + fc = 110 + elif fi_c == 1: + fc = 97 + else: + fc = 110 + else: + # inf (f_sp==1 +inf or f_sp==2 -inf) + if fi_c == 0: + fc = 105 + elif fi_c == 1: + fc = 110 + else: + fc = 102 + if f_upper and fc >= 97 and fc <= 122: + fc -= 32 + ptr[0] = char(fc) + write_count += 1 + ptr += _step + fi_c += 1 + if left_align and width > f_total_len: + f_pad = width - f_total_len + while f_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + f_pad -= 1 + fmt += 1 + continue + if fv < 0.0: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + fv = 0.0 - fv + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + di: int = 0 + work: double = fv + if work < 1.0: + temp[0] = 48 + di = 1 + else: + scale: double = 1.0 + # Upper-bound guard: inf makes `work >= scale*10.0` always true; finite values need at most 308 iterations + scale_guard: int = 0 + while work >= scale * 10.0 and scale_guard < 400: + scale *= 10.0 + scale_guard += 1 + while scale >= 1.0 and di < TEMP_BUF_SIZE - 1: + d: int = int(work / scale) + if d > 9: d = 9 + temp[di] = char(d + 48) + di += 1 + work = work - double(d) * scale + scale /= 10.0 + frac_val: double = work + for p in range(int(precision)): + frac_val *= 10.0 + frac_part: uint64 = uint64(frac_val + 0.5) + f_total: int = di + (precision + 1 if precision > 0 else 0) + if not left_align and not pad_zero and width > f_total: + pad_count = width - f_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > f_total: + pad_count = width - f_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + while di > 0 and write_count < size - 1: + di -= 1 + ptr[0] = temp[di] + write_count += 1 + ptr += _step + if precision > 0: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + fi: int = 0 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = char(frac_part % 10 + 48) + fi += 1 + frac_part //= 10 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = 48 + fi += 1 + while fi > 0 and write_count < size - 1: + fi -= 1 + ptr[0] = temp[fi] + write_count += 1 + ptr += _step + elif alt_form: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + if left_align and width > f_total: + pad_count = width - f_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 101 or *fmt == 69: + if precision < 0: precision = 6 + fv: double = arg(double) + e_upper: int = 1 if *fmt == 69 else 0 + e_sp: int = _check_special_float(fv) + if e_sp != 0: + e_sgn_len: int = 0 + if e_sp == 2: + e_sgn_len = 1 + elif e_sp == 1 and show_sign: + e_sgn_len = 1 + elif e_sp == 1 and space_sign: + e_sgn_len = 1 + e_total_len: int = e_sgn_len + 3 + if not left_align and width > e_total_len: + e_pad: int = width - e_total_len + while e_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + e_pad -= 1 + if e_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif e_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif e_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + ei_c: int = 0 + while ei_c < 3 and write_count < size - 1: + ec: int = 0 + if e_sp == 3: + # nan + if ei_c == 0: + ec = 110 + elif ei_c == 1: + ec = 97 + else: + ec = 110 + else: + # inf (e_sp==1 +inf or e_sp==2 -inf) + if ei_c == 0: + ec = 105 + elif ei_c == 1: + ec = 110 + else: + ec = 102 + if e_upper and ec >= 97 and ec <= 122: + ec -= 32 + ptr[0] = char(ec) + write_count += 1 + ptr += _step + ei_c += 1 + if left_align and width > e_total_len: + e_pad = width - e_total_len + while e_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + e_pad -= 1 + fmt += 1 + continue + if fv < 0.0: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + fv = 0.0 - fv + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + exp10: int = 0 + if fv != 0.0: + # Upper-bound guard: inf makes the two loops below always true (inf/10==inf, inf*10==inf) + exp_guard: int = 0 + while fv >= 10.0 and exp_guard < 400: + fv /= 10.0 + exp10 += 1 + exp_guard += 1 + exp_guard = 0 + while fv < 1.0 and exp_guard < 400: + fv *= 10.0 + exp10 -= 1 + exp_guard += 1 + int_digit: int = int(fv) + if int_digit > 9: int_digit = 9 + if int_digit < 0: int_digit = 0 + frac_v: double = fv - double(int_digit) + p: int = 0 + while p < precision: + frac_v *= 10.0 + p += 1 + frac_p: uint64 = uint64(frac_v + 0.5) + carry: uint64 = 1 + for p in range(int(precision)): + carry *= 10 + if frac_p >= carry and carry > 0: + frac_p = 0 + int_digit += 1 + if int_digit > 9: + int_digit = 1 + exp10 += 1 + e_total: int = 1 + (precision + 1 if precision > 0 else 0) + 4 + if not left_align and not pad_zero and width > e_total: + pad_count = width - e_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > e_total: + pad_count = width - e_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + if write_count < size - 1: + ptr[0] = char(int_digit + 48) + write_count += 1 + ptr += _step + if precision > 0: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + fi: int = 0 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = char(frac_p % 10 + 48) + fi += 1 + frac_p //= 10 + while fi < precision and fi < TEMP_BUF_SIZE - 1: + temp[fi] = 48 + fi += 1 + while fi > 0 and write_count < size - 1: + fi -= 1 + ptr[0] = temp[fi] + write_count += 1 + ptr += _step + elif alt_form: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = char(101 if e_upper == 0 else 69) + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = char(45 if exp10 < 0 else 43) + write_count += 1 + ptr += _step + exp_abs: uint64 = uint64(exp10) if exp10 >= 0 else uint64(0 - exp10) + ei: int = 0 + if exp_abs == 0: + temp[0] = 48 + temp[1] = 48 + ei = 2 + else: + while exp_abs > 0 and ei < TEMP_BUF_SIZE - 1: + temp[ei] = char(exp_abs % 10 + 48) + ei += 1 + exp_abs //= 10 + while ei < 2: + temp[ei] = 48 + ei += 1 + while ei > 0 and write_count < size - 1: + ei -= 1 + ptr[0] = temp[ei] + write_count += 1 + ptr += _step + if left_align and width > e_total: + pad_count = width - e_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 103 or *fmt == 71: + if precision < 0: precision = 6 + if precision == 0: precision = 1 + fv: double = arg(double) + g_upper: int = 1 if *fmt == 71 else 0 + g_sp: int = _check_special_float(fv) + if g_sp != 0: + g_sgn_len: int = 0 + if g_sp == 2: + g_sgn_len = 1 + elif g_sp == 1 and show_sign: + g_sgn_len = 1 + elif g_sp == 1 and space_sign: + g_sgn_len = 1 + g_total_len: int = g_sgn_len + 3 + if not left_align and width > g_total_len: + g_pad: int = width - g_total_len + while g_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + g_pad -= 1 + if g_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif g_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif g_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + gi_c: int = 0 + while gi_c < 3 and write_count < size - 1: + gc: int = 0 + if g_sp == 3: + # nan + if gi_c == 0: + gc = 110 + elif gi_c == 1: + gc = 97 + else: + gc = 110 + else: + # inf (g_sp==1 +inf or g_sp==2 -inf) + if gi_c == 0: + gc = 105 + elif gi_c == 1: + gc = 110 + else: + gc = 102 + if g_upper and gc >= 97 and gc <= 122: + gc -= 32 + ptr[0] = char(gc) + write_count += 1 + ptr += _step + gi_c += 1 + if left_align and width > g_total_len: + g_pad = width - g_total_len + while g_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + g_pad -= 1 + fmt += 1 + continue + if fv < 0.0: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + fv = 0.0 - fv + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + fv_orig: double = fv + exp10: int = 0 + if fv != 0.0: + # Upper-bound guard: this loop is where it hangs (inf makes fv>=10.0 always true, inf/10==inf) + exp_guard: int = 0 + while fv >= 10.0 and exp_guard < 400: + fv /= 10.0 + exp10 += 1 + exp_guard += 1 + exp_guard = 0 + while fv < 1.0 and exp_guard < 400: + fv *= 10.0 + exp10 -= 1 + exp_guard += 1 + g_use_e: int = 0 + if exp10 < -4 or exp10 >= precision: + g_use_e = 1 + g_len: int = 0 + if g_use_e: + int_digit: int = int(fv) + if int_digit > 9: int_digit = 9 + if int_digit < 0: int_digit = 0 + frac_v: double = fv - double(int_digit) + prec_e: int = precision - 1 + p: int = 0 + while p < prec_e: + frac_v *= 10.0 + p += 1 + frac_p: uint64 = uint64(frac_v + 0.5) + carry: uint64 = 1 + for p in range(int(prec_e)): + carry *= 10 + if frac_p >= carry and carry > 0: + frac_p = 0 + int_digit += 1 + if int_digit > 9: + int_digit = 1 + exp10 += 1 + temp[0] = char(int_digit + 48) + g_len = 1 + if prec_e > 0: + temp[1] = 46 + g_len = 2 + # Store fraction digits in reverse order (ones place at g_len+prec_e-1) so temp is already forward-ordered. + # The original swap-reverse loop fails in VPC output (digits not reversed, e.g. 456->654); reverse storage matches %f's output and avoids the three-variable swap. + fi: int = 0 + fp_inv: uint64 = frac_p + fp_idx: int = g_len + prec_e - 1 + while fi < prec_e and fp_idx < TEMP_BUF_SIZE - 1: + temp[fp_idx] = char(fp_inv % 10 + 48) + fp_idx -= 1 + fp_inv //= 10 + fi += 1 + g_len += prec_e + else: + prec_f: int = precision - 1 - exp10 + if prec_f < 0: prec_f = 0 + work: double = fv_orig + if work < 1.0: + temp[0] = 48 + g_len = 1 + else: + scale: double = 1.0 + # Upper-bound guard: inf makes `work >= scale*10.0` always true; finite values need at most 308 iterations + scale_guard: int = 0 + while work >= scale * 10.0 and scale_guard < 400: + scale *= 10.0 + scale_guard += 1 + while scale >= 1.0 and g_len < TEMP_BUF_SIZE - 1: + d: int = int(work / scale) + if d > 9: d = 9 + temp[g_len] = char(d + 48) + g_len += 1 + work = work - double(d) * scale + scale /= 10.0 + if prec_f > 0 and g_len < TEMP_BUF_SIZE - 1: + temp[g_len] = 46 + g_len += 1 + frac_val: double = work + for p in range(int(prec_f)): + frac_val *= 10.0 + frac_part: uint64 = uint64(frac_val + 0.5) + # Store decimal digits in reverse order (ones place goes to g_len prec_f-1), temp is directly in forward order. + # The original swap-reverse loop doesn't work in VPC compiled output (decimal digits aren't reversed, e.g., 456->654), + # Change to reverse storage, consistent with %f's reverse output, avoiding three-variable swapping. + fi = 0 + fp_inv: uint64 = frac_part + fp_idx: int = g_len + prec_f - 1 + while fi < prec_f and fp_idx < TEMP_BUF_SIZE - 1: + temp[fp_idx] = char(fp_inv % 10 + 48) + fp_idx -= 1 + fp_inv //= 10 + fi += 1 + g_len += prec_f + if not alt_form and g_len > 0: + while g_len > 0: + if temp[g_len - 1] == 48: + g_len -= 1 + else: + break + if g_len > 0 and temp[g_len - 1] == 46: + g_len -= 1 + g_total: int = g_len + if g_use_e: g_total = g_len + 4 + if not left_align and not pad_zero and width > g_total: + pad_count = width - g_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + if pad_zero and width > g_total: + pad_count = width - g_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + pad_count -= 1 + gi: int = 0 + while gi < g_len and write_count < size - 1: + ptr[0] = temp[gi] + write_count += 1 + ptr += _step + gi += 1 + if g_use_e: + if write_count < size - 1: + ptr[0] = char(101 if g_upper == 0 else 69) + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = char(45 if exp10 < 0 else 43) + write_count += 1 + ptr += _step + exp_abs: uint64 = uint64(exp10) if exp10 >= 0 else uint64(0 - exp10) + ei: int = 0 + if exp_abs == 0: + temp[0] = 48 + temp[1] = 48 + ei = 2 + else: + while exp_abs > 0 and ei < TEMP_BUF_SIZE - 1: + temp[ei] = char(exp_abs % 10 + 48) + ei += 1 + exp_abs //= 10 + while ei < 2: + temp[ei] = 48 + ei += 1 + while ei > 0 and write_count < size - 1: + ei -= 1 + ptr[0] = temp[ei] + write_count += 1 + ptr += _step + if left_align and width > g_total: + pad_count = width - g_total + while pad_count > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + pad_count -= 1 + elif *fmt == 97 or *fmt == 65: + a_upper: int = 1 if *fmt == 65 else 0 + fv: double = arg(double) + a_sp: int = _check_special_float(fv) + if a_sp != 0: + a_sgn_len: int = 0 + if a_sp == 2: + a_sgn_len = 1 + elif a_sp == 1 and show_sign: + a_sgn_len = 1 + elif a_sp == 1 and space_sign: + a_sgn_len = 1 + a_total_len: int = a_sgn_len + 3 + if not left_align and width > a_total_len: + a_pad: int = width - a_total_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + if a_sp == 2: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif a_sp == 1 and show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif a_sp == 1 and space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + ai_c: int = 0 + while ai_c < 3 and write_count < size - 1: + ac: int = 0 + if a_sp == 3: + # nan + if ai_c == 0: + ac = 110 + elif ai_c == 1: + ac = 97 + else: + ac = 110 + else: + # inf (a_sp==1 +inf or a_sp==2 -inf) + if ai_c == 0: + ac = 105 + elif ai_c == 1: + ac = 110 + else: + ac = 102 + if a_upper and ac >= 97 and ac <= 122: + ac -= 32 + ptr[0] = char(ac) + write_count += 1 + ptr += _step + ai_c += 1 + if left_align and width > a_total_len: + a_pad = width - a_total_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + fmt += 1 + continue + fbits: uint64 = 0 + *&fbits = *&fv + a_neg: int = 1 if (fbits >> 63) != 0 else 0 + a_exp: int = int((fbits >> 52) & 0x7FF) + a_frac: uint64 = fbits & 0x000FFFFFFFFFFFFF + a_leading: int = 0 + a_binexp: int = 0 + if a_exp == 0: + a_leading = 0 + if a_frac == 0: + a_binexp = 0 + else: + a_binexp = -1022 + else: + a_leading = 1 + a_binexp = a_exp - 1023 + a_prec: int = precision + if a_prec < 0: + a_prec = 13 + if a_prec > 50: + a_prec = 50 + a_hi: int = 0 + while a_hi < 13 and a_hi < a_prec: + a_digit: int = int((a_frac >> (48 - 4 * a_hi)) & 0xF) + if a_digit < 10: + temp[a_hi] = char(a_digit + 48) + else: + if a_upper: + temp[a_hi] = char(a_digit - 10 + 65) + else: + temp[a_hi] = char(a_digit - 10 + 97) + a_hi += 1 + while a_hi < a_prec: + temp[a_hi] = 48 + a_hi += 1 + a_hd_count: int = a_prec + if precision < 0: + while a_hd_count > 0 and temp[a_hd_count - 1] == 48: + a_hd_count -= 1 + a_has_dot: int = 0 + if a_hd_count > 0: + a_has_dot = 1 + elif alt_form: + a_has_dot = 1 + a_eb: int = a_binexp + a_esign: int = 0 + if a_binexp < 0: + a_esign = 1 + a_eb = -a_binexp + a_ei: int = 0 + if a_eb == 0: + temp[60] = 48 + a_ei = 1 + else: + while a_eb > 0 and a_ei < 7: + temp[60 + a_ei] = char(a_eb % 10 + 48) + a_ei += 1 + a_eb //= 10 + a_ecount: int = a_ei + a_sign_len: int = 0 + if a_neg: + a_sign_len = 1 + elif show_sign: + a_sign_len = 1 + elif space_sign: + a_sign_len = 1 + a_content_len: int = a_sign_len + 2 + 1 + a_has_dot + a_hd_count + 1 + 1 + a_ecount + if not left_align and not pad_zero and width > a_content_len: + a_pad = width - a_content_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + if a_neg: + if write_count < size - 1: + ptr[0] = 45 + write_count += 1 + ptr += _step + elif show_sign: + if write_count < size - 1: + ptr[0] = 43 + write_count += 1 + ptr += _step + elif space_sign: + if write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + if pad_zero and width > a_content_len: + a_pad = width - a_content_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + a_pad -= 1 + if write_count < size - 1: + ptr[0] = 48 + write_count += 1 + ptr += _step + if write_count < size - 1: + if a_upper: + ptr[0] = 88 + else: + ptr[0] = 120 + write_count += 1 + ptr += _step + if write_count < size - 1: + ptr[0] = char(a_leading + 48) + write_count += 1 + ptr += _step + if a_has_dot: + if write_count < size - 1: + ptr[0] = 46 + write_count += 1 + ptr += _step + a_hj: int = 0 + while a_hj < a_hd_count and write_count < size - 1: + ptr[0] = temp[a_hj] + write_count += 1 + ptr += _step + a_hj += 1 + if write_count < size - 1: + if a_upper: + ptr[0] = 80 + else: + ptr[0] = 112 + write_count += 1 + ptr += _step + if write_count < size - 1: + if a_esign: + ptr[0] = 45 + else: + ptr[0] = 43 + write_count += 1 + ptr += _step + a_ek: int = a_ecount + while a_ek > 0 and write_count < size - 1: + a_ek -= 1 + ptr[0] = temp[60 + a_ek] + write_count += 1 + ptr += _step + if left_align and width > a_content_len: + a_pad = width - a_content_len + while a_pad > 0 and write_count < size - 1: + ptr[0] = 32 + write_count += 1 + ptr += _step + a_pad -= 1 + elif *fmt == 110: + if is_long_long or is_long: + np: int64* = arg(int64*) + if np != None: + np[0] = int64(write_count) + elif is_short: + sp: short* = arg(short*) + if sp != None: + sp[0] = short(write_count) + elif is_char: + cp: str = arg(str) + if cp != None: + cp[0] = char(write_count) + else: + ip: int* = arg(int*) + if ip != None: + ip[0] = write_count + elif *fmt == 37: + if write_count < size - 1: + ptr[0] = 37 + write_count += 1 + ptr += _step + else: + if write_count < size - 1: + ptr[0] = *fmt + write_count += 1 + ptr += _step + fmt += 1 + ptr[0] = 0 + return write_count diff --git a/vipersimd.vp b/vipersimd.vp index a2b367c..95d6557 100644 --- a/vipersimd.vp +++ b/vipersimd.vp @@ -183,7 +183,7 @@ movups [{asminp(out, ASM_DESCR.REG_ANY)}], xmm0""", def simd_add_array(a: double*, b: double*, out: double*, n: sizet): """AVX2 batch: out[i:i+4] = a[i:i+4] + b[i:i+4], i=0,4,8,... ×8 double-buffering (32 doubles/iteration): 16 loads in flight (a: ymm0-7, b: ymm8-15), then 8 independent add->store chains; tail falls back to the single-vector loop (floor(n/4) vectors). - Note: ymm0-15 clobber requires TPV __asm__ support (CallGenCBuiltin ASM_MAX_CLOBBERS=32).""" + Note: ymm0-15 clobber requires VPC __asm__ support (CallGenCBuiltin ASM_MAX_CLOBBERS=32).""" __asm__(f"""mov rcx, {asminp(n, ASM_DESCR.REG_ANY)} shr rcx, 2 mov r8, {asminp(a, ASM_DESCR.REG_ANY)} diff --git a/win32/win32console.vp b/win32/win32console.vp index d771aa1..a581e2a 100644 --- a/win32/win32console.vp +++ b/win32/win32console.vp @@ -119,6 +119,6 @@ extern def FreeConsole() -> BOOL extern def SetConsoleWindowInfo(hConsoleOutput: HANDLE, bAbsolute: BOOL, lpConsoleWindow: SMALL_RECT*) -> BOOL extern def ScrollConsoleScreenBufferA(hConsoleOutput: HANDLE, lpScrollRectangle: SMALL_RECT*, lpClipRectangle: SMALL_RECT*, dwDestinationOrigin: COORD, lpFill: CHAR_INFO*) -> BOOL -# SetConsoleCtrlHandler registration/removal (consoleapi.h / kernel32.dll): pass the routine name directly, bitcast by TPV (function pointers declared as rawptr). +# SetConsoleCtrlHandler registration/removal (consoleapi.h / kernel32.dll): pass the routine name directly, bitcast by VPC (function pointers declared as rawptr). # Returns 0 on failure (see GetLastError); Add=FALSE removes the registered routine. extern def SetConsoleCtrlHandler(HandlerRoutine: rawptr, Add: BOOL) -> BOOL diff --git a/win32/win32dbghelp.vp b/win32/win32dbghelp.vp index c612041..b2a18c2 100644 --- a/win32/win32dbghelp.vp +++ b/win32/win32dbghelp.vp @@ -118,7 +118,7 @@ extern def SymGetModuleBase64(hProcess: HANDLE, AddrBase: ULONGLONG) -> ULONGLON # PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, # PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, # PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress); -# The three callbacks follow the "function pointers passed as rawptr" convention: pass the function name (ReadProcessMemory / SymFunctionTableAccess64 / SymGetModuleBase64), bitcast by TPV. +# The three callbacks follow the "function pointers passed as rawptr" convention: pass the function name (ReadProcessMemory / SymFunctionTableAccess64 / SymGetModuleBase64), bitcast by VPC. # TranslateAddress is None (0): x64 needs no address-translation routine. extern def StackWalk64(MachineType: DWORD, hProcess: HANDLE, hThread: HANDLE, StackFrame: STACKFRAME64*, ContextRecord: VOIDPTR,