From faf1f17b22cbba6d0a08dd9620cf78fe317c3bd7 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 2 Nov 2026 20:24:34 +0800 Subject: [PATCH] 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. --- App/lib/core/Gen/AssignGenMixin.vp | 6 ++-- App/lib/core/Gen/Expr/ExprGenAttr.vp | 52 ++++++++++++++++++++++++++++ App/lib/lsp/LspCompletion.vp | 8 +++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 2943b32..b904b80 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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: diff --git a/App/lib/core/Gen/Expr/ExprGenAttr.vp b/App/lib/core/Gen/Expr/ExprGenAttr.vp index 9300e10..c6352fc 100644 --- a/App/lib/core/Gen/Expr/ExprGenAttr.vp +++ b/App/lib/core/Gen/Expr/ExprGenAttr.vp @@ -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], diff --git a/App/lib/lsp/LspCompletion.vp b/App/lib/lsp/LspCompletion.vp index ccf1b85..3fd94bc 100644 --- a/App/lib/lsp/LspCompletion.vp +++ b/App/lib/lsp/LspCompletion.vp @@ -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