feat(obs): reference fields only accept borrows

`A.n = B` where `n: &U?` is now rejected: a reference field must be initialized from `&value` / another reference (None stays allowed). Fields carry a RefKind, `ref_kind` looks through the nullable wrapper, and nullable checks treat RefT/MutRefT as transparent.
This commit is contained in:
TermiNexus committed 2026-11-04 18:34:19 +08:00
1 parent e650d8dd45
commit 4ff813f5dc
5 files changed
+49 -6

No files matched your search

+14
View File
@@ -4056,6 +4056,20 @@ novirtual class AssignGenMixin:
at_wr: ast.Attribute* = ast.Attribute*(target)
if at_wr is not None:
if self.Gen.ExprGenAttr.base_pointee_is_volatile(pool, trans, at_wr.value) != 0: vol_fw = 1
# OBS reference field: the slot holds a borrow, so only `&value` (or another
# reference) may initialize it; an owned pointer is not a reference.
fe_rf: GenTypes.FieldEntry* = self.Gen.ExprGenAttr._resolve_attr_field_entry(pool, trans, target)
if fe_rf is not None and fe_rf.RefKind != 0 and asgn.value is not None:
ref_ok: int = 0
if GenApi.is_none_literal(asgn.value) != 0: ref_ok = 1
elif asgn.value.kind() == ast.ASTKind.UnaryOp:
uo_rf: ast.UnaryOp* = ast.UnaryOp*(asgn.value)
if uo_rf.op == ast.OpKind.AddrOf: ref_ok = 1
elif asgn.value.kind() == ast.ASTKind.Name:
nm_rf: ast.Name* = ast.Name*(asgn.value)
if nm_rf.id is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, nm_rf.id) != 0: ref_ok = 1
if ref_ok == 0:
self.Gen.TypeGenErr.fatal_error(asgn.value, f"field '.{at_tg.attr}' is a reference: assign a borrow (`&value`), not an owner", "TypeError")
mlirlite.build_store(builder, store_val, field_ptr, vol_fw)
self._move_out_assigned_source(trans, asgn.value, pool, builder)
continue
+13 -1
View File
@@ -166,10 +166,13 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
field_byteorder_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
# Field nullable flag buffer (1 = `own T?` / `T?`), parallel to field_volatile_buf.
field_nullable_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
# Field reference kind buffer (0=none, 1=`&T`, 2=`&mut T` in __obs__ files).
field_refkind_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
if field_names_buf is None or field_types_buf is None or field_defaults_buf is None: return 0
if field_annot_buf is None or field_volatile_buf is None: return 0
if field_pointee_volatile_buf is None or field_const_buf is None: return 0
if field_byteorder_buf is None or field_nullable_buf is None: return 0
if field_refkind_buf is None: return 0
string.memset(field_names_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_types_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_defaults_buf, 0, 8 * GenTypes.FIELD_MAX)
@@ -179,6 +182,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
string.memset(field_const_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_byteorder_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_nullable_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_refkind_buf, 0, 8 * GenTypes.FIELD_MAX)
field_count: int = 0
@@ -232,6 +236,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
field_byteorder_buf[field_count] = sizet(pfe.ByteOrder)
# Inherit the nullable flag (unchanged after flattening)
field_nullable_buf[field_count] = sizet(pfe.Nullable)
field_refkind_buf[field_count] = sizet(pfe.RefKind)
field_count += 1
children: list[ast.AST*]* = cd.children
@@ -358,6 +363,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
else: field_byteorder_buf[field_count] = 0
# `own T?` / `T?`: direct `.` access is rejected; use `?.` or a None check.
field_nullable_buf[field_count] = sizet(self._ann_nullable(aa.annotation))
field_refkind_buf[field_count] = sizet(self.Gen.TypeGenResolve.ref_kind(aa.annotation))
else:
field_annot_buf[field_count] = 0
field_volatile_buf[field_count] = 0
@@ -365,6 +371,7 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
field_const_buf[field_count] = 0
field_byteorder_buf[field_count] = 0
field_nullable_buf[field_count] = 0
field_refkind_buf[field_count] = 0
field_count += 1
if field_count == 0:
@@ -550,6 +557,10 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
if fidx_add >= 0 and field_nullable_buf[fi] != 0:
fe_n: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(entry, fidx_add)
if fe_n is not None: fe_n.Nullable = 1
# reference field kind (`&T` / `&mut T`)
if fidx_add >= 0 and field_refkind_buf[fi] != 0:
fe_rk: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(entry, fidx_add)
if fe_rk is not None: fe_rk.RefKind = int(field_refkind_buf[fi])
# Phase 1a declaration mode: only register the struct and OOP flags, no method bodies
if trans._declare_only == 1:
@@ -572,7 +583,8 @@ novirtual class ClassGenDef(ClassGenBase.ClassGenBase):
nm: ast.Name* = ast.Name*(sub.value)
if nm.id is None: return 0
if string.strcmp(nm.id, "OptionT") == 0 or string.strcmp(nm.id, "NullableT") == 0: return 1
if string.strcmp(nm.id, "OwnT") == 0:
if string.strcmp(nm.id, "OwnT") == 0 or string.strcmp(nm.id, "RefT") == 0 \
or string.strcmp(nm.id, "MutRefT") == 0:
node = sub.slice
depth += 1
continue
+2 -1
View File
@@ -142,7 +142,8 @@ def annotation_is_nullable(ann: ast.AST*) -> int:
if nm.id is None: return 0
if string.strcmp(nm.id, "OptionT") == 0: return 1
if string.strcmp(nm.id, "NullableT") == 0: return 1
if string.strcmp(nm.id, "OwnT") != 0: return 0
if string.strcmp(nm.id, "OwnT") != 0 and string.strcmp(nm.id, "RefT") != 0 \
and string.strcmp(nm.id, "MutRefT") != 0: return 0
node = sb.slice
depth += 1
return 0
+2
View File
@@ -135,6 +135,8 @@ class FieldEntry:
# 1 when the field annotation carries the nullable `?` suffix (`own U?` / `T?`): direct
# `.` access is rejected; use `?.` or check against None first. Appended at the end.
Nullable: int
# Reference kind in an `__obs__` file: 0=not a reference, 1=`&T` (read-only), 2=`&mut T`.
RefKind: int
# ============================================================
+18 -4
View File
@@ -1389,10 +1389,24 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase):
if m == 3: return 0
return m
def ref_kind(self, node: ast.AST*) -> int:
"""Reference kind of an annotation: 0=none / 1=`&T` / 2=`&mut T`."""
m: int = self._ref_marker_name_mode(node)
if m == 1: return 1
if m == 3: return 2
"""Reference kind of an annotation: 0=none / 1=`&T` / 2=`&mut T` (looks through `?`)."""
n: ast.AST* = node
depth: int = 0
while n is not None and depth < 8:
m: int = self._ref_marker_name_mode(n)
if m == 1: return 1
if m == 3: return 2
if n.kind() != ast.ASTKind.Subscript: return 0
sub_rk: ast.Subscript* = ast.Subscript*(n)
if sub_rk.value is None or sub_rk.value.kind() != ast.ASTKind.Name: return 0
nm_rk: ast.Name* = ast.Name*(sub_rk.value)
if nm_rk.id is None: return 0
if string.strcmp(nm_rk.id, "OptionT") == 0 or string.strcmp(nm_rk.id, "NullableT") == 0 \
or string.strcmp(nm_rk.id, "OwnT") == 0:
n = sub_rk.slice
depth += 1
continue
return 0
return 0
def reject_ref_marker_position(self, node: ast.AST*, imported_modules: str, where: str) -> int:
"""Fail fast when a reference marker is used where it is not allowed."""