feat(option): base?.field optional access and nullable own

Option access:
- `base?.field` evaluates to `Option[field]`: a null base yields null.
  A value field yields its address (`Option[int]` = `int*`); a pointer
  field is loaded only on the non-null branch and selected with null.
  Chaining (`n?.next?.a`) works through the pointer result.
- The AST `Attribute.optional` flag drives this in translate_attribute.

`own` nullable fix:
- `n: own U? = None` was rejected because the own-None check ignored the
  `?` wrapper. The check now skips nullable annotations, so nullable owned
  bindings accept None; non-null `own T = None` still fails.

LSP:
- member completion treats `n?.` as `n.` (base `n`, not `n?`), so fields
  and methods are offered after `?.`; semantic tokens and diagnostics
  already resolve through the optional attribute.

Probe: null `n?.a` prints the None branch; after `n = new U(5)`,
`q: Option[int] = n?.a; *q` = 5; `n?.next` null-checks; `U.__sizeof__()`
= 16; LSP completion after `n?.` offers `a`/`next` and diagnostics are
clean. Self-host 110/110; ListTest, GenericTest, GenericTest3/4,
PtrOpProbe, HashLibTest, NewTest, NewLifetimeTest, StructTest and
EscapeOkTest pass.
This commit is contained in:
TermiNexus committed 2026-11-02 20:24:34 +08:00
1 parent 3b8dc1377d
commit faf1f17b22
3 files changed
+61 -5

No files matched your search

+3 -3
View File
@@ -2786,7 +2786,7 @@ novirtual class AssignGenMixin:
nm: ast.Name* = ast.Name*(target)
if nm.id is None: return 0
if GenApi.is_none_literal(aa.value) != 0 and GenApi.annotation_is_own(aa.annotation) != 0:
if GenApi.is_none_literal(aa.value) != 0 and GenApi.annotation_is_own(aa.annotation) != 0 and GenApi.annotation_is_nullable(aa.annotation) == 0:
self.Gen.TypeGenErr.fatal_error(aa.value, f"`{nm.id}` is `own`: an owned binding must hold an object, not `None`; use `{nm.id}: own T?` for a nullable owned reference", "TypeError")
return 0
if trans.StrictOBS != 0 and GenApi.is_none_literal(aa.value) != 0:
@@ -3317,7 +3317,7 @@ novirtual class AssignGenMixin:
if annot is None:
self.Gen.TypeGenErr.fatal_error(aa, "AnnAssign is missing a type annotation", "SyntaxError")
return 0
if GenApi.is_none_literal(aa.value) != 0 and GenApi.annotation_is_own(annot) != 0:
if GenApi.is_none_literal(aa.value) != 0 and GenApi.annotation_is_own(annot) != 0 and GenApi.annotation_is_nullable(annot) == 0:
self.Gen.TypeGenErr.fatal_error(aa.value, "`own` targets must hold an object, not `None`", "TypeError")
return 0
if trans.StrictOBS != 0 and GenApi.is_none_literal(aa.value) != 0:
@@ -3556,7 +3556,7 @@ novirtual class AssignGenMixin:
nm_c: ast.Name* = ast.Name*(tgt_c)
if nm_c.id is None: continue
ve_c: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_c.id)
if GenApi.is_none_literal(asgn.value) != 0 and self.Gen.VarGen.var_is_own(trans.SymTab, nm_c.id) != 0:
if GenApi.is_none_literal(asgn.value) != 0 and self.Gen.VarGen.var_is_own(trans.SymTab, nm_c.id) != 0 and self.Gen.VarGen.var_is_nullable(trans.SymTab, nm_c.id) == 0:
self.Gen.TypeGenErr.fatal_error(asgn.value, f"`{nm_c.id}` is `own`: an owned binding must hold an object, not `None`", "TypeError")
return 0
if trans.StrictOBS != 0 and GenApi.is_none_literal(asgn.value) != 0:
+52
View File
@@ -194,6 +194,52 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
if inner_uo is not None and self.is_ptr_type(inner_uo.Ty) != 0: return inner_uo
return None
return None
def _optional_field_result(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, trans: HT.Translator*, obj_ptr: mlirlite.Value*, field_ptr: mlirlite.Value*, field_ty: mlirlite.LLVMType*, vol: int) -> mlirlite.Value*:
"""Option[field] for `base?.field`: null when the base is null."""
if pool is None or builder is None or trans is None: return None
if obj_ptr is None or obj_ptr.Ty is None or field_ptr is None or field_ty is None: return None
nulp_b: mlirlite.Value* = mlirlite.ConstNull(pool, obj_ptr.Ty, "null")
if nulp_b is None: return None
cond: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, obj_ptr, nulp_b)
if cond is None: return None
# Value field: the Option is the field address (no dereference, safe on a null base).
if self.is_ptr_type(field_ty) == 0:
fpt: mlirlite.LLVMType* = mlirlite.Ptr(pool, field_ty)
if fpt is None: return None
nulp_f: mlirlite.Value* = mlirlite.ConstNull(pool, fpt, "null")
if nulp_f is None: return None
return mlirlite.build_select(builder, cond, field_ptr, nulp_f)
# Pointer field: load only on the non-null path, then merge through a slot.
func: mlirlite.Function* = builder.Func
if func is None: return None
cnt: int = trans._label_counter
trans._label_counter = cnt + 1
b1: str = pool.alloc(40)
b2: str = pool.alloc(40)
b3: str = pool.alloc(40)
if b1 is None or b2 is None or b3 is None: return None
viperlib.snprintf(b1, 40, "opt.some.%d", cnt)
viperlib.snprintf(b2, 40, "opt.none.%d", cnt)
viperlib.snprintf(b3, 40, "opt.end.%d", cnt)
some_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, b1)
none_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, b2)
end_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, b3)
if some_bb is None or none_bb is None or end_bb is None: return None
slot: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, field_ty)
if slot is None: return None
zero: mlirlite.Value* = mlirlite.ConstZero(pool, field_ty)
if zero is None: return None
mlirlite.build_store(builder, zero, slot)
mlirlite.build_cond_br(builder, cond, some_bb, none_bb)
mlirlite.position_at_end(builder, some_bb)
v: mlirlite.Value* = mlirlite.build_load(builder, field_ty, field_ptr, vol)
if v is not None: mlirlite.build_store(builder, v, slot)
mlirlite.build_br(builder, end_bb)
mlirlite.position_at_end(builder, none_bb)
mlirlite.build_br(builder, end_bb)
mlirlite.position_at_end(builder, end_bb)
return mlirlite.build_load(builder, field_ty, slot)
def translate_attribute(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate an attribute access and return the loaded field value."""
if node is None or builder is None: return None
@@ -641,6 +687,12 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase):
self.Gen.StructGen.ensure_struct_def_in_module(pool, mod, annot_se.Ty)
if field_info is not None:
field_ty: mlirlite.LLVMType* = field_info.Ty
# `base?.field`: Option[field]. A null base yields null; a value field yields
# its address, a pointer field yields the guarded loaded pointer.
if at.optional != 0:
opt_fp: mlirlite.Value* = mlirlite.build_gep_struct(builder, struct_ty, field_ty, obj_ptr, field_info.Index)
if opt_fp is None: return None
return self._optional_field_result(pool, builder, trans, obj_ptr, opt_fp, field_ty, 0)
# t.VolatileT field: loads through the object carry the volatile qualifier
field_vol_rd: int = self.Gen.StructGen.field_is_volatile(field_info)
# merge "the field's pointee is volatile": when the base is rawptr[t.VolatileT|T],
+6 -2
View File
@@ -277,7 +277,7 @@ def _MemberCtx(pool: memhub.MemManager*, prefix: str, out_base: str*,
i: sizet = n
while i > 0:
ch: char = prefix[i - 1]
if _IsIdentChar(ch) != 0 or ch == '.':
if _IsIdentChar(ch) != 0 or ch == '.' or ch == '?':
i -= 1
continue
break
@@ -299,10 +299,14 @@ def _MemberCtx(pool: memhub.MemManager*, prefix: str, out_base: str*,
if base is None:
return 0
base[dot] = 0
out_filter[0] = LspSymbols._CopyStr(pool, run + dot + 1, 256)
# `n?.` / `n?.a`: the receiver is `n`, not `n?`.
if dot > 0 and base[dot - 1] == '?':
base[dot - 1] = 0
dot -= 1
if dot == 0 or (base[0] >= '0' and base[0] <= '9'):
return 0
out_base[0] = str(base)
out_filter[0] = LspSymbols._CopyStr(pool, run + dot + 1, 256)
return 1