diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index 546101c..e287cea 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -1664,6 +1664,22 @@ def _vpi_param_annot_seg_is_ptr(prm_annots: str, seg_idx: int) -> int: return 0 +# _vpi_param_annot_seg_has_nullable - 1 when the segment is a nullable type (`T?`). +def _vpi_param_annot_seg_has_nullable(prm_annots: str, seg_idx: int) -> int: + """Return 1 if the param-annotation segment contains '?' (nullable).""" + if prm_annots is None or seg_idx < 0: return 0 + cur: sizet = 0 + for seg in range(int(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] != '@': + if prm_annots[cur + slen] == '?': return 1 + slen += 1 + return 0 + + # _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: diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 926fdb3..7377a3b 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -101,6 +101,11 @@ novirtual class CallGenBase: # (PermPool-persistent; index = emitted parameter index, self included for methods). AutoAddrModes: hashtable.HashTable* = None + # --- nullable parameter flags (strict OBS): key = callee symbol name -> byte-per-parameter + # '0'/'1' (1 = the parameter is declared nullable `T?`); index = emitted parameter index, + # self included for methods. Registered when a callee definition is translated. + NullableParams: 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 @@ -140,6 +145,68 @@ novirtual class CallGenBase: if code == 50: return 2 # '2' = temp return 0 + # _register_nullable_params - store per-parameter nullability flags for a callee symbol name. + def _register_nullable_params(self, name: char*, flags: char*, count: int) -> int: + """Store per-parameter '0'/'1' nullable flags (PermPool-persistent) keyed by callee symbol name.""" + if name is None or flags is None or count <= 0: return 0 + perm: memhub.MemBuddy* = CompilerFactory.factory.PermPool + if perm is None: return 0 + if self.NullableParams is None: + self.NullableParams = hashtable.HashTable(perm) + if self.NullableParams is None: return 0 + key: char* = perm.alloc(string.strlen(name) + 1) + if key is None: return 0 + string.strcpy(key, name) + val: char* = perm.alloc(sizet(count) + 1) + if val is None: return 0 + string.memcpy(val, flags, sizet(count)) + val[count] = char(0) + self.NullableParams[key] = val + return 1 + + # _register_nullable_from_args - build and store the nullable flags from a callee's AST args. + def _register_nullable_from_args(self, pool: memhub.MemBuddy*, name: char*, + alist: list[ast.AST*]*, from_idx: int) -> int: + """Register which parameters (from from_idx on) are declared nullable; 1 on success.""" + if pool is None or name is None or alist is None: return 0 + n: sizet = len(alist) + if n <= 0: return 0 + buf: str = pool.alloc(n + 2) + if buf is None: return 0 + cnt: int = 0 + i: int = from_idx + if i < 0: i = 0 + while i < int(n): + a: ast.AST* = alist.get(sizet(i)) + i += 1 + flag: char = char(48) + if a is not None and a.kind() == ast.ASTKind.Arg: + ag: ast.Arg* = ast.Arg*(a) + if ag.annotation is not None and GenApi.annotation_is_nullable(ag.annotation) != 0: + flag = char(49) + buf[cnt] = flag + cnt += 1 + buf[cnt] = char(0) + return self._register_nullable_params(name, buf, cnt) + + # _param_nullable_known - 1 when a nullability mask was registered for the callee symbol. + def _param_nullable_known(self, name: char*) -> int: + if self.NullableParams is None or name is None: return 0 + v: rawptr = self.NullableParams.__getitem__(name) + if v is None: return 0 + return 1 + + # _param_is_nullable - 1 when parameter idx of the callee symbol is declared nullable. + def _param_is_nullable(self, name: char*, idx: int) -> int: + if self.NullableParams is None or name is None or idx < 0: return 0 + v: rawptr = self.NullableParams.__getitem__(name) + if v is None: return 0 + flags: char* = rawptr(v) + i: int = 0 + while i < idx and flags[i] != char(0): i += 1 + if i != idx: return 0 + return 1 if int(flags[idx]) == 49 else 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: char*, arg_idx: int, arg_node: ast.AST*, diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index 48e9bb2..fe25e4e 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -179,6 +179,31 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): # ensure-load + direct cache lookup (not _vpi_method_param_annot, which misses module funcs) if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0: VpiReader._ensure_vpi_param_annot_loaded(sha1_buf, self.Gen.CallGenBase.TempDir) prm_annots: char* = VpiReader._lookup_vpi_param_annots(sha1_buf, cls_nm, meth_nm) + # Strict OBS: a nullable binding/field may be None and must not flow into a non-nullable + # parameter. Same-module callees use the nullable registry, cross-module ones use their + # vpi param annotations; an unknown callee signature is left alone. + if trans is not None and trans.StrictOBS != 0 and cargs is not None: + arg_n_chk: int = int(len(cargs)) + ai_chk: int = 0 + while ai_chk < arg_n_chk: + an_chk: ast.AST* = cargs.get(sizet(ai_chk)) + ai_chk += 1 + if an_chk is None: continue + if self.Gen.ExprGenAttr._base_is_nullable(pool, trans, an_chk) == 0: continue + pidx_chk: int = ai_chk - 1 + self_off + checked_chk: int = 0 + allow_chk: int = 0 + if self.Gen.CallGenBase._param_nullable_known(call_name) != 0: + checked_chk = 1 + if self.Gen.CallGenBase._param_is_nullable(call_name, pidx_chk) != 0: allow_chk = 1 + elif prm_annots is not None: + checked_chk = 1 + if VpiReader._vpi_param_annot_seg_has_nullable(prm_annots, pidx_chk) != 0: allow_chk = 1 + if checked_chk == 0: continue + if allow_chk == 0: + pf_chk: int = GenApi.sha1_prefix_len(call_name) + short_chk: char* = call_name + pf_chk if pf_chk != 0 else call_name + self.Gen.TypeGenErr.fatal_error(an_chk, f"parameter {ai_chk} of '{short_chk}' is not nullable: a nullable value may be None; declare it as `T?` or unwrap it with `match` first", "TypeError") # generic-specialized class: annotations live on the template class, so fall back to template lookup tpl_src: int = 0 if prm_annots is None: diff --git a/App/lib/core/Gen/Class/ClassGenGeneric.vp b/App/lib/core/Gen/Class/ClassGenGeneric.vp index 660fb55..3183b87 100644 --- a/App/lib/core/Gen/Class/ClassGenGeneric.vp +++ b/App/lib/core/Gen/Class/ClassGenGeneric.vp @@ -8,6 +8,7 @@ import viperlib import lib.core.VLogger as VLogger import lib.core.Gen.Translator as HT import lib.core.CompilerFactory as CompilerFactory +import lib.core.Gen.GenApi as GenApi from . import ClassGenBase import lib.Stub.StubMerger.StubMergerStore as StubMergerStore @@ -547,6 +548,21 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase): 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) + # Per-parameter nullability (strict OBS), aligned with the modes above. + nul_buf_g_: str = pool.alloc(an + 1) + if nul_buf_g_ is not None: + nc_g: int = 0 + for ai_ng in range(int(an)): + pa_ng: ast.AST* = alist.get(ai_ng) + f_g: char = char(48) + if pa_ng is not None and pa_ng.kind() == ast.ASTKind.Arg: + arg_ng: ast.Arg* = ast.Arg*(pa_ng) + if arg_ng.annotation is not None and GenApi.annotation_is_nullable(arg_ng.annotation) != 0: + f_g = char(49) + nul_buf_g_[nc_g] = f_g + nc_g += 1 + nul_buf_g_[nc_g] = char(0) + self.Gen.CallGenBase._register_nullable_params(spec_name, nul_buf_g_, nc_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 e520245..038eb1a 100644 --- a/App/lib/core/Gen/Class/ClassGenVTable.vp +++ b/App/lib/core/Gen/Class/ClassGenVTable.vp @@ -724,6 +724,23 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase): 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) + # Per-parameter nullability (strict OBS), same emitted-parameter indexing. + nul_buf_m: str = pool.alloc(an + 1) + if nul_buf_m is not None: + nc_m: int = 0 + if is_static_m == 0: + nul_buf_m[nc_m] = char(48) + nc_m += 1 + for ai_n in range(int(start_idx), int(an)): + arg_n: ast.Arg* = ast.Arg*(alist.get(ai_n)) + f_m: char = char(48) + if arg_n is not None and arg_n.annotation is not None \ + and GenApi.annotation_is_nullable(arg_n.annotation) != 0: + f_m = char(49) + nul_buf_m[nc_m] = f_m + nc_m += 1 + nul_buf_m[nc_m] = char(0) + self.Gen.CallGenBase._register_nullable_params(mangled_name, nul_buf_m, nc_m) # *args variadic: set IsVarArg=1 so the signature carries the ... suffix (the body # pipeline allocates the va_list buffer, same as a module-level function). diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index a59ee04..abe8bf7 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -56,6 +56,21 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): buf[cnt] = char(0) if has_ref != 0: self.Gen.CallGenBase._register_auto_addr_modes(name, buf, cnt) stdlib.free(buf) + # Per-parameter nullability (strict OBS): same emitted-parameter indexing as the modes above. + nul_buf: bytes = stdlib.malloc(an + 1) + if nul_buf is not None: + nc: int = 0 + for i2 in range(int(an)): + arg2: ast.Arg* = ast.Arg*(alist.get(i2)) + if arg2 is None or arg2.arg is None: continue + if arg2.annotation is not None and self.Gen.TypeGenResolve._is_builtin_marker(arg2.annotation, "CVoid", imp) != 0: continue + f2: char = char(48) + if arg2.annotation is not None and GenApi.annotation_is_nullable(arg2.annotation) != 0: f2 = char(49) + nul_buf[nc] = f2 + nc += 1 + nul_buf[nc] = char(0) + self.Gen.CallGenBase._register_nullable_params(name, nul_buf, nc) + stdlib.free(nul_buf) return 1 # ============================================================