fix(asm): 内联汇编/LLVMIR 文本缓冲扩容并将所有超限改为 fail-fast

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 行缓冲改为按实际操作数与类型文本长度动态分配。
This commit is contained in:
TermiNexus committed 2026-10-17 17:41:11 +08:00
1 parent 201c881b43
commit 15952d3852
1 file changed
+80 -41
+80 -41
View File
@@ -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)