diff --git a/App/lib/Phase/BuildPipeline.vp b/App/lib/Phase/BuildPipeline.vp index 2e9193c..40be331 100644 --- a/App/lib/Phase/BuildPipeline.vp +++ b/App/lib/Phase/BuildPipeline.vp @@ -330,9 +330,12 @@ def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, c err_col: int = ast.get_parse_err_col() VLogger.error(f"parse failed: {file_path} (line {err_line} col {err_col})", "PARSE") return None - # Per-file strict NAFTAG opt-in (`from __future__ import __cwa__`): detect it before the + # Per-file strict NAFTAG opt-in (`from __future__ import __obs__`): detect it before the # vpi pass so stub generation and translation agree on the mode. - GenApi.set_strict_cwa(GenApi.detect_strict_cwa(tree)) + GenApi.set_strict_obs(GenApi.detect_strict_obs(tree)) + # OBS default boxing: rewrite declaration annotations (`T` -> `AutoBoxT[T]`) before the + # vpi pass so stubs and callers agree with the translated layout. + if GenApi.strict_obs() != 0: GenApi.apply_obs_autobox(mb, tree) # Generate the .vpi stub from the AST before tr.translate (same as Phase1); it lets # other modules query function signatures and TypedefT definitions. if temp_dir is not None: @@ -363,7 +366,7 @@ def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, c # App pre-registration (Phase A-pre) switch: when 1, import handling + dependency vpi # injection also run in the declare phase. includes pre-registration must pass 0. tr._pre_allow_dep_inject = pre_allow_dep_inject - tr.StrictCWA = GenApi.strict_cwa() + tr.StrictOBS = GenApi.strict_obs() # P2: reuse the global gen singleton (created by App/main.vp); create it as a fallback # when unregistered (defensive for direct-call paths). gen: GenGenerator.Generator* = GenGenerator._g_gen diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 61014d9..f825b7d 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -1741,6 +1741,8 @@ novirtual class AssignGenMixin: self.Gen.VarGen._SlifeNoteStore(pool, builder, tsh, 1) ssh: mlirlite.Value* = self.Gen.VarGen._SlifeShadow(src_slot) if ssh is not None: self.Gen.VarGen._SlifeNoteStore(pool, builder, ssh, 0) + # OBS move semantics: the source no longer owns the value. + if trans.StrictOBS != 0: self.Gen.VarGen._FreedMark(src_slot) return 1 self._SlifeDropOwned(pool, builder, mod, trans, target_slot) return 0 @@ -2771,9 +2773,9 @@ novirtual class AssignGenMixin: nm: ast.Name* = ast.Name*(target) if nm.id is None: return 0 - if trans.StrictCWA != 0 and GenApi.is_none_literal(aa.value) != 0: + if trans.StrictOBS != 0 and GenApi.is_none_literal(aa.value) != 0: if GenApi.annotation_is_nullable(aa.annotation) == 0: - self.Gen.TypeGenErr.fatal_error(aa.value, f"`{nm.id}` is not nullable: in a strict CWA file `None` requires the nullable declaration (`{nm.id}: T?`)", "TypeError") + self.Gen.TypeGenErr.fatal_error(aa.value, f"`{nm.id}` is not nullable: in an OBS file `None` requires the nullable declaration (`{nm.id}: T?`)", "TypeError") return 0 # Pure locals are value copies too: a pointer/array local would let pure code alias or @@ -3297,9 +3299,9 @@ novirtual class AssignGenMixin: if annot is None: self.Gen.TypeGenErr.fatal_error(aa, "AnnAssign is missing a type annotation", "SyntaxError") return 0 - if trans.StrictCWA != 0 and GenApi.is_none_literal(aa.value) != 0: + if trans.StrictOBS != 0 and GenApi.is_none_literal(aa.value) != 0: if GenApi.annotation_is_nullable(annot) == 0: - self.Gen.TypeGenErr.fatal_error(aa.value, "the annotated target is not nullable: in a strict CWA file `None` requires a nullable type (`T?`)", "TypeError") + self.Gen.TypeGenErr.fatal_error(aa.value, "the annotated target is not nullable: in an OBS file `None` requires a nullable type (`T?`)", "TypeError") return 0 # 与 Name 路径同口径解析注解:先特化泛型(避免 list[X] 退化成裸 list*),解析失败即 fail-fast。 self.Gen.TypeGenResolve.reject_ref_marker_position(annot, trans._imported_modules, "an annotated assignment to an attribute/subscript") @@ -3533,9 +3535,9 @@ 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 trans.StrictCWA != 0 and GenApi.is_none_literal(asgn.value) != 0: + if trans.StrictOBS != 0 and GenApi.is_none_literal(asgn.value) != 0: 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 a strict CWA file `None` requires the nullable declaration (`{nm_c.id}: T?`)", "TypeError") + 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 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)) diff --git a/App/lib/core/Gen/Expr/ExprGenAttr.vp b/App/lib/core/Gen/Expr/ExprGenAttr.vp index cd003d3..9300e10 100644 --- a/App/lib/core/Gen/Expr/ExprGenAttr.vp +++ b/App/lib/core/Gen/Expr/ExprGenAttr.vp @@ -527,7 +527,7 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): else: obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) if self.Gen.VarGen._FreedCheck(obj_ptr) != 0: - self.Gen.TypeGenErr.fatal_error(at.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(at.value, f"cannot use '{nm.id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError") return None # non-Name path: translate the object value (which loads) @@ -945,7 +945,7 @@ novirtual class ExprGenAttr(ExprGenBase.ExprGenBase): else: obj_ptr = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) if self.Gen.VarGen._FreedCheck(obj_ptr) != 0: - self.Gen.TypeGenErr.fatal_error(at.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(at.value, f"cannot use '{nm.id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError") return None # module attribute access (write path): sys._mbuddy = mb / os._win32._mbuddy = v → store to the module diff --git a/App/lib/core/Gen/Expr/ExprGenName.vp b/App/lib/core/Gen/Expr/ExprGenName.vp index 7a10d7a..0435a5a 100644 --- a/App/lib/core/Gen/Expr/ExprGenName.vp +++ b/App/lib/core/Gen/Expr/ExprGenName.vp @@ -42,7 +42,7 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase): # 只在**读**路径检查;对同名变量重新赋值会清除失效态(见 AssignGenMixin)。 if trans is not None: if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_id)) != 0: - self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(node, f"cannot use '{nm_id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError") # DefineT compile-time constant: return the constant (float -> double, int -> i32). # Existing variables win over DefineT table entries injected from vpi stubs (lookup_var guard). cdef_val_d: double = self.Gen.TypeGenCDef.lookup_cdefine_double(nm_id) @@ -536,7 +536,7 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase): return None alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) if self.Gen.VarGen._FreedCheck(alloca) != 0: - self.Gen.TypeGenErr.fatal_error(sub.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(sub.value, f"cannot use '{nm.id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError") return None if alloca is not None and alloca.Ty is not None: if self.is_ptr_type(alloca.Ty) != 0: @@ -848,7 +848,7 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase): if GenApi.is_nonlocal_name(trans, nm.id) != 0: return self._nonlocal_subscript_ptr(builder, pool, trans, nm.id, idx_val) alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id) if self.Gen.VarGen._FreedCheck(alloca) != 0: - self.Gen.TypeGenErr.fatal_error(sub.value, f"cannot use '{nm.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(sub.value, f"cannot use '{nm.id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError") return None if alloca is not None and alloca.Ty is not None: if self.is_ptr_type(alloca.Ty) != 0: diff --git a/App/lib/core/Gen/Expr/ExprGenOps.vp b/App/lib/core/Gen/Expr/ExprGenOps.vp index 7aa3bc9..d8856b7 100644 --- a/App/lib/core/Gen/Expr/ExprGenOps.vp +++ b/App/lib/core/Gen/Expr/ExprGenOps.vp @@ -918,7 +918,7 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase): if nm_ao.id is not None: # P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径) if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)) != 0: - self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released (by `del` or a @life(willFree(...)) call)", "LifetimeError") + self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released (by `del`, a move, or a @life(willFree(...)) call)", "LifetimeError") # self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))). if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0: se_ao: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ao.id) diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index b2105d6..40609c8 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -114,19 +114,19 @@ def get_current_file() -> str: return None -# ---------------- strict NAFTAG mode, per file (`from __future__ import __cwa__`) ---------------- +# ---------------- OBS mode, per file (`from __future__ import __obs__`) ---------------- # The opt-in is per file, so the flag is reset for every translation (BuildPipeline calls -# detect_strict_cwa before the vpi pass). Legacy @life annotations are rejected when set. -_strict_cwa: int = 0 +# detect_strict_obs before the vpi pass). Legacy @life annotations are rejected when set. +_strict_obs: int = 0 -def set_strict_cwa(flag: int) -> None: - global _strict_cwa - _strict_cwa = flag +def set_strict_obs(flag: int) -> None: + global _strict_obs + _strict_obs = flag -def strict_cwa() -> int: - return _strict_cwa +def strict_obs() -> int: + return _strict_obs # annotation_is_nullable - 1 when a type annotation carries the `?` suffix (`NullableT[T]`). @@ -136,7 +136,9 @@ def annotation_is_nullable(ann: ast.AST*) -> int: if sb.value is None or sb.value.kind() != ast.ASTKind.Name: return 0 nm: ast.Name* = ast.Name*(sb.value) 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, "Option") == 0: return 1 return 0 @@ -149,21 +151,97 @@ def is_none_literal(node: ast.AST*) -> int: return 0 -# _future_directive_names_ok - 1 when the from-import names are exactly `__cwa__`. +# _obs_wrap_autobox - wrap a bare declaration annotation in AutoBoxT for the OBS default. +# Only simple names / module-qualified names are wrapped: pointers, arrays, generics and +# `T?` already carry their own shape. DefineT/TypedefT are compile-time markers, not types. +def _obs_wrap_autobox(pool: memhub.MemManager*, ann: ast.AST*) -> ast.AST*: + if ann is None: return None + k: int = ann.kind() + if k != ast.ASTKind.Name and k != ast.ASTKind.Attribute: return ann + if k == ast.ASTKind.Name: + nm: ast.Name* = ast.Name*(ann) + if nm.id is None: return ann + if string.strcmp(nm.id, "None") == 0 or string.strcmp(nm.id, "void") == 0: return ann + if string.strcmp(nm.id, "DefineT") == 0 or string.strcmp(nm.id, "TypedefT") == 0: return ann + wn: ast.AST* = ast.Name(pool, "AutoBoxT", ast.ASTCtx.Load) + if wn is None: return ann + node: ast.AST* = ast.Subscript(pool, wn, ann, ast.ASTCtx.Load) + if node is None: return ann + node.lineno = ann.lineno + node.col_offset = ann.col_offset + return node + + +def _obs_box_arg(pool: memhub.MemManager*, ag: ast.AST*) -> None: + if ag is None or ag.kind() != ast.ASTKind.Arg: return + a2: ast.Arg* = ast.Arg*(ag) + a2.annotation = _obs_wrap_autobox(pool, a2.annotation) + + +def _obs_box_args(pool: memhub.MemManager*, args_node: ast.AST*) -> None: + if args_node is None or args_node.kind() != ast.ASTKind.Arguments: return + ar: ast.Arguments* = ast.Arguments*(args_node) + i: int = 0 + if ar.args is not None: + while i < int(len(ar.args)): + _obs_box_arg(pool, ar.args.get(i)) + i += 1 + _obs_box_arg(pool, ar.vararg) + _obs_box_arg(pool, ar.kwarg) + + +# _obs_box_walk - apply the OBS default boxing to declarations (not struct fields). +def _obs_box_walk(pool: memhub.MemManager*, node: ast.AST*, in_class: int) -> None: + if node is None: return + k: int = node.kind() + if k == ast.ASTKind.ClassDef: + # Direct AnnAssign children are field declarations: their layout stays raw. + if node.children is not None: + i: int = 0 + while i < int(len(node.children)): + ch: ast.AST* = node.children.get(i) + if ch is not None and ch.kind() != ast.ASTKind.AnnAssign: _obs_box_walk(pool, ch, 1) + i += 1 + return + if k == ast.ASTKind.AnnAssign: + if in_class == 0: + aa: ast.AnnAssign* = ast.AnnAssign*(node) + aa.annotation = _obs_wrap_autobox(pool, aa.annotation) + return + if k == ast.ASTKind.FunctionDef: + fd: ast.FunctionDef* = ast.FunctionDef*(node) + _obs_box_args(pool, fd.args) + fd.returns = _obs_wrap_autobox(pool, fd.returns) + in_class = 0 + if node.children is not None: + i: int = 0 + while i < int(len(node.children)): + _obs_box_walk(pool, node.children.get(i), in_class) + i += 1 + + +# apply_obs_autobox - OBS default boxing: `T` declaration annotations become `Box[T]`. +def apply_obs_autobox(pool: memhub.MemManager*, tree: ast.AST*) -> int: + if tree is None: return 0 + _obs_box_walk(pool, tree, 0) + return 1 + + +# _future_directive_names_ok - 1 when the from-import names are exactly `__obs__`. def _future_directive_names_ok(impf: ast.ImportFrom*) -> int: if impf is None or impf.names is None or len(impf.names) != 1: return 0 al: ast.Alias* = ast.Alias*(impf.names.get(0)) if al is None or al.name is None or al.asname is not None: return 0 - if string.strcmp(al.name, "__cwa__") == 0: return 1 + if string.strcmp(al.name, "__obs__") == 0: return 1 return 0 -# detect_strict_cwa - detect the file-level opt-in and publish it to strict_cwa(). +# detect_strict_obs - detect the file-level opt-in and publish it to strict_obs(). # The directive must be the first statement (a module docstring may precede it); # a `from __future__ import ...` anywhere else fails fast. -def detect_strict_cwa(tree: ast.AST*) -> int: - global _strict_cwa - _strict_cwa = 0 +def detect_strict_obs(tree: ast.AST*) -> int: + global _strict_obs + _strict_obs = 0 if tree is None or tree.children is None: return 0 ch: list[ast.AST*]* = tree.children first: int = -1 @@ -186,10 +264,10 @@ def detect_strict_cwa(tree: ast.AST*) -> int: fatal_error(child, "`from __future__ import ...` must be the first statement of the file", "SyntaxError") continue if _future_directive_names_ok(impf) == 0: - fatal_error(child, "unsupported `__future__` import: only `from __future__ import __cwa__` is known", "SyntaxError") + fatal_error(child, "unsupported `__future__` import: only `from __future__ import __obs__` is known", "SyntaxError") continue - _strict_cwa = 1 - return _strict_cwa + _strict_obs = 1 + return _strict_obs # handle_future_import - 1 when node is the per-file directive, which is not a module import. @@ -198,16 +276,16 @@ def handle_future_import(trans: HT.Translator*, node: ast.AST*) -> int: impf: ast.ImportFrom* = ast.ImportFrom*(node) if impf is None or impf.module is None: return 0 if string.strcmp(impf.module, "__future__") != 0: return 0 - # Not the first statement (detect_strict_cwa would have accepted it) or a nested copy. - if trans is not None and trans.StrictCWA == 0: + # Not the first statement (detect_strict_obs would have accepted it) or a nested copy. + if trans is not None and trans.StrictOBS == 0: fatal_error(node, "`from __future__ import ...` must be the first statement of the file", "SyntaxError") return 1 # _reject_legacy_life - fail fast when a strict file still carries an @life contract. def _reject_legacy_life(node: ast.AST*) -> None: - if strict_cwa() == 0: return - fatal_error(node, "@life/@willFree is not available in a strict CWA file (`from __future__ import __cwa__`); write the boundary contract at the call site instead", "LifetimeError") + if strict_obs() == 0: return + fatal_error(node, "@life/@willFree is not available in an OBS file (`from __future__ import __obs__`); write the boundary contract at the call site instead", "LifetimeError") # ---------------- _is_ctypedef_annotation (delegates to TypeGenCType._is_ctypedef_annotation) ---------------- diff --git a/App/lib/core/Gen/ImportsGenMixin.vp b/App/lib/core/Gen/ImportsGenMixin.vp index 817ed09..077314c 100644 --- a/App/lib/core/Gen/ImportsGenMixin.vp +++ b/App/lib/core/Gen/ImportsGenMixin.vp @@ -673,7 +673,7 @@ novirtual class ImportsGenMixin: """Translate the module part of a from-import statement.""" impf: ast.ImportFrom* = ast.ImportFrom*(node) if impf is None: return 0 - # `from __future__ import __cwa__` is a per-file mode switch, not a module import. + # `from __future__ import __obs__` is a per-file mode switch, not a module import. if GenApi.handle_future_import(trans, node) != 0: return 0 # Resolve the module name: relative imports need the package prefix resolved: str = self._resolve_relative_module(trans.Pool, trans.CurrentPackage, impf.level, impf.module) diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index 5ce836e..11d21f1 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -435,10 +435,10 @@ novirtual class StmtGenCifMixin: builder: mlirlite.IRBuilder* = trans._cur_builder mod: mlirlite.LLVMModule* = trans.Module - if trans.StrictCWA != 0 and GenApi.is_none_literal(rt.value) != 0: + if trans.StrictOBS != 0 and GenApi.is_none_literal(rt.value) != 0: fd_ret: ast.FunctionDef* = ast.FunctionDef*(self.Gen.StructGen.NllCurFunc) 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 a strict CWA file `None` requires a nullable return type (`-> T?`)", "TypeError") + 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 val: mlirlite.Value* = None diff --git a/App/lib/core/Gen/Translator.vp b/App/lib/core/Gen/Translator.vp index 996ed1e..b92eae7 100644 --- a/App/lib/core/Gen/Translator.vp +++ b/App/lib/core/Gen/Translator.vp @@ -234,10 +234,10 @@ novirtual class Translator: # Phase 1a declare mode: 1=register struct/enum/union only (no body), 0=full translation _declare_only: int - # Per-file strict NAFTAG mode: 1 when the file opted in with - # `from __future__ import __cwa__`. The legacy @life boundary syntax is then + # Per-file OBS mode: 1 when the file opted in with + # `from __future__ import __obs__`. The legacy @life boundary syntax is then # rejected and only the OBS call-site contracts are accepted. - StrictCWA: int + StrictOBS: int # Expression-statement context: 1 = the surrounding statement discards the call result # (e.g. a bare `VLogger.error(...)` line). A placeholder return type is then harmless, @@ -299,7 +299,7 @@ novirtual class Translator: self.GenericTypeParamNames = None self.GenericTypeArgs = None self._declare_only = 0 - self.StrictCWA = 0 + self.StrictOBS = 0 self._pre_allow_dep_inject = 0 self.SymTab = None # REPL mode context: unset by default (= non-REPL, all Gen-side interception bypassed) @@ -522,7 +522,7 @@ novirtual class Translator: """Delegate to GenApi.translate_children() to translate module-level statements (trans arg).""" # Per-file strict NAFTAG opt-in: must be known before imports/classes/functions are # translated, so every GenApi check in this file sees the same mode. - self.StrictCWA = GenApi.detect_strict_cwa(tree) + self.StrictOBS = GenApi.detect_strict_obs(tree) # File-level class-name pre-registration must run in all three modes: it is the only source of # evidence #5 for `_norm_type_arg_name` rule 5.5 and is hoisted above the _declare_only gate. GenApi.register_file_classes(pool, self.ModuleSha1, tree) diff --git a/App/lib/core/Gen/Type/TypeGenResolve.vp b/App/lib/core/Gen/Type/TypeGenResolve.vp index adbc428..35d0f32 100644 --- a/App/lib/core/Gen/Type/TypeGenResolve.vp +++ b/App/lib/core/Gen/Type/TypeGenResolve.vp @@ -1584,11 +1584,15 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): if k == ast.ASTKind.Subscript: sub: ast.Subscript* = ast.Subscript*(node) if sub.value is None: return None - # `T?` carries no class name of its own: unwrap to the annotated type. + # Type wrappers carry no class name of their own: unwrap to the annotated type. if sub.value.kind() == ast.ASTKind.Name: nul_nm2: ast.Name* = ast.Name*(sub.value) - if nul_nm2.id is not None and string.strcmp(nul_nm2.id, "NullableT") == 0: - return self.extract_class_name_from_annotation(sub.slice, imported_modules, pool, from_imports) + if nul_nm2.id is not None: + if string.strcmp(nul_nm2.id, "NullableT") == 0 or string.strcmp(nul_nm2.id, "OptionT") == 0 \ + or string.strcmp(nul_nm2.id, "Option") == 0 or string.strcmp(nul_nm2.id, "Box") == 0 \ + or string.strcmp(nul_nm2.id, "BoxT") == 0 or string.strcmp(nul_nm2.id, "RawT") == 0 \ + or string.strcmp(nul_nm2.id, "AutoBoxT") == 0: + return self.extract_class_name_from_annotation(sub.slice, imported_modules, pool, from_imports) # `attr.[T]` marker wrapper: the class name lives in the wrapped type. if self._is_cattr_marker(sub.value) != 0: if sub.slice is not None: return self.extract_class_name_from_annotation(sub.slice, imported_modules, pool, from_imports) @@ -2267,14 +2271,37 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): if sub.value.kind() == ast.ASTKind.Name: gen_nm: ast.Name* = ast.Name*(sub.value) if gen_nm.id is not None: - # Nullable suffix `T?`: a nullable class/struct value is a pointer; an existing - # pointer keeps its layer (the marker only relaxes the null check). - if string.strcmp(gen_nm.id, "NullableT") == 0: + # `T?` / `Option[T]`: nullable pointer; an existing pointer keeps its layer + # (the marker only relaxes the null check). + if string.strcmp(gen_nm.id, "OptionT") == 0 or string.strcmp(gen_nm.id, "NullableT") == 0 \ + or (string.strcmp(gen_nm.id, "Option") == 0 + and self.Gen.ClassGenGeneric._find_generic_template("Option") is None): nul_inner: mlirlite.LLVMType* = self.resolve_annotation_type(pool, sub.slice, imported_modules, from_imports, trans, depth + 1) if nul_inner is None: return None match nul_inner: case mlirlite.LLVMType.Ptr(_np): return nul_inner case _: return mlirlite.Ptr(pool, nul_inner) + # `Box[T]`: one owned indirection (explicit form of the OBS default). A user + # generic class named Box keeps precedence. + if (string.strcmp(gen_nm.id, "Box") == 0 or string.strcmp(gen_nm.id, "BoxT") == 0) \ + and self.Gen.ClassGenGeneric._find_generic_template(gen_nm.id) is None: + bx_inner: mlirlite.LLVMType* = self.resolve_annotation_type(pool, sub.slice, imported_modules, from_imports, trans, depth + 1) + if bx_inner is None: return None + match bx_inner: + case mlirlite.LLVMType.Ptr(_bp): return bx_inner + case _: return mlirlite.Ptr(pool, bx_inner) + # `RawT[T]`: the unboxed value type. + if string.strcmp(gen_nm.id, "RawT") == 0 \ + and self.Gen.ClassGenGeneric._find_generic_template("RawT") is None: + return self.resolve_annotation_type(pool, sub.slice, imported_modules, from_imports, trans, depth + 1) + # Compiler-inserted OBS default: struct/class values get one pointer layer, + # scalar/array/pointer types stay raw. + if string.strcmp(gen_nm.id, "AutoBoxT") == 0: + ab_inner: mlirlite.LLVMType* = self.resolve_annotation_type(pool, sub.slice, imported_modules, from_imports, trans, depth + 1) + if ab_inner is None: return None + match ab_inner: + case mlirlite.LLVMType.Struct(_, _, _): return mlirlite.Ptr(pool, ab_inner) + case _: return ab_inner # Builtin marker wrappers: descriptor / storage / value-reference markers carry # no LLVM type of their own, so resolve the wrapped type. if string.strcmp(gen_nm.id, "ConstT") == 0 or string.strcmp(gen_nm.id, "VolatileT") == 0 \ diff --git a/App/lib/lsp/LspCompletion.vp b/App/lib/lsp/LspCompletion.vp index a0ea9f1..a272ccc 100644 --- a/App/lib/lsp/LspCompletion.vp +++ b/App/lib/lsp/LspCompletion.vp @@ -183,6 +183,9 @@ def _AddBuiltinItems(pool: memhub.MemManager*, out: json.JsonValue*, _AddBuiltin(pool, out, seen, filter, "tuple") _AddBuiltin(pool, out, seen, filter, "malloc") _AddBuiltin(pool, out, seen, filter, "free") + _AddBuiltin(pool, out, seen, filter, "Box") + _AddBuiltin(pool, out, seen, filter, "RawT") + _AddBuiltin(pool, out, seen, filter, "Option") _AddBuiltin(pool, out, seen, filter, "sizeof") _AddBuiltin(pool, out, seen, filter, "memcpy") _AddBuiltin(pool, out, seen, filter, "memset")