feat(obs): default T to Box[T], explicit Box/RawT/Option, move analysis

Rename the per-file opt-in from `__cwa__` to `from __future__ import __obs__`
(internal StrictCWA becomes StrictOBS).

OBS type model:
- `Box[T]` / `BoxT[T]` is one owned indirection (`Ptr(T)`); a user generic
  class named Box keeps precedence.
- `RawT[T]` is the unboxed value type; primitives, arrays and pointers stay raw.
- `Option[T]` is the explicit spelling of the existing `T?` suffix
  (parser marker renamed to OptionT; NullableT still accepted) and resolves
  to a nullable pointer.
- Declaration annotations (locals, globals, parameters, returns) get an
  `AutoBoxT[...]` wrapper in OBS files before the vpi pass, so struct/class
  types become `Box[T]` while primitives stay raw; struct field declarations
  are left untouched, preserving the layout (`U.__sizeof__()` stays 16).
  Explicit `Box[...]`/`RawT[...]`/`Option[...]`/`T?` are not wrapped again.

Ownership:
- an OBS move (`b: T = a` where `a` owns a heap object) marks the source
  slot as released, so later uses (including `a.field`) fail fast; rebinding
  clears the state.

LSP: `Box`, `RawT` and `Option` are offered as builtin completions.

Probe: `_U: U = new U(1,2,3,4); _B: U = _U; print(U.__sizeof__())` prints 16 and
`print(_U.a)` fails with LifetimeError; Box[U]/Option[U]/U? declarations work;
`Box[U] = None` is rejected; `__cwa__` is rejected and `__obs__` accepted.
Self-host 110/110; ListTest, GenericTest, GenericTest3, GenericTest4,
PtrOpProbe, HashLibTest, NewTest, NewLifetimeTest, StructTest and
EscapeOkTest pass.
This commit is contained in:
TermiNexus committed 2026-11-02 18:10:34 +08:00
1 parent 8c72adde88
commit 1581aa0d84
11 files changed
+163 -50

No files matched your search

+6 -3
View File
@@ -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
+8 -6
View File
@@ -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))
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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)
+99 -21
View File
@@ -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) ----------------
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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)
+33 -6
View File
@@ -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.<name>[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 \
+3
View File
@@ -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")