diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index 76da0f8..5890129 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -94,6 +94,35 @@ def _AnnIsPointerLike(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, return 0 +def _DetailIsPointer(d: str) -> int: + """1 when a symbol detail names a pointer/owned type (`U*`, `own U`, `PtrT[...]`).""" + if d is None: + return 0 + s: str = d + while string.strlen(s) > 0 and s[0] == ' ': + s += 1 + n: sizet = string.strlen(s) + if n == 0: + return 0 + if s[n - 1] == '*': + return 1 + if string.strncmp(s, "own ", 4) == 0: + return 1 + if string.strstr(s, "PtrT[") is not None: + return 1 + return 0 + + +def _ValueIsPointer(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + e: ast.AST*) -> int: + """1 when the value expression yields a pointer (a `new` result or a pointer binding).""" + if e is None: + return 0 + if e.kind() == ast.ASTKind.New: + return 1 + return _DetailIsPointer(_SymDetailAt(pool, a, e)) + + def _AnnIsOwn(ann: ast.AST*) -> int: node: ast.AST* = ann depth: int = 0 @@ -1155,6 +1184,17 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, if occ_u is not None and occ_u.sym is None and occ_u.head is None and occ_u.builtin_type == 0: _EmitTypeError(pool, doc, e, f"unknown name '{nm_m.id}'", errors, 2) return + if k == ast.ASTKind.New: + nw: ast.New* = ast.New*(e) + _CheckCtorArgs(pool, doc, a, ws, table_pool, nw.func, nw.args, errors) + if nw.func is not None: + _WalkChecksExprs(pool, doc, a, ws, table_pool, nw.func, errors) + if nw.args is not None: + i_nw: int = 0 + while i_nw < int(len(nw.args)): + _WalkChecksExprs(pool, doc, a, ws, table_pool, nw.args.get(sizet(i_nw)), errors) + i_nw += 1 + return if k == ast.ASTKind.Call: call: ast.Call* = ast.Call*(e) _CheckCall(pool, doc, a, ws, table_pool, call, errors) @@ -1266,6 +1306,142 @@ def _WalkChecksExprs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, return +def _ParamNullFlags(pool: memhub.MemManager*, detail: str) -> str: + """Per-parameter nullability from a `def f(a: T, b: U?)` header. + + '1' = annotation contains `?`, '0' = annotation without `?`, '2' = no annotation. + """ + if detail is None: + return None + lp: str = string.strchr(detail, '(') + if lp is None: + return None + n: sizet = string.strlen(detail) + start: sizet = sizet(uint64(lp) - uint64(detail)) + 1 + if start >= n: + return None + buf: bytes = pool.alloc(n + 2) + if buf is None: + return None + cnt: int = 0 + i: sizet = start + seg_len: int = 0 + depth: int = 0 + in_ann: int = 0 + seg_null: int = 0 + while i < n: + c: int = int(detail[i]) + if depth == 0 and (c == ',' or c == ')'): + if seg_len > 0: + if in_ann == 0: buf[cnt] = 50 # '2' + elif seg_null != 0: buf[cnt] = 49 # '1' + else: buf[cnt] = 48 # '0' + cnt += 1 + if c == ')': + break + seg_len = 0 + in_ann = 0 + seg_null = 0 + i += 1 + continue + if c == '(' or c == '[' or c == '{': + depth += 1 + elif c == ')' or c == ']' or c == '}': + if depth > 0: depth -= 1 + elif c == ':' and depth == 0: + in_ann = 1 + elif c == '?' and in_ann != 0: + seg_null = 1 + elif c != ' ' and c != '\t': + seg_len += 1 + i += 1 + buf[cnt] = 0 + return str(buf) + + +def _ClassSymbolByName(a: LspSymbols.LspAnalysis*, name: str) -> LspSymbols.LspSymbol*: + """Class symbol (with its own scope) declared in this document, if any.""" + if a is None or name is None or a.symbols is None: + return None + i: int = 0 + while i < int(len(a.symbols)): + s: LspSymbols.LspSymbol* = a.symbols.get(sizet(i)) + i += 1 + if s is not None and s.kind == LspSymbols.LSP_KIND_CLASS and s.name is not None \ + and string.strcmp(s.name, name) == 0: + return s + return None + + +def _CheckCtorArgs(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, + table_pool: memhub.MemManager*, func: ast.AST*, + args: list[ast.AST*]*, errors: list[ast.ParseError*]*): + """Strict nullable flow for `new T(x)`: match args against `__init__` params or field order.""" + if func is None or args is None: + return + cname: str = None + if func.kind() == ast.ASTKind.Name: + cname = (ast.Name*)(func).id + elif func.kind() == ast.ASTKind.Attribute: + cname = (ast.Attribute*)(func).attr + if cname is None: + return + cs: LspSymbols.LspSymbol* = _ClassSymbolByName(a, cname) + if cs is None or cs.own_scope is None: + return + argc: int = int(len(args)) + if argc == 0: + return + init_s: LspSymbols.LspSymbol* = LspSymbols._FindSymbol(cs.own_scope, "__init__") + if init_s is not None: + pf: str = _ParamNullFlags(pool, init_s.detail) + if pf is None: + return + i: int = 0 + while i < argc: + arg: ast.AST* = args.get(sizet(i)) + pidx: int = i + 1 # skip self + i += 1 + if arg is None or sizet(pidx) >= string.strlen(pf): + continue + if int(pf[sizet(pidx)]) != 48: + continue + if _NullableDetail(_SymDetailAt(pool, a, arg)) == 0: + continue + msg: bytes = pool.alloc(sizet(320)) + if msg is not None: + stdio.snprintf(msg, sizet(320), + "constructor argument %d of '%s' is not nullable: a nullable value may be None; declare the parameter/field as `T?` or unwrap it with `match` first", + i, cname) + _EmitTypeError(pool, doc, arg, str(msg), errors) + return + if cs.own_scope.syms is None: + return + syms: list[LspSymbols.LspSymbol*]* = cs.own_scope.syms + fi: int = 0 + si: int = 0 + while si < int(len(syms)) and fi < argc: + s: LspSymbols.LspSymbol* = syms.get(sizet(si)) + si += 1 + if s is None or s.kind != LspSymbols.LSP_KIND_FIELD: + continue + arg: ast.AST* = args.get(sizet(fi)) + fi += 1 + if arg is None: + continue + if s.detail is not None and string.strchr(s.detail, '?') is not None: + continue + if _NullableDetail(_SymDetailAt(pool, a, arg)) == 0: + continue + msg2: bytes = pool.alloc(sizet(320)) + if msg2 is not None: + stdio.snprintf(msg2, sizet(320), + "constructor argument %d of '%s' is not nullable: a nullable value may be None; declare the parameter/field as `T?` or unwrap it with `match` first", + fi, cname) + _EmitTypeError(pool, doc, arg, str(msg2), errors) + + def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, call: ast.Call*, @@ -1309,6 +1485,7 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, fname: str = t.name if fname is None: fname = "function" + pflags: str = _ParamNullFlags(pool, t.detail) if argc > param_n and sig.has_vararg == 0: msg: bytes = pool.alloc(sizet(256)) if msg is not None: @@ -1339,13 +1516,24 @@ def _CheckCall(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, stdio.snprintf(ctx, sizet(128), "argument %d of '%s': ", ai + 1, fname) context = str(ctx) _CheckAssignValue(pool, doc, a, ws, table_pool, pt, arg, context, errors) + # Strict nullable flow: a nullable binding/field must not fill a non-nullable parameter. + if pflags is not None and arg is not None and sizet(pi) < string.strlen(pflags) \ + and int(pflags[sizet(pi)]) == 48 \ + and _NullableDetail(_SymDetailAt(pool, a, arg)) != 0: + msg_n: bytes = pool.alloc(sizet(320)) + if msg_n is not None: + stdio.snprintf(msg_n, sizet(320), + "argument %d of '%s' is not nullable: a nullable value may be None; declare it as `T?` or unwrap it with `match` first", + ai + 1, fname) + _EmitTypeError(pool, doc, arg, str(msg_n), errors) ai += 1 def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, stmts: list[ast.AST*]*, - errors: list[ast.ParseError*]*, ret_t: LspTypes.LspType* = None): + errors: list[ast.ParseError*]*, ret_t: LspTypes.LspType* = None, + ret_ann: str = None): if stmts is None: return i: int = 0 @@ -1363,14 +1551,15 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, fd: ast.FunctionDef* = ast.FunctionDef*(s) _MoveReset(pool) fn_ret: LspTypes.LspType* = None + fn_ret_ann: str = None rsl: int = 0 rsc: int = 0 rel: int = 0 rec: int = 0 if _AnnotationSpan(doc, fd, &rsl, &rsc, &rel, &rec) != 0: - fn_ret = LspTypes.ParseType( - pool, LspTypes._Trim(pool, LspSource.SliceBytes(pool, doc, rsl, rsc, rel, rec))) - _WalkChecks(pool, doc, a, ws, table_pool, fd.children, errors, fn_ret) + fn_ret_ann = LspTypes._Trim(pool, LspSource.SliceBytes(pool, doc, rsl, rsc, rel, rec)) + fn_ret = LspTypes.ParseType(pool, fn_ret_ann) + _WalkChecks(pool, doc, a, ws, table_pool, fd.children, errors, fn_ret, fn_ret_ann) continue if k == ast.ASTKind.ClassDef: _WalkChecks(pool, doc, a, ws, table_pool, s.children, errors) @@ -1386,11 +1575,11 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, _EmitTypeError(pool, doc, an.value, "a nullable value may be None; declare the target as `T?` or unwrap it with `match` first", errors) _WalkChecksExprs(pool, doc, a, ws, table_pool, an.value, errors) - if an.value is not None and an.value.kind() == ast.ASTKind.Name: - sn_v: ast.Name* = ast.Name*(an.value) - if sn_v.id is not None and _OwnHas(sn_v.id) != 0 and _AnnIsPointerLike(pool, doc, an) == 0: - _EmitTypeError(pool, doc, an.value, - "a pointer/`own` value cannot be stored into a value-typed variable; declare the target as `own T` / `T*`, or dereference explicitly (`*p`)", errors) + if an.value is not None and _AnnIsPointerLike(pool, doc, an) == 0 \ + and _NullableDetail(_SymDetailAt(pool, a, an.value)) == 0 \ + and _ValueIsPointer(pool, a, an.value) != 0: + _EmitTypeError(pool, doc, an.value, + "a pointer/`own` value cannot be stored into a value-typed variable; declare the target as `own T` / `T*`, or dereference explicitly (`*p`)", errors) if an.target is not None and an.target.kind() == ast.ASTKind.Name: tn: ast.Name* = ast.Name*(an.target) if tn.id is not None: @@ -1419,6 +1608,11 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, and _NullableDetail(_SymDetailAt(pool, a, asn.value)) != 0: _EmitTypeError(pool, doc, asn.value, "a nullable value may be None; declare the target as `T?` or unwrap it with `match` first", errors) + if tt.detail is not None and _DetailIsPointer(tt.detail) == 0 \ + and _NullableDetail(_SymDetailAt(pool, a, asn.value)) == 0 \ + and _ValueIsPointer(pool, a, asn.value) != 0: + _EmitTypeError(pool, doc, asn.value, + "a pointer/`own` value cannot be stored into a value-typed variable; declare the target as `own T` / `T*`, or dereference explicitly (`*p`)", errors) _WalkChecksExprs(pool, doc, a, ws, table_pool, asn.value, errors) if asn.value is not None and asn.value.kind() == ast.ASTKind.Name: sn_a: ast.Name* = ast.Name*(asn.value) @@ -1466,6 +1660,11 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, continue if k == ast.ASTKind.Return: rt: ast.Return* = ast.Return*(s) + if rt.value is not None and ret_ann is not None \ + and _NullableDetail(ret_ann) == 0 \ + and _NullableDetail(_SymDetailAt(pool, a, rt.value)) != 0: + _EmitTypeError(pool, doc, rt.value, + "the function's return type is not nullable: a nullable value may be None; return `T?` or unwrap it with `match` first", errors) if rt.value is not None and ret_t is not None \ and ret_t.kind != LspTypes.LT_VOID and ret_t.kind != LspTypes.LT_NONE \ and ret_t.kind != LspTypes.LT_UNKNOWN: