From 15952d38522d48475c453303621e4388ba5a5108 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 17 Oct 2026 17:41:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(asm):=20=E5=86=85=E8=81=94=E6=B1=87?= =?UTF-8?q?=E7=BC=96/LLVMIR=20=E6=96=87=E6=9C=AC=E7=BC=93=E5=86=B2?= =?UTF-8?q?=E6=89=A9=E5=AE=B9=E5=B9=B6=E5=B0=86=E6=89=80=E6=9C=89=E8=B6=85?= =?UTF-8?q?=E9=99=90=E6=94=B9=E4=B8=BA=20fail-fast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ASM_TEXT_BUF_SIZE 2048→8192(内核 _do_switch 模板已 2245 字节),c.LLVMIR 模板 512→4096。_asm_append_cstr 不再静默截断:装不下时报 TypeError 并给出已用/待写/容量字节数(此前截断会把模板切成半条 mov,最终以 llc invalid operand 的面目出现)。操作数超过 16、clobber 超过 32、符号改写缓冲溢出同样 fail-fast;约束串/参数串/extractvalue 行缓冲改为按实际操作数与类型文本长度动态分配。 --- App/lib/core/Gen/Call/CallGenCBuiltin.vp | 121 +++++++++++++++-------- 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/App/lib/core/Gen/Call/CallGenCBuiltin.vp b/App/lib/core/Gen/Call/CallGenCBuiltin.vp index 04476ad..ecd3623 100644 --- a/App/lib/core/Gen/Call/CallGenCBuiltin.vp +++ b/App/lib/core/Gen/Call/CallGenCBuiltin.vp @@ -20,12 +20,18 @@ define ASM_MAX_OPERANDS = 16 # Clobber (~{...}) capacity is decoupled from the operand limit: vipersimd x8 # needs 21 entries, so ASM_MAX_OPERANDS=16 silently truncated it to 16. define ASM_MAX_CLOBBERS = 32 -define ASM_TEXT_BUF_SIZE = 2048 +# ASM_TEXT_BUF_SIZE holds the whole inline-asm template (a kernel switch statement +# template is already 2245 bytes). Overflow is fail-fast in _asm_append_cstr, never +# silently truncated: a cut-in-half template used to surface as a bogus llc parse +# error (e.g. a dangling `mov` with its operands chopped off). +define ASM_TEXT_BUF_SIZE = 8192 +# Constraint buffer base size; the real capacity scales with operand/clobber count. define ASM_CONST_BUF_SIZE = 512 define ASM_ARGS_BUF_SIZE = 512 define ASM_LINE_BUF_SIZE = 4096 define LLVMIR_MAX_OPS = 16 -define LLVMIR_TEMPLATE_BUF = 512 +# Raw LLVM IR templates (c.LLVMIR) are multi-instruction text, not one-liners. +define LLVMIR_TEMPLATE_BUF = 4096 # CallGenCBuiltin - c.*/asm/llvmir builtins @@ -441,15 +447,22 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): return loaded - def _asm_append_cstr(self, dst: str, dst_size: sizet, src: str): - """Append the C string src to the end of dst.""" + def _asm_append_cstr(self, dst: str, dst_size: sizet, src: str, what: str = "inline asm"): + """Append the C string src to the end of dst; fail-fast when it does not fit. + + Silent truncation corrupts the generated text (a template cut in the middle of an + instruction yields a bogus llc/verifier error much later), so overflow is reported + here at the real cause. + """ if dst is None or src is None: return dlen: sizet = string.strlen(dst) slen: sizet = string.strlen(src) - remain: sizet = dst_size - dlen - if remain <= 0: return + # Capacity includes the terminating NUL. + if dlen + slen + 1 > dst_size: + self.Gen.TypeGenErr.fatal_error(None, f"{what} text does not fit the fixed buffer: {dlen} byte(s) already used + {slen} byte(s) to append + 1 NUL > capacity {dst_size} byte(s); shorten or split the template (silent truncation would generate broken code)", "TypeError") + return i: sizet = 0 - while i < slen and i + 1 < remain: + while i < slen: dst[dlen + i] = src[i] i += 1 dst[dlen + i] = '\0' @@ -492,9 +505,14 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): return asm_text cur = cur.Next if matched == 0: - if out_pos + 1 < asm_size * 2: - out[out_pos] = asm_text[i] - out_pos += 1 + # The output buffer is 2x the input, so this cannot trigger for a plain copy; + # keep it fail-fast so a future longer rewrite (mangled expansion) can never + # silently drop characters. + if out_pos + 1 >= asm_size * 2: + self.Gen.TypeGenErr.fatal_error(None, f"inline asm text overflows the symbol-mangling buffer (capacity {asm_size * 2} byte(s)); shorten the template", "TypeError") + return asm_text + out[out_pos] = asm_text[i] + out_pos += 1 i += 1 out[out_pos] = '\0' return out @@ -729,7 +747,11 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): def _asm_add_operand(self, operands: GenTypes.AsmOperand*, count: int, val: mlirlite.Value*, constraint: str, is_output: int) -> int: """Append an operand, dedup by value and direction, and return the new count.""" - if count >= ASM_MAX_OPERANDS: return count + # fail-fast: the operand array is fixed at ASM_MAX_OPERANDS slots; dropping the + # excess operand here would silently change the generated constraint/argument lists. + if count >= ASM_MAX_OPERANDS: + self.Gen.TypeGenErr.fatal_error(None, f"inline asm has more than {ASM_MAX_OPERANDS} operands (compiler limit); split the asm block into several ones", "TypeError") + return count # fail-fast: AsmInp/AsmOut operands must not carry SHA1-mangled symbols; # LLVM MC cannot parse digit-leading names, so mark them t.CExport. if val is not None and val.Name is not None: @@ -956,11 +978,15 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): if celt is None: continue cstr: str = self._resolve_asm_descr(pool, celt) if cstr is None or cstr[0] == '\0': continue - if clobber_count < ASM_MAX_CLOBBERS: - cl_addr: uint64 = uint64(clobbers) + clobber_count * 8 - cl_ptr: str* = (str*)(cl_addr) - cl_ptr[0] = cstr - clobber_count += 1 + # fail-fast: the clobber slot array is fixed at ASM_MAX_CLOBBERS; + # dropping the excess would silently weaken the clobber list. + if clobber_count >= ASM_MAX_CLOBBERS: + self.Gen.TypeGenErr.fatal_error(celt, f"inline asm has more than {ASM_MAX_CLOBBERS} clobbers (compiler limit); remove duplicates or split the asm block", "TypeError") + return None + cl_addr: uint64 = uint64(clobbers) + clobber_count * 8 + cl_ptr: str* = (str*)(cl_addr) + cl_ptr[0] = cstr + clobber_count += 1 # 3. Parse the second positional arg as the clobber list if len(cargs) >= 2: @@ -975,11 +1001,13 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): if pelt is None: continue pstr: str = self._resolve_asm_descr(pool, pelt) if pstr is None or pstr[0] == '\0': continue - if clobber_count < ASM_MAX_CLOBBERS: - pcl_addr: uint64 = uint64(clobbers) + clobber_count * 8 - pcl_ptr: str* = (str*)(pcl_addr) - pcl_ptr[0] = pstr - clobber_count += 1 + if clobber_count >= ASM_MAX_CLOBBERS: + self.Gen.TypeGenErr.fatal_error(pelt, f"inline asm has more than {ASM_MAX_CLOBBERS} clobbers (compiler limit); remove duplicates or split the asm block", "TypeError") + return None + pcl_addr: uint64 = uint64(clobbers) + clobber_count * 8 + pcl_ptr: str* = (str*)(pcl_addr) + pcl_ptr[0] = pstr + clobber_count += 1 # 4. Count outputs/inputs and determine the return type output_count: int = 0 @@ -1106,8 +1134,12 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): cn4: ast.Constant* = ast.Constant*(tmpl_node) if cn4.const_kind == ast.CONST_STR and cn4.str_val is not None: self._asm_append_cstr(asm_text, ASM_TEXT_BUF_SIZE, cn4.str_val) - # 5. Build the constraint string in reordered order (outputs first) - const_buf: str = pool.alloc(ASM_CONST_BUF_SIZE) + # 5. Build the constraint string in reordered order (outputs first). + # Capacity scales with the operand/clobber count: a 32-entry clobber list alone is + # ~300 bytes, so the old fixed 512-byte cap could silently cut it. Overflow of this + # (or any asm text buffer) is fail-fast in _asm_append_cstr. + const_cap: sizet = ASM_CONST_BUF_SIZE + sizet(operand_count + clobber_count + 1) * 64 + const_buf: str = pool.alloc(const_cap) if const_buf is None: return None const_buf[0] = '\0' @@ -1118,29 +1150,31 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): cons_val: str = cop.Constraint if cons_val is None: cons_val = "r" - if first_const == 0: self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, ",") + if first_const == 0: self._asm_append_cstr(const_buf, const_cap, ",") first_const = 0 if cop.IsOutput != 0: # Output constraint: ensure it starts with = - if cons_val[0] != '=': self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, "=") - self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, cons_val) - else: self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, cons_val) + if cons_val[0] != '=': self._asm_append_cstr(const_buf, const_cap, "=") + self._asm_append_cstr(const_buf, const_cap, cons_val) + else: self._asm_append_cstr(const_buf, const_cap, cons_val) # Append clobber constraints ~{name} for cidx in range(int(clobber_count)): cl_addr2: uint64 = uint64(clobbers) + cidx * 8 cname: str = (str*)(cl_addr2)[0] if cname is None or cname[0] == '\0': continue - if first_const == 0: self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, ",") + if first_const == 0: self._asm_append_cstr(const_buf, const_cap, ",") first_const = 0 - self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, "~{") - self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, cname) - self._asm_append_cstr(const_buf, ASM_CONST_BUF_SIZE, "}") + self._asm_append_cstr(const_buf, const_cap, "~{") + self._asm_append_cstr(const_buf, const_cap, cname) + self._asm_append_cstr(const_buf, const_cap, "}") - # 6. Build the argument list text (input operands only): each arg is type text (an - # inlined named struct can reach 512 bytes) plus the value name, so scale the buffer. - asm_arg_cap: sizet = sizet(operand_count + 1) * 544 + 64 + # 6. Build the argument list text (input operands only): each arg is type text + # (TypePrint is given a 4096-byte buffer below) plus the value name, so size the + # buffer from that worst case instead of underestimating it (544 bytes/operand + # silently cut wide inlined named structs). + asm_arg_cap: sizet = sizet(operand_count + 1) * 4224 + 64 args_buf: str = pool.alloc(asm_arg_cap) if args_buf is None: return None args_buf[0] = '\0' @@ -1215,12 +1249,17 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): # Multi-output: extractvalue index out_emit_idx, then store if oo.Value is not None and oo.Value.Ty is not None and oo.Value.Ty.Pointee != 0: ev_name: str = pool.alloc(32) - ev_line: str = pool.alloc(ASM_LINE_BUF_SIZE) + # Size the line from its actual parts: ret_ty_buf alone can be ~4096 + # bytes, so the old fixed cap could cut the instruction in half. + ev_cap: sizet = ASM_LINE_BUF_SIZE + 64 + if ret_ty_buf is not None: ev_cap += string.strlen(ret_ty_buf) + if ssa_name is not None: ev_cap += string.strlen(ssa_name) + ev_line: str = pool.alloc(ev_cap) if ev_name is not None and ev_line is not None: # ssa_name is "%N"; build a unique "%asm.N.e{i}" (letter- # initial) so %N.e{i} is not parsed as id plus a bad suffix. viperlib.snprintf(ev_name, 32, "%%asm.%s.e%d", ssa_name + 1, out_emit_idx) - viperlib.snprintf(ev_line, ASM_LINE_BUF_SIZE, "%s = extractvalue %s %s, %d", ev_name, ret_ty_buf, ssa_name, out_emit_idx) + viperlib.snprintf(ev_line, ev_cap, "%s = extractvalue %s %s, %d", ev_name, ret_ty_buf, ssa_name, out_emit_idx) mlirlite.block_append_text(pool, builder.CurBlock, ev_line) ev_val: mlirlite.Value* = mlirlite.SSAValue(pool, oo.Value.Ty.Pointee, ev_name) mlirlite.build_store(builder, ev_val, oo.Value) @@ -1271,7 +1310,7 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): if pk == ast.ASTKind.Constant: # String literal fragment cn: ast.Constant* = ast.Constant*(part) - if cn.str_val is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn.str_val) + if cn.str_val is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn.str_val, "c.LLVMIR") elif pk == ast.ASTKind.FormattedValue: fv: ast.FormattedValue* = ast.FormattedValue*(part) @@ -1289,7 +1328,7 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): if string.strcmp(attr_nm, "LInp") == 0: # Input operand: translate to an LLVM value reference text (%x or constant) inp_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, None, 0, trans) - if inp_val is not None and inp_val.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, inp_val.Name) + if inp_val is not None and inp_val.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, inp_val.Name, "c.LLVMIR") elif string.strcmp(attr_nm, "LOut") == 0: # Output operand: allocate a unique SSA register @@ -1303,21 +1342,21 @@ novirtual class CallGenCBuiltin(CallGenBase.CallGenBase): reg_buf: str = pool.alloc(32) if reg_buf is not None: viperlib.snprintf(reg_buf, 32, "%%llvmir_%d", self.Gen.CallGenBase.LLVMIRRegSeq) - self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, reg_buf) + self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, reg_buf, "c.LLVMIR") out_alloca[out_count] = sizet(alloca) out_regname[out_count] = sizet(reg_buf) out_count += 1 continue # Non-LInp/LOut f-string expression: translate and embed the value text ival: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, fexpr, None, 0, trans) - if ival is not None and ival.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, ival.Name) + if ival is not None and ival.Name is not None: self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, ival.Name, "c.LLVMIR") elif tk == ast.ASTKind.Constant: cn2: ast.Constant* = ast.Constant*(first_arg) if cn2.str_val is not None: # A bare strcpy is unbounded and could overflow the pool block, so # use the same bounded append as the JoinedStr branch. - self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn2.str_val) + self._asm_append_cstr(text, LLVMIR_TEMPLATE_BUF, cn2.str_val, "c.LLVMIR") # 2. Embed the template text directly (no instruction parsing) + LOut writeback mlirlite.block_append_text(pool, builder.CurBlock, text)