feat(obs): reject nullable values in non-nullable bindings

In strict mode a `T?` binding or field may hold None, so assigning it to an `own T`, a plain pointer, a non-nullable field or a non-nullable return now fails instead of silently narrowing. Unwrap with `match` (or declare the target as `T?`). The language server publishes the same diagnostic for annotated/plain assignments.
This commit is contained in:
TermiNexus committed 2026-11-03 18:01:21 +08:00
1 parent 6073f60321
commit b17de2bb65
3 files changed
+37

No files matched your search

+22
View File
@@ -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
+7
View File
@@ -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:
+8
View File
@@ -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)