feat(obs): reject nullable call arguments for non-nullable parameters
Compiled callees register a per-parameter nullable mask (self included for methods, specializations included); cross-module callees fall back to their vpi param annotations. A nullable binding/field argument now fails instead of silently narrowing.
This commit is contained in:
1 parent
b17de2bb65
commit
65e03c823f
6 files changed
+156
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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*,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
# ============================================================
|
||||
|
||||
Reference in new issue
Block a user