Files
ViperC/App/lib/core/Gen/Expr/ExprGenFstr.vp
T

407 lines
22 KiB
Plaintext

from stdint import *
import ast
import mlirlite
import memhub
import string
import lib.core.Gen.Translator as HT
import lib.core.Gen.Expr.ExprGenBase as ExprGenBase
# f-string translation constants (local copy of ExprGenMixin's to avoid a cross-module
# CDefine lookup bug: ExprGenFstr compiles before ExprGenMixin, so the table is unregistered).
define FSTR_BUF_SIZE = 1024
define FSTR_SPEC_MAX = 64
define FSTR_FMT_MAX = 64
# ExprGenFstr - ExprGenMixin split (stage D): f-string/formatted-value translation
novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
"""f-string and formatted-value translation."""
# translate_joined_str - f-string translation (aligned with TPC _HandleJoinedStrLlvm):
# join literals and format specs, snprintf into a per-site i8* stack buffer (no escape).
def translate_joined_str(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Lower an f-string to snprintf into a per-site i8* stack buffer."""
if node is None or builder is None or trans is None: return None
js: ast.JoinedStr* = ast.JoinedStr*(node)
if js is None or js.values is None: return None
parts: list[ast.AST*]* = js.values
pn: sizet = len(parts)
# Size the format buffer from the actual parts: each literal may double ('%' -> '%%') and
# each conversion contributes a short spec. A fixed 1024 silently dropped trailing
# literals/converters while their arguments were still appended -> wrong runtime output.
fmt_cap: sizet = 16
for pi0 in range(int(pn)):
part0: ast.AST* = parts.get(pi0)
if part0 is None: continue
if part0.kind() == ast.ASTKind.Constant:
pc0: ast.Constant* = ast.Constant*(part0)
if pc0.const_kind == ast.CONST_STR and pc0.str_val is not None:
fmt_cap = fmt_cap + string.strlen(pc0.str_val) * 2
elif part0.kind() == ast.ASTKind.FormattedValue:
fmt_cap = fmt_cap + FSTR_SPEC_MAX + 16
# Compile-time format string buffer (pool-allocated)
fmt_buf: str = pool.alloc(fmt_cap)
if fmt_buf is None: return None
fmt_buf[0] = '\0'
# Runtime argument list (linked by Value.Next, matching the printf call)
arg_head: mlirlite.Value* = None
arg_tail: mlirlite.Value* = None
arg_count: int = 0
for pi in range(int(pn)):
part: ast.AST* = parts.get(pi)
if part is None: continue
if part.kind() == ast.ASTKind.Constant:
pcn: ast.Constant* = ast.Constant*(part)
if pcn.const_kind == ast.CONST_STR and pcn.str_val is not None:
self._fstr_append_literal(fmt_buf, fmt_cap, pcn.str_val)
continue
if part.kind() != ast.ASTKind.FormattedValue: continue
fv: ast.FormattedValue* = ast.FormattedValue*(part)
if fv.value is None: continue
val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, fv.value, None, 0, trans)
if val is None:
# fail-fast: a failed formatted-value translation must abort compilation
self.Gen.TypeGenErr.fatal_error(fv.value, "f-string formatted value translation failed (expression returned None)", "TypeError")
continue
# Object defines __str__ and no format is given -> use its returned string (like TPC)
if fv.format_spec is None:
str_ov_fs: mlirlite.Value* = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, val, "__str__", trans)
if str_ov_fs is not None: val = str_ov_fs
# Determine the C format specifier (format_spec first, else infer from the value type)
c_fmt: str = None
if fv.format_spec is not None: c_fmt = self._fstr_parse_format_spec(pool, fv.format_spec, val, fv.value)
if c_fmt is None: c_fmt = self._fstr_infer_c_fmt(pool, val, fv.value, trans)
if c_fmt is None:
# fail-fast: cannot infer the f-string format (missing value type); abort
self.Gen.TypeGenErr.fatal_error(fv.value, "f-string format cannot be inferred (value type missing)", "TypeError")
# Append the format specifier
self._fstr_append_raw(fmt_buf, fmt_cap, c_fmt)
# Convert the argument type (i8/i16->i32, bool->i32, f32->f64)
cast_val: mlirlite.Value* = self._fstr_cast_value(builder, pool, val, c_fmt, fv.value, trans)
if cast_val is None: cast_val = val
cast_val.Next = None
if arg_head is None: arg_head = cast_val
else: arg_tail.Next = cast_val
arg_tail = cast_val
arg_count += 1
return self._fstr_finalize(builder, pool, mod, fmt_buf, arg_head, arg_tail, arg_count, trans)
def translate_formatted_value(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a bare FormattedValue without any literal parts."""
if node is None or builder is None or trans is None: return None
fv: ast.FormattedValue* = ast.FormattedValue*(node)
if fv is None or fv.value is None: return None
# A bare FormattedValue contributes exactly one conversion spec (bounded by FSTR_SPEC_MAX
# plus the C prefix/width); size the buffer from that instead of the fixed 1024.
fmt_cap: sizet = FSTR_SPEC_MAX + 64
fmt_buf: str = pool.alloc(fmt_cap)
if fmt_buf is None: return None
fmt_buf[0] = '\0'
arg_head: mlirlite.Value* = None
arg_tail: mlirlite.Value* = None
arg_count: int = 0
val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, fv.value, None, 0, trans)
if val is None:
# fail-fast: a failed bare FormattedValue translation must abort compilation
self.Gen.TypeGenErr.fatal_error(fv.value, "f-string formatted value translation failed (expression returned None)", "TypeError")
else:
# Object defines __str__ and no format is given -> use its returned string (like TPC)
if fv.format_spec is None:
str_ov_sf: mlirlite.Value* = self.Gen.ExprGenOps._try_named_dunder(pool, builder, mod, val, "__str__", trans)
if str_ov_sf is not None: val = str_ov_sf
c_fmt: str = None
if fv.format_spec is not None: c_fmt = self._fstr_parse_format_spec(pool, fv.format_spec, val, fv.value)
if c_fmt is None: c_fmt = self._fstr_infer_c_fmt(pool, val, fv.value, trans)
if c_fmt is None:
# fail-fast: cannot infer the f-string format (missing value type); abort
self.Gen.TypeGenErr.fatal_error(fv.value, "f-string format cannot be inferred (value type missing)", "TypeError")
self._fstr_append_raw(fmt_buf, fmt_cap, c_fmt)
cast_val: mlirlite.Value* = self._fstr_cast_value(builder, pool, val, c_fmt, fv.value, trans)
if cast_val is None: cast_val = val
cast_val.Next = None
arg_head = cast_val
arg_tail = cast_val
arg_count = 1
return self._fstr_finalize(builder, pool, mod, fmt_buf, arg_head, arg_tail, arg_count, trans)
def _fstr_finalize(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, fmt_buf: str, arg_head: mlirlite.Value*, arg_tail: mlirlite.Value*, arg_count: int, trans: HT.Translator*) -> mlirlite.Value*:
"""Emit the final snprintf over the global format and per-site buffer."""
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
if fmt_buf is None or fmt_buf[0] == '\0':
# Empty f-string (f"") -> empty string constant
return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, "", trans)
# Format string -> global string constant (i8*)
fmt_ptr: mlirlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, mod, fmt_buf, trans)
if fmt_ptr is None: return None
# Result buffer: a per-site entry alloca (thread-safe, no runtime dependency), so the
# f-string result is a function-local temporary that must not escape; copy to escape.
buf_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty, FSTR_BUF_SIZE)
buf_alloca: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, buf_ty)
if buf_alloca is None: return None
zero_i32: mlirlite.Value* = mlirlite.const_int32(pool, 0)
buf_ptr: mlirlite.Value* = mlirlite.build_gep_array(builder, buf_ty, i8_ty, buf_alloca, zero_i32)
if buf_ptr is None: return None
# f-string lowers to snprintf, declared in includes/stdio.vp. If the source imports no
# module declaring it, add the declare here (implicit-provide); i8*, i64, i8* + varargs.
if self.Gen.CallGenBase.find_func_in_module(mod, "snprintf") is None:
snprintf_decl: mlirlite.Function* = mlirlite.create_declare(pool, mod, "snprintf", i32_ty)
if snprintf_decl is not None:
snprintf_decl.IsVarArg = 1
# buf/fmt are pointer params (i8*), not the element type i8: a bare i8 declare
# would cause an MLIR 'llvm.call' operand type mismatch ('!llvm.ptr' != 'i8').
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
mlirlite.add_param(pool, snprintf_decl, i8_ptr_ty, "buf")
mlirlite.add_param(pool, snprintf_decl, mlirlite.Int64(pool), "size")
mlirlite.add_param(pool, snprintf_decl, i8_ptr_ty, "fmt")
# snprintf(buf, FSTR_BUF_SIZE, fmt, args...)
size_val: mlirlite.Value* = mlirlite.const_int64(pool, FSTR_BUF_SIZE)
buf_ptr.Next = size_val
size_val.Next = fmt_ptr
fmt_ptr.Next = arg_head
if arg_tail is not None: arg_tail.Next = None
mlirlite.build_call(builder, "snprintf", buf_ptr, 3 + arg_count, i32_ty, 1)
return buf_ptr
def _fstr_append_literal(self, fmt_buf: str, cap: sizet, lit: str) -> None:
"""Append a literal to the format string, escaping percent signs; fail-fast on overflow."""
if fmt_buf is None or lit is None: return
cur_len: sizet = string.strlen(fmt_buf)
src_len: sizet = string.strlen(lit)
# Worst case: every character expands to "%%".
if cur_len + src_len * 2 + 1 > cap:
self.Gen.TypeGenErr.fatal_error(None, f"f-string format text exceeds its {cap}-byte buffer ({cur_len} used + up to {src_len * 2} escaped + 1 NUL); shorten the literal", "OverflowError")
si: sizet = 0
while si < src_len:
ch: char = lit[si]
if ch == '%':
fmt_buf[cur_len] = '%'
fmt_buf[cur_len + 1] = '%'
cur_len += 2
else:
fmt_buf[cur_len] = ch
cur_len += 1
si += 1
fmt_buf[cur_len] = '\0'
def _fstr_append_raw(self, fmt_buf: str, cap: sizet, s: str) -> None:
"""Append a raw string without percent escaping; fail-fast on overflow."""
if fmt_buf is None or s is None: return
cur_len: sizet = string.strlen(fmt_buf)
src_len: sizet = string.strlen(s)
if cur_len + src_len + 1 > cap:
self.Gen.TypeGenErr.fatal_error(None, f"f-string format text exceeds its {cap}-byte buffer ({cur_len} used + {src_len} to append + 1 NUL); shorten the format spec", "OverflowError")
i: sizet = 0
while i < src_len:
fmt_buf[cur_len + i] = s[i]
i += 1
fmt_buf[cur_len + src_len] = '\0'
def _fstr_parse_format_spec(self, pool: memhub.MemBuddy*, spec_node: ast.AST*, val: mlirlite.Value*, arg_node: ast.AST*) -> str:
"""Convert a Python format_spec into a C printf format string."""
if spec_node is None or pool is None or val is None: return None
# 1. Extract the spec string (JoinedStr with constants only)
spec: str = pool.alloc(FSTR_SPEC_MAX)
if spec is None: return None
spec[0] = '\0'
sk: int = spec_node.kind()
if sk == ast.ASTKind.JoinedStr:
jss: ast.JoinedStr* = ast.JoinedStr*(spec_node)
if jss.values is None: return None
vn: sizet = len(jss.values)
for vi in range(int(vn)):
vpart: ast.AST* = jss.values.get(vi)
if vpart is None: continue
if vpart.kind() != ast.ASTKind.Constant: return None
vcn: ast.Constant* = ast.Constant*(vpart)
if vcn.const_kind != ast.CONST_STR or vcn.str_val is None: return None
# fail-fast: a truncated spec can still look valid (e.g. ">10.5f" cut to ">10."),
# which silently changes the output format.
if string.strlen(spec) + string.strlen(vcn.str_val) >= FSTR_SPEC_MAX - 1:
self.Gen.TypeGenErr.fatal_error(arg_node, f"f-string format spec exceeds {FSTR_SPEC_MAX - 1} characters; shorten it", "OverflowError")
string.strcat(spec, vcn.str_val)
elif sk == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(spec_node)
if cn.const_kind != ast.CONST_STR or cn.str_val is None: return None
if string.strlen(cn.str_val) >= FSTR_SPEC_MAX - 1:
self.Gen.TypeGenErr.fatal_error(arg_node, f"f-string format spec exceeds {FSTR_SPEC_MAX - 1} characters; shorten it", "OverflowError")
string.strncpy(spec, cn.str_val, FSTR_SPEC_MAX - 1)
else: return None
slen: sizet = string.strlen(spec)
if slen == 0: return None
# 2. Parse segment by segment: [fill]align / sign / # / 0 / width / , / .precision / type
pos: sizet = 0
align_char: char = 0
if pos < slen:
ch0: char = spec[pos]
if ch0 == '<' or ch0 == '>' or ch0 == '^' or ch0 == '=':
align_char = ch0
pos += 1
elif pos + 1 < slen:
ch1: char = spec[pos + 1]
if ch1 == '<' or ch1 == '>' or ch1 == '^' or ch1 == '=':
align_char = ch1
pos += 2
c_flags: str = pool.alloc(FSTR_FMT_MAX)
c_width: str = pool.alloc(32)
c_prec: str = pool.alloc(32)
if c_flags is None or c_width is None or c_prec is None: return None
c_flags[0] = '\0'
c_width[0] = '\0'
c_prec[0] = '\0'
# sign
if pos < slen and (spec[pos] == '+' or spec[pos] == '-' or spec[pos] == ' '):
if spec[pos] == '+': string.strcat(c_flags, "+")
elif spec[pos] == ' ': string.strcat(c_flags, " ")
pos += 1
# alt '#'
if pos < slen and spec[pos] == '#':
string.strcat(c_flags, "#")
pos += 1
# zero '0'
if pos < slen and spec[pos] == '0':
string.strcat(c_flags, "0")
pos += 1
# Left align '<'/'^' -> '-' (C semantics give '-' priority over '0')
if align_char == '<' or align_char == '^': string.strcat(c_flags, "-")
# width digits
while pos < slen and spec[pos] >= '0' and spec[pos] <= '9':
wl: sizet = string.strlen(c_width)
if wl < 30:
c_width[wl] = spec[pos]
c_width[wl + 1] = '\0'
pos += 1
# comma ',' -> C printf has no thousands separator, ignore
if pos < slen and spec[pos] == ',': pos += 1
# .precision
if pos < slen and spec[pos] == '.':
string.strcat(c_prec, ".")
pos += 1
while pos < slen and spec[pos] >= '0' and spec[pos] <= '9':
pl: sizet = string.strlen(c_prec)
if pl < 30:
c_prec[pl] = spec[pos]
c_prec[pl + 1] = '\0'
pos += 1
if string.strlen(c_prec) == 1:
return None # a lone '.' is invalid
# type char
fmt_type: char = 0
if pos < slen:
fmt_type = spec[pos]
pos += 1
if pos != slen: return None # extra characters cannot be parsed
# 3. Length modifier (by value type bit width)
len_mod: str = pool.alloc(4)
if len_mod is None: return None
len_mod[0] = '\0'
ibits: int = self.Gen.ExprGenBase.get_llvm_type_bits(val.Ty)
# Do not write a length modifier for 32-bit (N14): `{x:08X}` passes an i32, but `l`
# would make %08lX (long) mismatch on LP64 and misread the varargs.
if ibits == 64: string.strcpy(len_mod, "ll")
elif ibits == 16: string.strcpy(len_mod, "h")
elif ibits == 8: string.strcpy(len_mod, "hh")
# 4. Assemble the C format string
out: str = pool.alloc(FSTR_FMT_MAX)
if out is None: return None
out[0] = '\0'
string.strcat(out, "%")
string.strcat(out, c_flags)
string.strcat(out, c_width)
string.strcat(out, c_prec)
if fmt_type == 0:
# No type: infer from the value type
fbits2: int = self.Gen.ExprGenBase.get_llvm_float_bits(val.Ty)
if ibits != 0:
if ibits == 8: string.strcat(out, "c")
elif ibits == 1: string.strcat(out, "d")
else:
string.strcat(out, len_mod)
string.strcat(out, "d")
elif fbits2 != 0: string.strcat(out, "f")
elif self.Gen.ExprGenBase.is_ptr_type(val.Ty) != 0: string.strcat(out, "s")
else: string.strcat(out, "d")
elif fmt_type == 'c': string.strcat(out, "c")
elif fmt_type == 'f' or fmt_type == 'F': string.strcat(out, "f")
elif fmt_type == 'e' or fmt_type == 'E' or fmt_type == 'g' or fmt_type == 'G':
tmp_ch: str = pool.alloc(2)
if tmp_ch is None: return None
tmp_ch[0] = fmt_type
tmp_ch[1] = '\0'
string.strcat(out, tmp_ch)
elif fmt_type == 's': string.strcat(out, "s")
elif fmt_type == '%': string.strcat(out, "%")
elif fmt_type == 'b':
return None # binary is unsupported
elif fmt_type == 'd' or fmt_type == 'i':
string.strcat(out, len_mod)
string.strcat(out, "d")
elif fmt_type == 'x' or fmt_type == 'X' or fmt_type == 'o' or fmt_type == 'u':
string.strcat(out, len_mod)
tmp_ch2: str = pool.alloc(2)
if tmp_ch2 is None: return None
tmp_ch2[0] = fmt_type
tmp_ch2[1] = '\0'
string.strcat(out, tmp_ch2)
else: return None
return out
def _fstr_infer_c_fmt(self, pool: memhub.MemBuddy*, val: mlirlite.Value*, arg_node: ast.AST*, trans: HT.Translator*) -> str:
"""Infer the C printf format from the value type when no format_spec is given."""
if val is None or pool is None: return None
out: str = pool.alloc(16)
if out is None: return None
out[0] = '\0'
ibits: int = self.Gen.ExprGenBase.get_llvm_type_bits(val.Ty)
fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(val.Ty)
if ibits == 1: return "%d" # bool
if ibits == 8: return "%c" # character
if ibits != 0:
if self.Gen.ExprGenOps._node_type_unsigned(pool, arg_node, trans) != 0:
if ibits == 64: string.strcpy(out, "%llu")
else: string.strcpy(out, "%u")
else:
if ibits == 64: string.strcpy(out, "%lld")
else: string.strcpy(out, "%d")
return out
if fbits != 0: return "%f"
if self.Gen.ExprGenBase.is_ptr_type(val.Ty) != 0: return "%s"
# fail-fast: missing value type (not int/float/pointer); cannot infer the C format
self.Gen.TypeGenErr.fatal_error(arg_node, "f-string format cannot be inferred (value type missing)", "TypeError")
return None
def _fstr_cast_value(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, val: mlirlite.Value*, c_fmt: str, arg_node: ast.AST*, trans: HT.Translator*) -> mlirlite.Value*:
"""Cast the value to the type required by the C format specifier."""
if val is None or c_fmt is None: return None
# Find the format type char (last letter, ignoring l/h length modifiers)
fmt_char: char = 0
flen: sizet = string.strlen(c_fmt)
fi: sizet = flen
while fi > 0:
fi -= 1
ch: char = c_fmt[fi]
if ch == '%': break
if (ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z'):
if ch != 'l' and ch != 'h':
fmt_char = ch
break
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
if fmt_char == 's': return val
if fmt_char == 'c':
ibits: int = self.Gen.ExprGenBase.get_llvm_type_bits(val.Ty)
if ibits != 0 and ibits < 32: return mlirlite.build_zext(builder, val, i32_ty)
return val
if fmt_char == 'd' or fmt_char == 'i' or fmt_char == 'x' or fmt_char == 'X' or fmt_char == 'o' or fmt_char == 'u':
ibits = self.Gen.ExprGenBase.get_llvm_type_bits(val.Ty)
if ibits == 1: return mlirlite.build_zext(builder, val, i32_ty)
if ibits == 8 or ibits == 16:
if self.Gen.ExprGenOps._node_type_unsigned(pool, arg_node, trans) != 0: return mlirlite.build_zext(builder, val, i32_ty)
return mlirlite.build_sext(builder, val, i32_ty)
return val
if fmt_char == 'f' or fmt_char == 'e' or fmt_char == 'E' or fmt_char == 'g' or fmt_char == 'G':
fbits: int = self.Gen.ExprGenBase.get_llvm_float_bits(val.Ty)
if fbits == 32: return mlirlite.build_fpext(builder, val, mlirlite.Double(pool))
return val
return val