diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index b904b80..cd21b32 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -2862,6 +2862,15 @@ novirtual class AssignGenMixin: # fail-fast: AnnAssign must have an annotation self.Gen.TypeGenErr.fatal_error(aa, f"AnnAssign is missing a type annotation, variable name: {nm.id}", "SyntaxError") + # A nullable binding/field (`T?`) may hold None: in strict mode it must not flow into a + # non-nullable pointer target without an explicit unwrap (match / checked branch). + if trans.StrictOBS != 0 and aa.value is not None and var_ty is not None \ + and GenApi.annotation_is_nullable(aa.annotation) == 0 \ + and self.Gen.ExprGenBase.is_ptr_type(var_ty) != 0 \ + and self.Gen.ExprGenAttr._base_is_nullable(pool, trans, aa.value) != 0: + self.Gen.TypeGenErr.fatal_error(aa.value, f"`{nm.id}` is not nullable: a nullable value may be None; declare `{nm.id}: T?` or unwrap it with `match` first", "TypeError") + return 0 + # void cannot be a variable type (only void* and opaque-struct typedef) if var_ty is not None and self.Gen.StmtGenDecl._is_void_type(var_ty) != 0: self.Gen.TypeGenErr.fatal_error(aa, f"cannot declare a variable of void type '{nm.id}' (void may only be used for void* and typedef aliases)", "TypeError") @@ -3563,6 +3572,12 @@ novirtual class AssignGenMixin: if self.Gen.VarGen.var_is_nullable(trans.SymTab, nm_c.id) == 0: self.Gen.TypeGenErr.fatal_error(asgn.value, f"`{nm_c.id}` is not nullable: in an OBS file `None` requires the nullable declaration (`{nm_c.id}: T?`)", "TypeError") return 0 + if trans.StrictOBS != 0 and asgn.value is not None and ve_c is not None \ + and ve_c.Spec is not None and ve_c.Spec.PtrLevel > 0 \ + and self.Gen.VarGen.var_is_nullable(trans.SymTab, nm_c.id) == 0 \ + and self.Gen.ExprGenAttr._base_is_nullable(pool, trans, asgn.value) != 0: + self.Gen.TypeGenErr.fatal_error(asgn.value, f"`{nm_c.id}` is not nullable: a nullable value may be None; declare it as `T?` or unwrap it with `match` first", "TypeError") + return 0 if self.Gen.VarGen.var_is_const(ve_c) != 0: self.const_assign_fail(tgt_c, "assignment", nm_c.id, self.Gen.VarGen.var_annot_class_name(ve_c)) elif tgt_c.kind() == ast.ASTKind.Attribute: @@ -3570,6 +3585,13 @@ novirtual class AssignGenMixin: if at_c.attr is None: continue if self.Gen.ExprGenAttr.attr_field_is_const(pool, trans, tgt_c) != 0: self.const_assign_fail(tgt_c, "field assignment", at_c.attr, self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, tgt_c)) + if trans.StrictOBS != 0 and asgn.value is not None: + fe_t: GenTypes.FieldEntry* = self.Gen.ExprGenAttr._resolve_attr_field_entry(pool, trans, tgt_c) + if fe_t is not None and fe_t.Nullable == 0 and fe_t.Spec is not None \ + and fe_t.Spec.PtrLevel > 0 \ + and self.Gen.ExprGenAttr._base_is_nullable(pool, trans, asgn.value) != 0: + self.Gen.TypeGenErr.fatal_error(asgn.value, f"field '.{at_c.attr}' is not nullable: a nullable value may be None; declare it as `T?` or unwrap it with `match` first", "TypeError") + return 0 elif tgt_c.kind() == ast.ASTKind.Subscript: sub_c: ast.Subscript* = ast.Subscript*(tgt_c) if sub_c.value is None: continue diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index 11d21f1..357bd89 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -440,6 +440,13 @@ novirtual class StmtGenCifMixin: if fd_ret is not None and GenApi.annotation_is_nullable(fd_ret.returns) == 0: self.Gen.TypeGenErr.fatal_error(rt.value, "the function's return type is not nullable: in an OBS file `None` requires a nullable return type (`-> T?`)", "TypeError") return 0 + # A nullable value may be None: a non-nullable return type requires an explicit unwrap. + if trans.StrictOBS != 0 and rt.value is not None: + fd_retn: ast.FunctionDef* = ast.FunctionDef*(self.Gen.StructGen.NllCurFunc) + if fd_retn is not None and GenApi.annotation_is_nullable(fd_retn.returns) == 0 \ + and self.Gen.ExprGenAttr._base_is_nullable(pool, trans, rt.value) != 0: + self.Gen.TypeGenErr.fatal_error(rt.value, "the function's return type is not nullable: a nullable value may be None; return `T?` or unwrap it with `match` first", "TypeError") + return 0 val: mlirlite.Value* = None if rt.value is not None: diff --git a/App/lib/lsp/LspChecks.vp b/App/lib/lsp/LspChecks.vp index b48b901..76da0f8 100644 --- a/App/lib/lsp/LspChecks.vp +++ b/App/lib/lsp/LspChecks.vp @@ -1381,6 +1381,10 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, ann: str = _DeclAnnotationText(pool, doc, an.target) dst: LspTypes.LspType* = LspTypes.ParseType(pool, ann) _CheckAssignValue(pool, doc, a, ws, table_pool, dst, an.value, None, errors) + if _NullableDetail(ann) == 0 \ + and _NullableDetail(_SymDetailAt(pool, a, an.value)) != 0: + _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) @@ -1411,6 +1415,10 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, continue dst_t: LspTypes.LspType* = _TypeFromDetail(pool, tt.detail) _CheckAssignValue(pool, doc, a, ws, table_pool, dst_t, asn.value, None, errors) + if _NullableDetail(tt.detail) == 0 \ + 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) _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)