feat(lang): 新增 cif/celif/celse 条件编译与值引用注解 &T/&temp T,assert 改译 testcheck.check

cif 三件套:StmtGenCifMixin 负责常量求值(含 defined(模块宏) 的 Attribute 形式),链式错配与含运行时变量一律 fail-fast;删掉 c.CIf 家族。值引用:TypeGenResolve 让 CRef/CTempRef 与 t.CPtr 走同一解析路径(&void 折叠为 i8*,删除 t.CNeedPtr),CallGenBase 在调用点按形参模式自动取址(ref 只接受左值,右值 fail-fast;temp 物化临时栈),跨模块经 VpiReader/StubMergerBuild 传递模式。assert:StmtGenMixin 合成 testcheck.check(cond, None, msg),Translator 检测到 assert 时自动注入 testcheck 导入(带去重与防环),深递归守卫改用 stdio.printf + stdlib.exit(Translator 无 Gen 成员)。
This commit is contained in:
TermiNexus committed 2026-10-07 19:03:16 +08:00
1 parent ce50daa87a
commit 2fb648d68a
22 files changed
+556 -85

No files matched your search

+9 -2
View File
@@ -129,7 +129,7 @@ def _RewriteDepsTxt(temp_dir: str, sha1: str, deps_str: str) -> None:
def _BreakContainerCyclesInDeps(temp_dir: str) -> int:
"""Remove _list/_dict injection cycle edges after scanning and sync deps.txt"""
"""Remove _list/_dict/testcheck injection cycle edges after scanning and sync deps.txt"""
if StubMerger._includes_deps_sha1 is None or StubMerger._includes_deps_str is None: return 0
cnt: sizet = StubMerger._includes_deps_count
ci: sizet
@@ -137,7 +137,7 @@ def _BreakContainerCyclesInDeps(temp_dir: str) -> int:
sha1_m: str = StubMerger._includes_deps_sha1 + sizet(ci) * 17
deps_m: str = StubMerger._includes_deps_str + sizet(ci) * StubMerger.MAX_DEPS_STR_LEN
if sha1_m is None or deps_m is None: continue
if string.strstr(deps_m, "_list") is None and string.strstr(deps_m, "_dict") is None: continue
if string.strstr(deps_m, "_list") is None and string.strstr(deps_m, "_dict") is None and string.strstr(deps_m, "testcheck") is None: continue
# Module name (basename of RelPath without extension)
rel_m: str = StubMergerStore.LookupSha1RelPath(sha1_m)
if rel_m is None: continue
@@ -173,6 +173,13 @@ def _BreakContainerCyclesInDeps(temp_dir: str) -> int:
dct_deps: str = StubMergerStore.LookupIncludesDeps(dct_sha1)
if dct_deps is not None and _DepsContainsName(dct_deps, nm_m) != 0:
if _DepsRemoveName(deps_m, "_dict") != 0: _RewriteDepsTxt(temp_dir, sha1_m, deps_m)
# testcheck <-> M cycle (assert auto-import)
if string.strstr(deps_m, "testcheck") is not None:
tc_sha1: str = StubMergerStore.LookupSha1ByModName("testcheck")
if tc_sha1 is not None:
tc_deps: str = StubMergerStore.LookupIncludesDeps(tc_sha1)
if tc_deps is not None and _DepsContainsName(tc_deps, nm_m) != 0:
if _DepsRemoveName(deps_m, "testcheck") != 0: _RewriteDepsTxt(temp_dir, sha1_m, deps_m)
stdlib.free(nm_m)
return 0
+40 -2
View File
@@ -850,6 +850,41 @@ def _vpi_param_annot_seg_is(src_sha1: str, cls_name: str, method_name: str, seg_
return 1
# _vpi_param_annot_seg_mode - value-reference mode of a param-annotation segment: 0=none / 1=ref / 2=temp.
# The definition side writes `T | t.CRef` / `T | t.CTempRef`; the call side mirrors it (auto-address).
def _vpi_param_annot_seg_mode(prm_annots: str, seg_idx: int) -> int:
"""Return the value-reference mode encoded in a vpi param-annotation segment."""
if prm_annots is None or seg_idx < 0: return 0
cur: sizet = 0
for seg in range(seg_idx):
while prm_annots[cur] != '\0' and prm_annots[cur] != '@': cur += 1
if prm_annots[cur] == '\0': return 0
cur += 1
slen: sizet = 0
while prm_annots[cur + slen] != '\0' and prm_annots[cur + slen] != '@': slen += 1
if slen == 0: return 0
seg_buf: bytes = stdlib.malloc(slen + 1)
if seg_buf is None: return 0
string.strncpy(seg_buf, prm_annots + cur, slen)
seg_buf[slen] = '\0'
mode: int = 0
if string.strstr(seg_buf, "CTempRef") is not None: mode = 2
elif string.strstr(seg_buf, "CRef") is not None: mode = 1
elif seg_buf[0] == '&':
# The stub may carry the union marker (`T | t.CRef`, written by this compiler's StubGen) or
# the source syntax (`&T` / `&temp T`, written by TPC's StubGen). Accept both, else a
# cross-module value-reference call would silently lose its auto-address.
q: sizet = 0
while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1
if seg_buf[q] == '&':
q += 1
while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1
if slen - q >= 4 and string.strncmp(seg_buf + q, "temp", 4) == 0: mode = 2
else: mode = 1
stdlib.free(seg_buf)
return mode
# _ensure_vpi_param_annot_loaded - lazily load a module's vpi stub param-annotation cache.
# Mirror of the return variant; reads the .vpi file on miss, idempotent.
def _ensure_vpi_param_annot_loaded(src_sha1: str, temp_dir: str) -> int:
@@ -1192,6 +1227,7 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m
return mlirlite.Void(pool)
has_cptr: int = 0
has_cref: int = 0
has_cvoid: int = 0
first_ty: mlirlite.LLVMType* = None
# Attributes of the segment that produced first_ty (for the has_cptr branch, N12):
@@ -1252,7 +1288,9 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m
if lvl_cs > cptr_sub_lvl:
cptr_sub_lvl = lvl_cs
cptr_sub_ty = seg_ty_cs
elif string.strcmp(seg_buf, "t.CPtr") == 0 or string.strcmp(seg_buf, "CPtr") == 0: has_cptr = 1
elif string.strcmp(seg_buf, "t.CPtr") == 0 or string.strcmp(seg_buf, "CPtr") == 0: has_cptr = 1
elif string.strcmp(seg_buf, "t.CRef") == 0 or string.strcmp(seg_buf, "CRef") == 0: has_cref = 1
elif string.strcmp(seg_buf, "t.CTempRef") == 0 or string.strcmp(seg_buf, "CTempRef") == 0: has_cref = 1
elif string.strcmp(seg_buf, "void") == 0 or string.strcmp(seg_buf, "CVoid") == 0 or string.strcmp(seg_buf, "void") == 0:
# Bare void is an alias of t.CVoid and must be handled the same, else the
# else branch sets first_td and produces the illegal Ptr(Void) (void* = i8*).
@@ -1274,7 +1312,7 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m
stdlib.free(seg_buf)
pos += 1 # skip '|'
if has_cptr != 0:
if has_cptr != 0 or has_cref != 0:
if has_cvoid != 0: return i8_ptr_ty
if first_ty is not None:
# The result itself is void (e.g. an opaque typedef resolving to void): CPtr merge
@@ -132,6 +132,8 @@ def _BuildReachableSha1Set(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str,
if ret_a == 0:
# Container use detection: inject _list/_dict deps when an App file uses list/dict (bare annotation/ctor)
tr._inject_auto_container_visibility(mb, tree)
# assert lowers to testcheck.check(...): inject the testcheck dep when used
tr._inject_auto_assert_import(mb, tree)
# Register the App file in the store (empty incdir) and fill deps; read by
# LookupIncludesDeps(app_sha1) when BFS expands its includes deps
if e_r.Sha1 is not None:
@@ -440,6 +442,8 @@ def _BuildReachableSha1Set(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str,
# Inject containers on demand (append _list/_dict when includes use list/dict generics);
# inlined to break the _list/_dict cycle
tr_r2._inject_auto_container_visibility(mb, tree_r)
# assert lowers to testcheck.check(...): inject the testcheck dep when used
tr_r2._inject_auto_assert_import(mb, tree_r)
# Fill in-memory deps and write deps.txt (for dep-graph expansion and on-demand Phase B loading)
deps_r2: str = tr_r2._imported_modules
+4
View File
@@ -1290,6 +1290,8 @@ novirtual class AssignGenMixin:
var_ty: mlirlite.LLVMType* = None
annot: ast.AST* = aa.annotation
if annot is not None:
# Value-reference markers are parameter-only: a local variable `x: &T` fails fast.
self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "a local variable declaration")
# Specialize generics before resolving so list[X] does not degrade to opaque list*
self._prespecialize_generic_annotation(trans, pool, annot)
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0)
@@ -1429,6 +1431,8 @@ novirtual class AssignGenMixin:
var_ty: mlirlite.LLVMType* = None
annot: ast.AST* = aa.annotation
if annot is not None:
# Value-reference markers are parameter-only: a local variable `x: &T` fails fast.
self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "a local variable declaration")
# Same as PreScan: specialize generics first so list[X] does not degrade to bare list*
self._prespecialize_generic_annotation(trans, pool, annot)
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, annot, trans._imported_modules, trans._from_imports, trans, 0)
+8
View File
@@ -976,6 +976,14 @@ novirtual class AsyncGenMixin(CallGenBase.CallGenBase):
def _handle_coro_if(self, trans: HT.Translator*, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, stmt: ast.AST*):
ifn: ast.If* = ast.If*(stmt)
if ifn is None or ifn.test is None: return
# cif/celif (internal c.@CIf marker): compile-time prune, then translate only the taken branch
# so awaits inside it still suspend in place. The marker must never be lowered as a runtime value.
if self.Gen.StmtGenCif._is_cif_call(ifn.test) == 1:
if self.Gen.StmtGenCif._prune_cif_test(pool, trans, ifn.test) != 0:
self._handle_segment(trans, builder, pool, mod, ifn.children)
else:
self._handle_segment(trans, builder, pool, mod, ifn.orelse)
return
if self._find_expr_await(ifn.test) is not None:
err_b: str = pool.alloc(256)
if err_b is not None:
+74
View File
@@ -97,11 +97,85 @@ novirtual class CallGenBase:
# _LookupMethodRetTyFromTextLL); key = full mangled name, value = text only.
MethodRetTyMap: hashtable.HashTable* = None
# --- parameter auto-address modes for value-reference params (`&T` / `&temp T`) ---
# key = callee symbol name -> byte-per-parameter mode string: 0=none, '1'=ref, '2'=temp
# (PermPool-persistent; index = emitted parameter index, self included for methods).
AutoAddrModes: hashtable.HashTable* = None
def InitCallGen(self) -> int:
"""Initialize the call mixin fields (inlined by subclass constructors since NoVTable does not forward inherited symbols)"""
if self is None: return 1
return 0
# _register_auto_addr_modes - store per-parameter value-reference modes for a callee symbol name.
def _register_auto_addr_modes(self, name: str, modes: str, count: int) -> int:
"""Store per-parameter auto-address modes (PermPool-persistent) keyed by callee symbol name."""
if name is None or modes is None or count <= 0: return 0
perm: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if perm is None: return 0
if self.AutoAddrModes is None:
self.AutoAddrModes = hashtable.HashTable(perm)
if self.AutoAddrModes is None: return 0
key: str = perm.alloc(string.strlen(name) + 1)
if key is None: return 0
string.strcpy(key, name)
val: str = perm.alloc(sizet(count) + 1)
if val is None: return 0
string.memcpy(val, modes, sizet(count))
val[count] = char(0)
self.AutoAddrModes[key] = val
return 1
# _auto_addr_mode - look up the value-reference mode of parameter idx (0=none / 1=ref / 2=temp).
def _auto_addr_mode(self, name: str, idx: int) -> int:
"""Return the value-reference mode for parameter idx of a callee symbol name (0 if none/unknown)."""
if self.AutoAddrModes is None or name is None or idx < 0: return 0
v: t.CPtr = self.AutoAddrModes.__getitem__(name)
if v is None: return 0
modes: str = t.CPtr(v)
i: int = 0
while i < idx and modes[i] != char(0): i += 1
if i != idx: return 0
code: int = int(modes[idx])
if code == 49: return 1 # '1' = ref
if code == 50: return 2 # '2' = temp
return 0
# translate_call_arg - translate one call argument, applying ref/temp auto-address when required.
def translate_call_arg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
trans: HT.Translator*, callee_name: str, arg_idx: int, arg_node: ast.AST*,
fpty: mlirlite.LLVMType*, use_ctx: str = "param") -> mlirlite.Value*:
"""Translate a call argument; value-reference params take the address (ref) or a temp copy (temp)."""
mode: int = self._auto_addr_mode(callee_name, arg_idx)
if mode == 0:
return self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node, None, 0, trans, fpty, use_ctx)
# Value-reference param: translate without the pointer target hint (a char/str literal must
# not be turned into a string), then apply the auto-address transform.
av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node, None, 0, trans, None, use_ctx)
return self._apply_auto_addr_arg(builder, pool, mod, trans, mode, arg_node, av)
# _apply_auto_addr_arg - ref: take the lvalue address; temp: copy into a fresh entry-block slot.
def _apply_auto_addr_arg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
trans: HT.Translator*, mode: int, node: ast.AST*, av: mlirlite.Value*) -> mlirlite.Value*:
"""Apply a value-reference mode to a translated argument value."""
if av is None: return None
if self.Gen.ExprGenBase.is_ptr_type(av.Ty) != 0: return av # already a pointer: pass through
if mode == 1:
if node is None: return av
nk: int = node.kind()
if nk == ast.ASTKind.Name or nk == ast.ASTKind.Attribute or nk == ast.ASTKind.Subscript:
ao_node: ast.AST* = ast.UnaryOp(pool, ast.OpKind.AddrOf, node)
if ao_node is None: return av
return self.Gen.ExprGen.translate_value(builder, pool, mod, ao_node, None, 0, trans, None, None)
self.Gen.TypeGenErr.fatal_error(node, "value-reference parameter '&T' needs an lvalue argument (variable / field / element) or an existing pointer; a literal or expression result has no address (use '&temp T' to pass a temporary copy)", "CastError")
return av
if mode == 2:
slot: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, av.Ty)
if slot is None: return av
mlirlite.build_store(builder, av, slot)
return slot
return av
def _lookup_module_sha1(self, mod_name: str) -> str:
"""Look up the SHA1 for a module name via StubMerger.LookupSha1ByModName (exact match + package fallback); None if absent."""
return StubMergerStore.LookupSha1ByModName(mod_name)
+7 -2
View File
@@ -985,7 +985,9 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
if arg_node_k is not None:
# Fetch the param type first (char literal -> str param needs a materialized string).
pty_k0: mlirlite.LLVMType* = mlirlite.param_get_ty(cur_param_k)
arg_val_k = self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node_k, None, 0, trans, pty_k0)
ao_name_k: str = None
if found_func is not None: ao_name_k = mlirlite.function_get_name(found_func)
arg_val_k = self.Gen.CallGenBase.translate_call_arg(builder, pool, mod, trans, ao_name_k, pi_k, arg_node_k, pty_k0, None)
if arg_val_k is None:
if arg_node_k is not None:
# (a) The arg node exists but translated to None: fail fast;
@@ -1026,7 +1028,10 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
pp = mlirlite.param_get_next(pp)
pi += 1
if pp is not None: pty = mlirlite.param_get_ty(pp)
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node, None, 0, trans, pty)
# Value-reference params (&T / &temp T): auto-address (self is param 0, arg ai -> param ai+1).
ao_name_p: str = None
if found_func is not None: ao_name_p = mlirlite.function_get_name(found_func)
arg_val: mlirlite.Value* = self.Gen.CallGenBase.translate_call_arg(builder, pool, mod, trans, ao_name_p, int(ai) + 1, arg_node, pty, None)
if arg_val is None:
# (a) The arg expression translated to None: fail fast, never continue;
# skipping would reduce arg_count_eff and get zero-filled later.
+16 -20
View File
@@ -176,7 +176,8 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
fpty: mlirlite.LLVMType* = None
if fp is not None: fpty = mlirlite.param_get_ty(fp)
if arg is not None:
av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans, fpty, "param")
# Value-reference params (&T / &temp T): auto-address the argument (specialized callee).
av: mlirlite.Value* = self.Gen.CallGenBase.translate_call_arg(builder, pool, mod, trans, spec_name, int(ai), arg, fpty, "param")
if av is not None:
if fpty is not None: av = self.Gen.ExprGenBase.coerce_to_type(builder, av, fpty, arg, trans)
av.Next = None
@@ -226,6 +227,15 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
# intercept before the c.* attribute branch, since the bare cl.func is a Name.
if func_name is not None and cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
bare_nm: ast.Name* = ast.Name*(cl.func)
if bare_nm.id is not None and string.strcmp(bare_nm.id, "defined") == 0:
# defined(NAME) is a compile-time predicate valid only inside a cif/celif condition;
# it is evaluated there and never lowered as a runtime call.
self.Gen.TypeGenErr.fatal_error(cl, "defined(NAME) is only valid inside a cif/celif condition; it cannot be used in a normal expression", "NameError")
return None
if bare_nm.id is not None and (string.strcmp(bare_nm.id, "CIf") == 0 or string.strcmp(bare_nm.id, "CIfdef") == 0 or string.strcmp(bare_nm.id, "CIfndef") == 0):
# removed spellings (also covered for the bare-name form of c.CIf/c.CIfdef/c.CIfndef)
self.Gen.TypeGenErr.fatal_error(cl, "c.CIf/c.CIfdef/c.CIfndef were removed; use the cif/celif/celse statements and the defined(NAME) predicate instead (e.g. `cif defined(NAME):` ... `celse:` ...)", "NameError")
return None
if bare_nm.id is not None and string.strcmp(bare_nm.id, "__asm__") == 0:
# translate_c_asm returns None for void output: set the hit flag so translate_call
# does not misread it as a miss and fall through to the external function path.
@@ -268,25 +278,11 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
elif string.strcmp(func_name, "LInp") == 0 or string.strcmp(func_name, "LOut") == 0:
# c.LInp/c.LOut standalone calls return 0 (only meaningful in c.LLVMIR f-strings)
return mlirlite.ConstInt(pool, mlirlite.Int32(pool), 0, "0")
elif string.strcmp(func_name, "CIf") == 0:
# Runtime c.CIf(expr) fallback: compare expr against 0 to get i1
# (mirrors TPC HandlesExprCall.py L2434-2444).
cif_args: list[ast.AST*]* = cl.args
if cif_args is not None and len(cif_args) >= 1:
cif_arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cif_args.get(0), funcs_ptr, func_count, trans)
if cif_arg_val is not None:
cif_bits: int = self.Gen.ExprGenBase.get_llvm_type_bits(cif_arg_val.Ty)
if cif_bits == 1: return cif_arg_val
if self.Gen.ExprGenBase.is_ptr_type(cif_arg_val.Ty) != 0:
cif_null: mlirlite.Value* = mlirlite.ConstNull(pool, cif_arg_val.Ty, "null")
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cif_arg_val, cif_null)
cif_zero: mlirlite.Value* = mlirlite.const_int32(pool, 0)
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cif_arg_val, cif_zero)
return mlirlite.ConstInt(pool, mlirlite.Int1(pool), 0, "0")
elif string.strcmp(func_name, "CIfdef") == 0 or string.strcmp(func_name, "CIfndef") == 0:
# Runtime c.CIfdef/c.CIfndef fallback: always true
# (mirrors TPC HandlesExprCall.py L2445-2446).
return mlirlite.ConstInt(pool, mlirlite.Int1(pool), 1, "1")
elif string.strcmp(func_name, "CIf") == 0 or string.strcmp(func_name, "CIfdef") == 0 or string.strcmp(func_name, "CIfndef") == 0:
# c.CIf/c.CIfdef/c.CIfndef were removed: report the replacement spelling.
# (There is no runtime fallback: compile-time conditions use cif/celif/celse.)
self.Gen.TypeGenErr.fatal_error(cl, "c.CIf/c.CIfdef/c.CIfndef were removed; use the cif/celif/celse statements and the defined(NAME) predicate instead (e.g. `cif defined(NAME):` ... `celse:` ...)", "NameError")
return None
elif is_coro_mod != 0 and string.strcmp(func_name, "coro_run") == 0:
# coro.coro_run(frame): inline translation (resume loop until done, returns the ret slot).
# Note: translate_coro_* take (builder, pool, mod, cl, trans), unlike the c.* branches.
+7 -1
View File
@@ -208,12 +208,18 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
arg_idx: int = idx - self_off
arg_node: ast.AST* = None
if cargs is not None and arg_idx >= 0 and arg_idx < len(cargs): arg_node = cargs.get(arg_idx)
# Value-reference params (&T / &temp T): auto-address the argument (cross-module callee).
# The mode comes from the definition-side vpi annotation segment; if the arg is not a
# pointer it is addressed (ref: lvalue) or copied into a temp slot (temp).
mode_cm: int = VpiReader._vpi_param_annot_seg_mode(prm_annots, idx)
if mode_cm != 0 and trans is not None and trans.Module is not None:
node_v = self.Gen.CallGenBase._apply_auto_addr_arg(builder, pool, trans.Module, trans, mode_cm, arg_node, node_v)
# "str / bytes are not generic pointer wildcards": void* / t.CPtr / rawptr may
# convert to any pointer, but str/bytes may not, even though both are i8* at LLVM level.
if self.Gen.ExprGenBase._str_rejects_ptr_kind(cur) != 0:
if VpiReader._vpi_param_annot_seg_is(sha1_buf, cls_nm, meth_nm, idx, "str") != 0 or VpiReader._vpi_param_annot_seg_is(sha1_buf, cls_nm, meth_nm, idx, "bytes") != 0:
self.Gen.ExprGenBase.fail_if_str_annot_agg_ptr(cur, 1, arg_node)
node_v = self._coerce_arg_value_to_ty(pool, builder, cur, tgt_ty, arg_node, trans, tpl_src)
node_v = self._coerce_arg_value_to_ty(pool, builder, node_v, tgt_ty, arg_node, trans, tpl_src)
if new_head is None: new_head = node_v
else: new_tail.Next = node_v
new_tail = node_v
+4 -1
View File
@@ -758,7 +758,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
fp = mlirlite.param_get_next(fp)
fpi += 1
if fp is not None: fpty = mlirlite.param_get_ty(fp)
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg, None, 0, trans, fpty, "param")
# Value-reference params (&T / &temp T): auto-address the argument (same-module callee).
ao_name: str = None
if found_func is not None: ao_name = mlirlite.function_get_name(found_func)
arg_val: mlirlite.Value* = self.Gen.CallGenBase.translate_call_arg(builder, pool, mod, trans, ao_name, int(ai), arg, fpty, "param")
if arg_val is not None:
# Arg promotion: when found_func is None (stub not injected), infer the expected type from
# the external signature and promote literals (e.g. i32->i64) to avoid declare/call mismatches.
+2
View File
@@ -272,6 +272,8 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
continue
# Resolve the field type
# Value-reference markers are parameter-only: a struct field `x: &T` fails fast.
self.Gen.TypeGenResolve.reject_ref_marker_position(aa.annotation, trans._imported_modules, "a struct field")
field_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, aa.annotation, trans._imported_modules, trans._from_imports, trans, 0)
# void is not a valid field type (only for `void*` and opaque typedefs); letting
# it through builds an illegal layout, so fail-fast at compile time here.
+6 -4
View File
@@ -114,7 +114,8 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
at: ast.Attribute* = ast.Attribute*(node)
if at.attr is not None:
if string.strcmp(at.attr, "CPtr") == 0: return 1
if string.strcmp(at.attr, "CNeedPtr") == 0: return 1
if string.strcmp(at.attr, "CRef") == 0: return 1
if string.strcmp(at.attr, "CTempRef") == 0: return 1
if string.strcmp(at.attr, "CAutoPtr") == 0: return 1
if string.strcmp(at.attr, "rawptr") == 0: return 1
if string.strcmp(at.attr, "str") == 0: return 1
@@ -124,7 +125,8 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
nm: ast.Name* = ast.Name*(node)
if nm.id is not None:
if string.strcmp(nm.id, "CPtr") == 0: return 1
if string.strcmp(nm.id, "CNeedPtr") == 0: return 1
if string.strcmp(nm.id, "CRef") == 0: return 1
if string.strcmp(nm.id, "CTempRef") == 0: return 1
if string.strcmp(nm.id, "CAutoPtr") == 0: return 1
if string.strcmp(nm.id, "rawptr") == 0: return 1
if string.strcmp(nm.id, "str") == 0: return 1
@@ -265,8 +267,8 @@ novirtual class ClassGenEnum(ClassGenBase.ClassGenBase):
tgt: ast.Name* = ast.Name*(aa.target)
if tgt.id is None: continue
# [enum spec fail-fast] Members may not be pointer types: t.CPtr / CNeedPtr /
# CAutoPtr / rawptr[...] / str / bytes and unions carrying a pointer. Checked before type resolution.
# [enum spec fail-fast] Members may not be pointer types: t.CPtr / CRef /
# CTempRef / CAutoPtr / rawptr[...] / str / bytes and unions carrying a pointer. Checked before type resolution.
if self._enum_member_annotation_is_ptr(aa.annotation) == 1:
self.Gen.TypeGenErr.fatal_error(aa, f"enum member '{tgt.id}' cannot be a pointer type (enum values only support integer member types)", "TypeError")
return 0 # unreachable (fatal_error already exited)
+23 -1
View File
@@ -376,7 +376,10 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
tpl_file: str = self._find_generic_func_template_file(template_key)
if tpl_file is not None: self.Gen.TypeGenResolve.set_current_file(tpl_file)
ret_ty: mlirlite.LLVMType* = None
if spec_fd.returns is not None: ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, spec_fd.returns, trans._imported_modules, trans._from_imports, trans, 0)
if spec_fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast on a generic too.
self.Gen.TypeGenResolve.reject_ref_marker_position(spec_fd.returns, trans._imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, spec_fd.returns, trans._imported_modules, trans._from_imports, trans, 0)
if ret_ty is None: ret_ty = i32_ty
func: mlirlite.Function* = mlirlite.create_declare(pool, mod, spec_name, ret_ty)
if func is not None and spec_fd.args is not None and spec_fd.args.kind() == ast.ASTKind.Arguments:
@@ -404,6 +407,25 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
# Register value-reference (&T / &temp T) parameter modes for the specialization.
mode_buf_g: str = pool.alloc(an + 1)
if mode_buf_g is not None:
mode_cnt_g: int = 0
has_ref_g: int = 0
for ai_g in range(an):
pa_g: ast.AST* = alist.get(ai_g)
if pa_g is None or pa_g.kind() != ast.ASTKind.Arg: continue
arg_g: ast.Arg* = ast.Arg*(pa_g)
if arg_g.arg is None: continue
mm_g: int = 0
if arg_g.annotation is not None: mm_g = self.Gen.TypeGenResolve.ref_param_mode(arg_g.annotation, trans._imported_modules)
if mm_g == 1: mode_buf_g[mode_cnt_g] = char(49)
elif mm_g == 2: mode_buf_g[mode_cnt_g] = char(50)
else: mode_buf_g[mode_cnt_g] = char(48)
if mm_g != 0: has_ref_g = 1
mode_cnt_g += 1
mode_buf_g[mode_cnt_g] = char(0)
if has_ref_g != 0: self.Gen.CallGenBase._register_auto_addr_modes(spec_name, mode_buf_g, mode_cnt_g)
if tpl_file is not None: self.Gen.TypeGenResolve.set_current_file(saved_cur)
trans.ModuleSha1 = saved_ms
trans._imported_modules = saved_imp
+25 -1
View File
@@ -470,7 +470,10 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
# Infer the return type.
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None: ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a method return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
if ret_ty is None and fd.returns is not None:
# t.State / t.CExport / t.CExtern markers mean Void: such annotations carry no real return
# type (written as "<original> | t.<marker>"); only the marker itself reaches here.
@@ -583,6 +586,27 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
# Register value-reference (&T / &temp T) parameter modes (index 0 = self for non-static methods).
mode_buf: str = pool.alloc(an + 1)
if mode_buf is not None:
mode_cnt: int = 0
has_ref_m: int = 0
if is_static_m == 0:
mode_buf[mode_cnt] = char(48)
mode_cnt += 1
for ai_m in range(start_idx, an):
arg_m: ast.Arg* = ast.Arg*(alist.get(ai_m))
mm: int = 0
if arg_m is not None and arg_m.annotation is not None:
if self.Gen.TypeGenResolve._is_t_attr(arg_m.annotation, "CVoid", imported_modules) == 0:
mm = self.Gen.TypeGenResolve.ref_param_mode(arg_m.annotation, imported_modules)
if mm == 1: mode_buf[mode_cnt] = char(49)
elif mm == 2: mode_buf[mode_cnt] = char(50)
else: mode_buf[mode_cnt] = char(48)
if mm != 0: has_ref_m = 1
mode_cnt += 1
mode_buf[mode_cnt] = char(0)
if has_ref_m != 0: self.Gen.CallGenBase._register_auto_addr_modes(mangled_name, mode_buf, mode_cnt)
# OOP iter protocol: __next__ gets a hidden __stop_iter_flag__ (i1*), set to 1 by raise
# StopIteration. It is not counted in md_param_count; the for loop passes it explicitly.
+2 -1
View File
@@ -103,7 +103,8 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
if string.strcmp(name, "CArray") == 0: return 1
if string.strcmp(name, "CTypedef") == 0: return 1
if string.strcmp(name, "CNoVTable") == 0: return 1
if string.strcmp(name, "CNeedPtr") == 0: return 1
if string.strcmp(name, "CRef") == 0: return 1
if string.strcmp(name, "CTempRef") == 0: return 1
if string.strcmp(name, "CAutoPtr") == 0: return 1
return 0
def _attr_module_path_str(self, pool: memhub.MemBuddy*, node: ast.AST*) -> str:
+6 -2
View File
@@ -267,9 +267,13 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase):
return ref_fi
# All lookup paths failed: fail-fast (NameError) instead of returning None and crashing
# downstream. Exempt keywords/builtins (self/pool/None/_, CIf-like) whose absence is normal.
# downstream. Exempt keywords/builtins (self/pool/None/_) whose absence is normal.
if nm_id is None: return None
if string.strcmp(nm_id, "self") == 0 or string.strcmp(nm_id, "pool") == 0 or string.strcmp(nm_id, "true") == 0 or string.strcmp(nm_id, "false") == 0 or string.strcmp(nm_id, "None") == 0 or string.strcmp(nm_id, "match") == 0 or string.strcmp(nm_id, "_") == 0 or string.strcmp(nm_id, "CIf") == 0 or string.strcmp(nm_id, "CIfdef") == 0 or string.strcmp(nm_id, "CIfndef") == 0:
if string.strcmp(nm_id, "CIf") == 0 or string.strcmp(nm_id, "CIfdef") == 0 or string.strcmp(nm_id, "CIfndef") == 0:
# The c.CIf/c.CIfdef/c.CIfndef spellings were removed; report the replacement.
self.Gen.TypeGenErr.fatal_error(node, "c.CIf/c.CIfdef/c.CIfndef were removed; use the cif/celif/celse statements and the defined(NAME) predicate instead (e.g. `cif defined(NAME):` ... `celse:` ...)", "NameError")
return None
if string.strcmp(nm_id, "self") == 0 or string.strcmp(nm_id, "pool") == 0 or string.strcmp(nm_id, "true") == 0 or string.strcmp(nm_id, "false") == 0 or string.strcmp(nm_id, "None") == 0 or string.strcmp(nm_id, "match") == 0 or string.strcmp(nm_id, "_") == 0:
return None
# Diagnostics: report the generic context, since "undefined name 'T'" is often far from the
# real cause (0 params = no context; params>0/args 0 = lost args; both>0 = name not a param).
+41
View File
@@ -21,6 +21,37 @@ import lib.core.Gen.DebugInfoGen as DebugInfoGen
novirtual class FuncGenDef(CallGenBase.CallGenBase):
"""Function declarations, translate_function_def, and decorator wrappers."""
# _register_func_auto_addr_modes - record which parameters of fd are value references (&T / &temp T).
# Registered under the emitted symbol name so call sites can auto-address the argument.
def _register_func_auto_addr_modes(self, trans: HT.Translator*, name: str, fd: ast.FunctionDef*) -> int:
"""Register per-parameter value-reference modes for a compiled function definition."""
if trans is None or name is None or fd is None or fd.args is None: return 0
ags: ast.Arguments* = ast.Arguments*(fd.args)
if ags.args is None: return 0
alist: list[ast.AST*]* = ags.args
an: sizet = len(alist)
buf: bytes = stdlib.malloc(an + 1)
if buf is None: return 0
cnt: int = 0
has_ref: int = 0
imp: str = trans._imported_modules
for i in range(an):
arg: ast.Arg* = ast.Arg*(alist.get(i))
if arg is None or arg.arg is None: continue
# CVoid params are not emitted into the signature, so they must not occupy a mode slot.
if arg.annotation is not None and self.Gen.TypeGenResolve._is_t_attr(arg.annotation, "CVoid", imp) != 0: continue
m: int = 0
if arg.annotation is not None: m = self.Gen.TypeGenResolve.ref_param_mode(arg.annotation, imp)
if m == 1: buf[cnt] = char(49)
elif m == 2: buf[cnt] = char(50)
else: buf[cnt] = char(48)
if m != 0: has_ref = 1
cnt += 1
buf[cnt] = char(0)
if has_ref != 0: self.Gen.CallGenBase._register_auto_addr_modes(name, buf, cnt)
stdlib.free(buf)
return 1
# forward_declare_functions - pre-scan all top-level FunctionDefs and emit forward declares.
# Solves same-module forward references; translate_function_def later reuses the declare via find_func_in_module.
def forward_declare_functions(self, trans: HT.Translator*, tree: ast.AST*) -> int:
@@ -77,6 +108,8 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# Infer the return type
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
# A lone t.CExport return annotation is an export modifier with no real type -> Void
# (CExport fell back to i32, forcing the tail check to return i32: mm.vp free root cause).
@@ -122,6 +155,9 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: mangled_name: str = fd.name
else: mangled_name = self.Gen.FuncGenBase._mangle_func_name(trans, fd.name, 0)
# Record value-reference (&T / &temp T) parameter modes for call-site auto-address.
self._register_func_auto_addr_modes(trans, mangled_name, fd)
# Skip if the function already exists (e.g. duplicate definition)
existing: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name)
if existing is not None:
@@ -242,6 +278,8 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# Infer the return type: prefer the return annotation
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
# A lone t.CExport return annotation is an export modifier with no real type -> Void
# (CExport fell back to i32, forcing return i32: mm.vp free root cause). Mixed types keep the resolved result.
@@ -340,6 +378,9 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0: mangled_name: str = fd.name
else: mangled_name: str = self.Gen.FuncGenBase._mangle_func_name(trans, fd.name, 0)
# Record value-reference (&T / &temp T) parameter modes for call-site auto-address.
self._register_func_auto_addr_modes(trans, mangled_name, fd)
# Register the CExport function in the global table (for cross-module calls). t.CExport definitions use bare
# names (@strlen); cross-module calls consult the table to use the bare name instead of @{sha1}.func.
if (has_export != 0 or has_state != 0) and trans.ModuleSha1 is not None: self.Gen.CallGenBase.register_cexport_func(trans.ModuleSha1, fd.name)
+108 -39
View File
@@ -42,11 +42,15 @@ novirtual class StmtGenCifMixin:
# Cluster B: compile-time CIf macro eval (migrated from HandlesIf.vp)
def _is_cif_attr(self, attr: str) -> int:
"""Check whether the attribute name is a CIf-family name (CIf/CIfdef/CIfndef)."""
"""Check the internal cif marker attribute name produced by the parser (c.@CIf family).
The '@' prefix is internal-only spelling (a leading '@' cannot be lexed as an identifier), so a
user-written c.CIf/c.CIfdef/c.CIfndef never matches here and is reported as removed instead.
"""
if attr is None: return 0
if string.strcmp(attr, "CIf") == 0: return 1
if string.strcmp(attr, "CIfdef") == 0: return 1
if string.strcmp(attr, "CIfndef") == 0: return 1
if string.strcmp(attr, "@CIf") == 0: return 1
if string.strcmp(attr, "@CIfdef") == 0: return 1
if string.strcmp(attr, "@CIfndef") == 0: return 1
return 0
def _is_cif_call(self, node: ast.AST*) -> int:
@@ -151,7 +155,8 @@ novirtual class StmtGenCifMixin:
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn.const_kind == ast.CONST_INT:
# integer and boolean literals are compile-time constants (True/False carry 1/0)
if cn.const_kind == ast.CONST_INT or cn.const_kind == ast.CONST_BOOL:
self.CifEvalOk = 1
return cn.int_val
self.CifEvalOk = 0
@@ -272,13 +277,58 @@ novirtual class StmtGenCifMixin:
if pointee is not None and self.Gen.ExprGenBase.is_ptr_type(pointee) != 0: return 1
return 0
def _cif_eval_defined(self, pool: memhub.MemBuddy*, trans: HT.Translator*, cl: ast.Call*) -> int:
"""Evaluate the defined(NAME) compile-time predicate (1 = macro defined, 0 = not).
defined() is always evaluable: an unknown name yields 0. A wrong argument shape is fail-fast.
The macro name resolves through the same CDefine / cross-module / platform-macro paths as a
bare name in a cif condition.
"""
cargs: list[ast.AST*]* = cl.args
if cargs is None or len(cargs) != 1:
self.Gen.TypeGenErr.fatal_error(cl, "defined() takes exactly one macro name, e.g. defined(NAME)", "SyntaxError")
return 0
arg0: ast.AST* = cargs.get(0)
# Dotted macro name (module.NAME, e.g. defined(platmacro.IS_WINDOWS)): resolve it the same
# way a bare macro name resolves; a resolvable value means the macro exists.
if arg0 is not None and arg0.kind() == ast.ASTKind.Attribute:
self._cif_eval_node(pool, trans, arg0)
dotted_defined: int = self.CifEvalOk
self.CifEvalOk = 1
return 1 if dotted_defined != 0 else 0
name_arg: str = None
if arg0 is not None and arg0.kind() == ast.ASTKind.Name:
nm0: ast.Name* = ast.Name*(arg0)
name_arg = nm0.id
elif arg0 is not None and arg0.kind() == ast.ASTKind.Constant:
cn0: ast.Constant* = ast.Constant*(arg0)
if cn0.const_kind == ast.CONST_STR: name_arg = cn0.str_val
if name_arg is None:
self.Gen.TypeGenErr.fatal_error(cl, "defined() requires a bare macro name, e.g. defined(NAME)", "SyntaxError")
return 0
self._cif_name_value(pool, trans, name_arg)
defined: int = self.CifEvalOk
# the defined() test itself is evaluable even when the macro is absent (defined = 0)
self.CifEvalOk = 1
return 1 if defined != 0 else 0
def _cif_eval_call(self, pool: memhub.MemBuddy*, trans: HT.Translator*, node: ast.AST*) -> int:
"""Evaluate the ctraits.isptr(x) compile-time call, returning 1/0; unknown calls return 0."""
"""Evaluate a compile-time call inside a cif/celif condition: defined(NAME) or ctraits.isptr(x).
Unknown / runtime calls return 0 with CifEvalOk=0, so the caller reports "not a compile-time
constant" instead of emitting a runtime branch.
"""
self.CifEvalOk = 0
if node is None or trans is None: return 0
cl: ast.Call* = ast.Call*(node)
if cl is None or cl.func is None: return 0
fk: int = cl.func.kind()
# defined(NAME): compile-time macro predicate. Valid only inside a cif/celif condition; any
# other use is reported by the call generator (bare `defined(...)` name).
if fk == ast.ASTKind.Name:
dn: ast.Name* = ast.Name*(cl.func)
if dn.id is not None and string.strcmp(dn.id, "defined") == 0:
return self._cif_eval_defined(pool, trans, cl)
if fk != ast.ASTKind.Attribute: return 0
at: ast.Attribute* = ast.Attribute*(cl.func)
if at.value is None or at.attr is None: return 0
@@ -306,7 +356,11 @@ novirtual class StmtGenCifMixin:
return r
def _evaluate_cif_test(self, pool: memhub.MemBuddy*, trans: HT.Translator*, test: ast.AST*) -> int:
"""Evaluate a CIf-family call, returning the condition and setting CifEvalOk on success."""
"""Evaluate an internal cif marker call (c.@CIf family), returning the condition.
Sets CifEvalOk=1 when the condition is a compile-time constant, 0 otherwise (the caller then
fail-fasts: cif/celif conditions must be compile-time constants, no runtime fallback).
"""
self.CifEvalOk = 0
cl: ast.Call* = ast.Call*(test)
if cl is None or cl.func is None: return 0
@@ -322,11 +376,11 @@ novirtual class StmtGenCifMixin:
cargs: list[ast.AST*]* = cl.args
if cargs is None or len(cargs) < 1: return 0
arg0: ast.AST* = cargs.get(0)
if string.strcmp(kind, "CIf") == 0:
if string.strcmp(kind, "@CIf") == 0:
v: int = self._cif_eval_node(pool, trans, arg0)
if self.CifEvalOk == 0: return 0
return 1 if v != 0 else 0
# CIfdef / CIfndef: test whether the macro name is defined
# @CIfdef / @CIfndef: test whether the macro name is defined
defined: int = 0
if arg0 is not None and arg0.kind() == ast.ASTKind.Name:
nm0: ast.Name* = ast.Name*(arg0)
@@ -346,10 +400,24 @@ novirtual class StmtGenCifMixin:
else: return 0
# the defined-check itself succeeded (an undefined name is not an error; defined=0)
self.CifEvalOk = 1
if string.strcmp(kind, "CIfdef") == 0: return 1 if defined != 0 else 0
# CIfndef
if string.strcmp(kind, "@CIfdef") == 0: return 1 if defined != 0 else 0
# @CIfndef
return 0 if defined != 0 else 1
def _prune_cif_test(self, pool: memhub.MemBuddy*, trans: HT.Translator*, test: ast.AST*) -> int:
"""Return the compile-time value (1/0) of a cif/celif marker condition.
Fail-fast when the condition is not a compile-time constant: cif/celif never fall back to a
runtime branch. Shared by translate_if and the coroutine If handler.
"""
v: int = self._evaluate_cif_test(pool, trans, test)
if self.CifEvalOk == 0:
self.Gen.TypeGenErr.fatal_error(test,
f"the cif/celif condition is not a compile-time constant (line {test.lineno}):"
" cif/celif accept only CDefine constants, integer literals, defined(NAME), and their"
" constant arithmetic/comparison/boolean/bitwise combinations", "TypeError")
return v
# translate_return - translate Return (migrated from HandlesReturn.ReturnHandle): emits ret,
# a bare return emits the return-type default zero (void→ret void, ptr→null, float→0.0).
def translate_return(self, trans: HT.Translator*, node: ast.AST*) -> int:
@@ -539,7 +607,8 @@ novirtual class StmtGenCifMixin:
return 0
# translate_if - translate if/elif/else (migrated from HandlesIf.IfHandle.Handle):
# compile-time c.CIf/CIfdef/CIfndef branch-prunes when evaluable, else emits a runtime cond br.
# a cif/celif condition (internal c.@CIf marker) branch-prunes at compile time; a condition that
# is not a compile-time constant is a fail-fast error (no runtime fallback).
def translate_if(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""Translate an if/elif/else statement."""
if node is None: return 0
@@ -552,34 +621,34 @@ novirtual class StmtGenCifMixin:
if_node: ast.If* = ast.If*(node)
# 0. compile-time macro condition: if c.CIf/CIfdef/CIfndef - if evaluable, compile only
# the taken branch (elif recurses); if not, fall back to runtime translation.
# 0. compile-time condition (cif/celif desugared to If(test=c.@CIf(EXPR))): evaluate the
# constant expression and compile only the taken branch (celif recurses via orelse). A
# condition that is not a compile-time constant is fail-fast; there is no runtime fallback.
if self._is_cif_call(if_node.test) == 1:
cif_val: int = self._evaluate_cif_test(pool, trans, if_node.test)
if self.CifEvalOk != 0:
if cif_val != 0:
body_cif: list[ast.AST*]* = if_node.children
if body_cif is not None:
bc_cif: sizet = len(body_cif)
bi_cif: sizet
for bi_cif in range(bc_cif):
stmt_cif: ast.AST* = body_cif.get(bi_cif)
if stmt_cif is not None: self.Gen.StmtGen.translate_stmt(trans, stmt_cif)
else:
orelse_cif: list[ast.AST*]* = if_node.orelse
if orelse_cif is not None and len(orelse_cif) == 1:
sub_cif: ast.AST* = orelse_cif.get(0)
if sub_cif is not None and sub_cif.kind() == ast.ASTKind.If:
# elif: recurse (the sub-If test again goes through CIf or runtime)
self.translate_if(trans, sub_cif)
elif sub_cif is not None: self.Gen.StmtGen.translate_stmt(trans, sub_cif)
elif orelse_cif is not None:
oc_cif: sizet = len(orelse_cif)
oi_cif: sizet
for oi_cif in range(oc_cif):
stmt_cif2: ast.AST* = orelse_cif.get(oi_cif)
if stmt_cif2 is not None: self.Gen.StmtGen.translate_stmt(trans, stmt_cif2)
return 0
cif_val: int = self._prune_cif_test(pool, trans, if_node.test)
if cif_val != 0:
body_cif: list[ast.AST*]* = if_node.children
if body_cif is not None:
bc_cif: sizet = len(body_cif)
bi_cif: sizet
for bi_cif in range(bc_cif):
stmt_cif: ast.AST* = body_cif.get(bi_cif)
if stmt_cif is not None: self.Gen.StmtGen.translate_stmt(trans, stmt_cif)
else:
orelse_cif: list[ast.AST*]* = if_node.orelse
if orelse_cif is not None and len(orelse_cif) == 1:
sub_cif: ast.AST* = orelse_cif.get(0)
if sub_cif is not None and sub_cif.kind() == ast.ASTKind.If:
# celif: recurse (the sub-If test is another c.@CIf marker)
self.translate_if(trans, sub_cif)
elif sub_cif is not None: self.Gen.StmtGen.translate_stmt(trans, sub_cif)
elif orelse_cif is not None:
oc_cif: sizet = len(orelse_cif)
oi_cif: sizet
for oi_cif in range(oc_cif):
stmt_cif2: ast.AST* = orelse_cif.get(oi_cif)
if stmt_cif2 is not None: self.Gen.StmtGen.translate_stmt(trans, stmt_cif2)
return 0
# 1. evaluate the condition expression
cond_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, trans.Module, if_node.test, trans._funcs, trans._func_count, trans)
+44
View File
@@ -68,6 +68,7 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
elif k == ast.ASTKind.Match: return self.Gen.MatchGen.translate_match(trans, node)
elif k == ast.ASTKind.Delete: return self.translate_delete(trans, node)
elif k == ast.ASTKind.Raise: return self.translate_raise(trans, node)
elif k == ast.ASTKind.Assert: return self.translate_assert(trans, node)
elif k == ast.ASTKind.Try: return self.translate_try(trans, node)
elif k == ast.ASTKind.With: return self.translate_with(trans, node)
# unknown statement type: fail-fast to expose an upstream parse problem
@@ -136,6 +137,49 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
mlirlite.build_call(builder, dl_fn_name, dl_self, 2, dl_ret, 0)
return 0
# translate_assert - translate `assert cond[, msg]` into a single testcheck.check(...) call:
# a true condition is silent (ok_msg=None), a false one reports FAIL via the standard path
# (no printf/exit/exception; the checker prints nothing when the condition holds).
def translate_assert(self, trans: HT.Translator*, node: ast.AST*) -> int:
"""Translate an assert statement; returns 0"""
if trans is None or node is None: return 0
asn: ast.Assert* = ast.Assert*(node)
if asn is None or asn.test is None: return 0
pool: memhub.MemBuddy* = trans.Pool
builder: mlirlite.IRBuilder* = trans._cur_builder
mod: mlirlite.LLVMModule* = trans.Module
if pool is None or builder is None or mod is None: return 0
# Resolve the message argument: omitted -> "assertion failed". A non-string literal is
# rejected here (clear report); a non-string expression is caught by the call's int->ptr rule.
msg_node: ast.AST* = asn.msg
if msg_node is None:
msg_node = ast.Constant(pool, ast.CONST_STR, 0, 0.0, "assertion failed", node.lineno, node.col_offset)
if msg_node is None: return 0
elif msg_node.kind() == ast.ASTKind.Constant:
msg_c: ast.Constant* = ast.Constant*(msg_node)
if msg_c is None or msg_c.const_kind != ast.CONST_STR:
self.Gen.TypeGenErr.fatal_error(msg_node, "assert message must be a string (str)", "TypeError")
return 0
# Synthesize `testcheck.check(<test>, None, <msg>)` and reuse the normal call translation
# (cross-module name resolution + argument coercion stay in one place).
none_node: ast.AST* = ast.Constant(pool, ast.CONST_NONE, 0, 0.0, None, node.lineno, node.col_offset)
if none_node is None: return 0
args_list: list[ast.AST*]* = list[ast.AST*](pool, 8)
if args_list is None: return 0
args_list.append(asn.test)
args_list.append(none_node)
args_list.append(msg_node)
mod_name_node: ast.AST* = ast.Name(pool, "testcheck", ast.ASTCtx.Load)
if mod_name_node is None: return 0
check_attr: ast.AST* = ast.Attribute(pool, mod_name_node, "check", ast.ASTCtx.Load)
if check_attr is None: return 0
call_node: ast.AST* = ast.Call(pool, check_attr, args_list, None)
if call_node is None: return 0
self.Gen.ExprGen.translate_value(builder, pool, mod, call_node, trans._funcs, trans._func_count, trans)
return 0
# translate_try - try/except translation. Exception = (i32 code, i8* msg);
# except X statically expands X and all descendant exception codes.
def translate_try(self, trans: HT.Translator*, node: ast.AST*) -> int:
+55
View File
@@ -501,6 +501,9 @@ novirtual class Translator:
# Mirror TPC _inject_auto_imports: detect list/dict container usage (bare annotations,
# constructors, subscripts) and mark the container classes visible so bare names resolve.
self._inject_auto_container_visibility(pool, tree)
# A module using `assert` behaves as if it had `import testcheck`: inject the module
# before dependency-vpi loading so testcheck.check resolves across modules.
self._inject_auto_assert_import(pool, tree)
# CTypedef declarations are registered by Phase1 text scanning (AST string tokens are unstable).
# Cross-module .vpi injection loads dep .vpi declares when full-translating or during App pre-registration.
if self._imported_modules is not None:
@@ -689,6 +692,58 @@ novirtual class Translator:
stdlib.free(need_list_box)
stdlib.free(need_dict_box)
# Auto assert-import injection: a module that uses `assert` is equivalent to having
# `import testcheck` at the top, since assert lowers to testcheck.check(...). Injected
# after real imports (so de-duplication works) and before dependency-vpi loading.
def _inject_auto_assert_import(self, pool: memhub.MemBuddy*, tree: ast.AST*):
"""Ensure `testcheck` is imported when the tree contains an assert statement."""
if tree is None: return
if self._tree_has_assert(tree, 0) == 0: return
if self._imported_modules is not None and GenApi.is_module_imported(self._imported_modules, "testcheck") != 0: return
# Break cycles: skip when testcheck already depends on the current module (mirrors the
# _list/_dict guard; the residual case is cleaned up by Phase1 _BreakContainerCyclesInDeps).
if self._ContainerInjectionCreatesCycle("testcheck") != 0: return
self._imported_modules = GenApi.add_imported_module(pool, self._imported_modules, "testcheck")
# _tree_has_assert - scan statement blocks for an `assert` node (depth-guarded).
def _tree_has_assert(self, node: ast.AST*, depth: int) -> int:
"""Return 1 when the subtree contains an assert statement, else 0."""
if node is None: return 0
# Deep-recursion guard: Translator has no Gen facade, so report through stdio/stdlib
# (same idiom as Generator.__init__'s allocation failures).
if depth > 512:
stdio.printf("FATAL: assert-scan recursion too deep (AST cycle or nesting over 512 levels)\n")
stdlib.exit(1)
k_a: int = node.kind()
if k_a == ast.ASTKind.Assert: return 1
# Try: handlers/orelse/finalbody are separate lists, not part of children.
if k_a == ast.ASTKind.Try:
tr_a: ast.Try* = ast.Try*(node)
if tr_a is not None:
if self._list_has_assert(tr_a.handlers, depth) != 0: return 1
if self._list_has_assert(tr_a.orelse, depth) != 0: return 1
if self._list_has_assert(tr_a.finalbody, depth) != 0: return 1
# Match: the case bodies live in cases, not children.
elif k_a == ast.ASTKind.Match:
mt_a: ast.Match* = ast.Match*(node)
if mt_a is not None and self._list_has_assert(mt_a.cases, depth) != 0: return 1
# Recurse into this node's statement block (module/function/class/if/for/while/with/except bodies).
if node.children is not None:
ch_a: list[ast.AST*]* = node.children
n_a: sizet = len(ch_a)
for bi_a in range(n_a):
if self._tree_has_assert(ch_a.get(bi_a), depth + 1) != 0: return 1
return 0
# _list_has_assert - apply _tree_has_assert over a statement list.
def _list_has_assert(self, lst: list[ast.AST*]*, depth: int) -> int:
"""Return 1 when any node in the statement list contains an assert statement."""
if lst is None: return 0
n_l: sizet = len(lst)
for i_l in range(n_l):
if self._tree_has_assert(lst.get(i_l), depth + 1) != 0: return 1
return 0
def _ContainerInjectionCreatesCycle(self, mod_name: str) -> int:
"""Return 1 if injecting mod_name (_list/_dict) would create a dependency cycle."""
if mod_name is None or self.ModuleSha1 is None: return 0
+2
View File
@@ -531,6 +531,8 @@ novirtual class TranslatorGenMixin:
if self.Gen.CallGenLeafKit.register_func_ptr_sig(pool, var_name, ml_callable_ret, aa) != 0: return 0
# Resolve the type
if aa.annotation is not None:
# Value-reference markers are parameter-only: a module variable `x: &T` fails fast.
self.Gen.TypeGenResolve.reject_ref_marker_position(aa.annotation, trans._imported_modules, "a module-level variable declaration")
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, aa.annotation, trans._imported_modules, trans._from_imports, trans, 0)
if resolved is not None:
# Object-class module vars (@t.Object/@t.CVTable: object-as-pointer) wrap the
+69 -9
View File
@@ -1416,9 +1416,8 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase):
# CDefine: compile-time constant marker, backed by i32 (TPC-aligned). Global/field
# CDefine annotations are short-circuited earlier, so only return-position reaches here.
elif string.strcmp(type_name, "CDefine") == 0: return mlirlite.Int32(pool)
# CNeedPtr/CAutoPtr: auto address-of parameter markers; both map to i8* at the LLVM
# level (TPV does not implement call-site auto address-of).
elif string.strcmp(type_name, "CNeedPtr") == 0: return mlirlite.Ptr(pool, mlirlite.Int8(pool))
# CAutoPtr: unconditional-address-of parameter marker; maps to i8* at the LLVM level
# (t.CNeedPtr was removed in favor of the `&T` / `&temp T` parameter annotation).
elif string.strcmp(type_name, "CAutoPtr") == 0: return mlirlite.Ptr(pool, mlirlite.Int8(pool))
# Unsigned modifiers
elif string.strcmp(type_name, "CUnsigned") == 0: return mlirlite.Int32(pool)
@@ -1477,6 +1476,41 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase):
nm: ast.Name* = ast.Name*(node)
if nm.id is not None and string.strcmp(nm.id, "CPtr") == 0: return 1
return 0
def _is_ref_marker(self, node: ast.AST*, imported_modules: str) -> int:
"""Whether a node is a CRef/CTempRef value-reference marker: 1=yes / 0=no."""
if self._is_t_attr(node, "CRef", imported_modules) != 0: return 1
if self._is_t_attr(node, "CTempRef", imported_modules) != 0: return 1
return 0
def _ref_marker_name_mode(self, node: ast.AST*) -> int:
"""Syntactic value-reference marker test (no import context): 0=none / 1=CRef / 2=CTempRef."""
if node is None: return 0
nm: str = None
if node.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
if at.value is not None and at.value.kind() == ast.ASTKind.Name:
at_vn: ast.Name* = ast.Name*(at.value)
if at_vn.id is not None and string.strcmp(at_vn.id, "t") == 0: nm = at.attr
elif node.kind() == ast.ASTKind.Name:
nm = (ast.Name*(node)).id
if nm is None: return 0
if string.strcmp(nm, "CRef") == 0: return 1
if string.strcmp(nm, "CTempRef") == 0: return 2
return 0
def ref_param_mode(self, node: ast.AST*, imported_modules: str) -> int:
"""Value-reference mode of a top-level union annotation: 0=none / 1=ref (&T) / 2=temp (&temp T)."""
if node is None or node.kind() != ast.ASTKind.BinOp: return 0
bo: ast.BinOp* = ast.BinOp*(node)
if bo.op != ast.OpKind.BitOr: return 0
if self._is_t_attr(bo.left, "CTempRef", imported_modules) != 0: return 2
if self._is_t_attr(bo.right, "CTempRef", imported_modules) != 0: return 2
if self._is_t_attr(bo.left, "CRef", imported_modules) != 0: return 1
if self._is_t_attr(bo.right, "CRef", imported_modules) != 0: return 1
return 0
def reject_ref_marker_position(self, node: ast.AST*, imported_modules: str, where: str) -> int:
"""Fail fast when a value-reference marker (&T / &temp T) is used outside a parameter position."""
if self.ref_param_mode(node, imported_modules) == 0: return 0
self.Gen.TypeGenErr.fatal_error(node, f"value-reference parameter annotation '&T' / '&temp T' is only allowed in function parameter position, not for {where}", "TypeError")
return 1
def _is_cptr_subscript(self, node: ast.AST*) -> int:
"""Whether a node is a t.CPtr[T] subscript pointer type (e.g. t.CPtr[t.CPtr])."""
if node is None or node.kind() != ast.ASTKind.Subscript: return 0
@@ -1557,9 +1591,31 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase):
return buf2
return "<subscript>"
# BinOp node: A | B (union type)
# BinOp node: A | B (union type); value-reference params render as `&T` / `&temp T`
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = ast.BinOp*(annot)
if bo.op == ast.OpKind.BitOr:
m_left: int = self._ref_marker_name_mode(bo.left)
m_right: int = self._ref_marker_name_mode(bo.right)
if m_left != 0 or m_right != 0:
other_rs: ast.AST* = bo.left
m_rs: int = m_left
if m_left != 0:
other_rs = bo.right
else:
m_rs = m_right
inner_rs: str = self.annotation_to_str(other_rs)
if inner_rs is not None:
if m_rs == 2:
buf_tr: str = stdlib.malloc(string.strlen(inner_rs) + 7)
if buf_tr is not None:
viperlib.snprintf(buf_tr, string.strlen(inner_rs) + 7, "&temp %s", inner_rs)
return buf_tr
else:
buf_rr: str = stdlib.malloc(string.strlen(inner_rs) + 2)
if buf_rr is not None:
viperlib.snprintf(buf_rr, string.strlen(inner_rs) + 2, "&%s", inner_rs)
return buf_rr
left_s: str = self.annotation_to_str(bo.left)
right_s: str = self.annotation_to_str(bo.right)
if left_s is not None and right_s is not None:
@@ -2271,14 +2327,18 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase):
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(node)
if bop.op == ast.OpKind.BitOr:
# Detect the t.CPtr marker (supports t.CPtr and the bare from t import CPtr name)
# Detect the t.CPtr marker (supports t.CPtr and the bare from t import CPtr name).
# CRef/CTempRef (value-reference parameter markers) resolve identically here: the
# parameter ABI is Ptr(other); the ref/temp distinction only drives call-site auto-address.
left_is_cptr: int = self._is_cptr_marker(bop.left, imported_modules)
right_is_cptr: int = self._is_cptr_marker(bop.right, imported_modules)
if left_is_cptr != 0 or right_is_cptr != 0:
# CPtr present: resolve the actual type on the other side and wrap it in Ptr
# (left CPtr -> other is right; right CPtr -> other is left).
left_is_ref: int = self._is_ref_marker(bop.left, imported_modules)
right_is_ref: int = self._is_ref_marker(bop.right, imported_modules)
if left_is_cptr != 0 or right_is_cptr != 0 or left_is_ref != 0 or right_is_ref != 0:
# CPtr/ref present: resolve the actual type on the other side and wrap it in Ptr
# (left marker -> other is right; right marker -> other is left).
other_node: ast.AST* = bop.left
if left_is_cptr != 0: other_node = bop.right
if left_is_cptr != 0 or left_is_ref != 0: other_node = bop.right
# Detect whether the other side is t.CVoid (void* = i8*), strictly validating
# the full form (Attribute with value Name("t"), or bare CVoid with t imported).
other_is_cvoid: int = 0