diff --git a/App/lib/Phase/Phase1/Phase1Main.vp b/App/lib/Phase/Phase1/Phase1Main.vp index 35afa7c..01b01a9 100644 --- a/App/lib/Phase/Phase1/Phase1Main.vp +++ b/App/lib/Phase/Phase1/Phase1Main.vp @@ -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 diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index 65e3552..bafa847 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -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 diff --git a/App/lib/Stub/StubMerger/StubMergerBuild.vp b/App/lib/Stub/StubMerger/StubMergerBuild.vp index ebad062..468dc87 100644 --- a/App/lib/Stub/StubMerger/StubMergerBuild.vp +++ b/App/lib/Stub/StubMerger/StubMergerBuild.vp @@ -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 diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 2a208fa..9be2a65 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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) diff --git a/App/lib/core/Gen/AsyncGenMixin.vp b/App/lib/core/Gen/AsyncGenMixin.vp index 723743b..1633233 100644 --- a/App/lib/core/Gen/AsyncGenMixin.vp +++ b/App/lib/core/Gen/AsyncGenMixin.vp @@ -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: diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index b49b94b..5552968 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -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) diff --git a/App/lib/core/Gen/Call/CallGenCall.vp b/App/lib/core/Gen/Call/CallGenCall.vp index 284ae03..24387e1 100644 --- a/App/lib/core/Gen/Call/CallGenCall.vp +++ b/App/lib/core/Gen/Call/CallGenCall.vp @@ -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. diff --git a/App/lib/core/Gen/Call/CallGenDispatch.vp b/App/lib/core/Gen/Call/CallGenDispatch.vp index 6a75ce9..8374e61 100644 --- a/App/lib/core/Gen/Call/CallGenDispatch.vp +++ b/App/lib/core/Gen/Call/CallGenDispatch.vp @@ -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. diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index 8aea5ad..dce1c3d 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -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 diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index 9cdf289..e18faa6 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -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. diff --git a/App/lib/core/Gen/Class/ClassGenDef.vp b/App/lib/core/Gen/Class/ClassGenDef.vp index fd79264..f6c6fe0 100644 --- a/App/lib/core/Gen/Class/ClassGenDef.vp +++ b/App/lib/core/Gen/Class/ClassGenDef.vp @@ -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. diff --git a/App/lib/core/Gen/Class/ClassGenEnum.vp b/App/lib/core/Gen/Class/ClassGenEnum.vp index 2005488..5e01bb4 100644 --- a/App/lib/core/Gen/Class/ClassGenEnum.vp +++ b/App/lib/core/Gen/Class/ClassGenEnum.vp @@ -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) diff --git a/App/lib/core/Gen/Class/ClassGenGeneric.vp b/App/lib/core/Gen/Class/ClassGenGeneric.vp index d21d5c9..52f3fc0 100644 --- a/App/lib/core/Gen/Class/ClassGenGeneric.vp +++ b/App/lib/core/Gen/Class/ClassGenGeneric.vp @@ -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 diff --git a/App/lib/core/Gen/Class/ClassGenVTable.vp b/App/lib/core/Gen/Class/ClassGenVTable.vp index 113f3a8..50270d1 100644 --- a/App/lib/core/Gen/Class/ClassGenVTable.vp +++ b/App/lib/core/Gen/Class/ClassGenVTable.vp @@ -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 " | t."); 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. diff --git a/App/lib/core/Gen/Expr/ExprGenAttr.vp b/App/lib/core/Gen/Expr/ExprGenAttr.vp index 6350d57..4de7585 100644 --- a/App/lib/core/Gen/Expr/ExprGenAttr.vp +++ b/App/lib/core/Gen/Expr/ExprGenAttr.vp @@ -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: diff --git a/App/lib/core/Gen/Expr/ExprGenName.vp b/App/lib/core/Gen/Expr/ExprGenName.vp index bebef01..6dfb4ff 100644 --- a/App/lib/core/Gen/Expr/ExprGenName.vp +++ b/App/lib/core/Gen/Expr/ExprGenName.vp @@ -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). diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index 3196f26..b33254e 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -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) diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index eba2e47..cb49156 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -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) diff --git a/App/lib/core/Gen/Stmt/StmtGenMixin.vp b/App/lib/core/Gen/Stmt/StmtGenMixin.vp index bd088e6..e1324a4 100644 --- a/App/lib/core/Gen/Stmt/StmtGenMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenMixin.vp @@ -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(, None, )` 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: diff --git a/App/lib/core/Gen/Translator.vp b/App/lib/core/Gen/Translator.vp index 92c11ca..734198b 100644 --- a/App/lib/core/Gen/Translator.vp +++ b/App/lib/core/Gen/Translator.vp @@ -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 diff --git a/App/lib/core/Gen/TranslatorGenMixin.vp b/App/lib/core/Gen/TranslatorGenMixin.vp index a341b4d..b7be654 100644 --- a/App/lib/core/Gen/TranslatorGenMixin.vp +++ b/App/lib/core/Gen/TranslatorGenMixin.vp @@ -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 diff --git a/App/lib/core/Gen/Type/TypeGenResolve.vp b/App/lib/core/Gen/Type/TypeGenResolve.vp index 2533927..2017f69 100644 --- a/App/lib/core/Gen/Type/TypeGenResolve.vp +++ b/App/lib/core/Gen/Type/TypeGenResolve.vp @@ -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 "" - # 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