feat(gen): per-file strict CWA mode via from __future__ import __cwa__

A file opts into the strict NAFTAG lifetime mode with a leading

    from __future__ import __cwa__

and only that file changes: every other module keeps the legacy @life
escape analysis, so existing code is untouched.

- detect_strict_cwa publishes the flag per file (reset before each
  translation, also visible to the .vpi pass); the directive must be the
  file's first statement, a module docstring may precede it
- the directive is not a module import: import translation returns early
  and the .vpi writer skips it, so importers never look for `__future__`
- strict files reject @life/@willFree with LifetimeError; the OBS
  call-site contracts are the only boundary syntax there
- Translator.StrictCWA carries the mode for the current file
This commit is contained in:
TermiNexus committed 2026-11-01 19:03:56 +08:00
1 parent 63f3e053b2
commit 40e3388dc3
5 files changed
+101

No files matched your search

+4
View File
@@ -330,6 +330,9 @@ 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
# vpi pass so stub generation and translation agree on the mode.
GenApi.set_strict_cwa(GenApi.detect_strict_cwa(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:
@@ -360,6 +363,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()
# 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
+3
View File
@@ -1411,6 +1411,9 @@ def _GeneratePyiFromAst(mb: memhub.MemBuddy*, tree: ast.AST*, rel_path: str, buf
# ImportFrom statement
elif k == ast.ASTKind.ImportFrom:
ifr: ast.ImportFrom* = ast.ImportFrom*(stmt)
# The strict-CWA opt-in is a per-file directive, not a dependency: writing it
# into the .vpi would make importers look for a `__future__` module.
if ifr is not None and ifr.module is not None and string.strcmp(ifr.module, "__future__") == 0: continue
pos = _PyiEmit(buf, buf_size, pos, "from ")
lvl: int = ifr.level
while lvl > 0:
+80
View File
@@ -114,6 +114,82 @@ def get_current_file() -> str:
return None
# ---------------- strict NAFTAG mode, per file (`from __future__ import __cwa__`) ----------------
# 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
def set_strict_cwa(flag: int) -> None:
global _strict_cwa
_strict_cwa = flag
def strict_cwa() -> int:
return _strict_cwa
# _future_directive_names_ok - 1 when the from-import names are exactly `__cwa__`.
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
return 0
# detect_strict_cwa - detect the file-level opt-in and publish it to strict_cwa().
# 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
if tree is None or tree.children is None: return 0
ch: list[ast.AST*]* = tree.children
first: int = -1
for ci in range(int(len(ch))):
child: ast.AST* = ch.get(ci)
if child is None: continue
kd: int = child.kind()
# The module docstring is not executable code: it may precede the directive.
if first < 0 and kd == ast.ASTKind.Expr:
ex: ast.Expr* = ast.Expr*(child)
if ex is not None and ex.value is not None and ex.value.kind() == ast.ASTKind.Constant:
cst: ast.Constant* = ast.Constant*(ex.value)
if cst is not None and cst.const_kind == ast.CONST_STR: continue
if first < 0: first = ci
if kd != ast.ASTKind.ImportFrom: continue
impf: ast.ImportFrom* = ast.ImportFrom*(child)
if impf is None or impf.module is None: continue
if string.strcmp(impf.module, "__future__") != 0: continue
if first != ci:
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")
continue
_strict_cwa = 1
return _strict_cwa
# handle_future_import - 1 when node is the per-file directive, which is not a module import.
def handle_future_import(trans: HT.Translator*, node: ast.AST*) -> int:
if node is None or node.kind() != ast.ASTKind.ImportFrom: return 0
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:
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")
# ---------------- _is_ctypedef_annotation (delegates to TypeGenCType._is_ctypedef_annotation) ----------------
def _is_ctypedef_annotation(annotation: ast.AST*) -> int:
"""Delegate to gen._is_ctypedef_annotation"""
@@ -432,12 +508,14 @@ def compute_willfree_mask(fd: ast.AST*) -> int:
fname: ast.Name* = ast.Name*(dc.func)
if fname.id is None: continue
if string.strcmp(fname.id, "life") == 0:
_reject_legacy_life(deco)
# @life(<tag>, <tag>, ...):逐个标签并入
if dc.args is not None:
an: sizet = len(dc.args)
for j in range(int(an)):
mask = _WillFreeMergeTag(fn, dc.args.get(j), mask)
elif string.strcmp(fname.id, "willFree") == 0:
_reject_legacy_life(deco)
# 兼容形式:@willFree(0) 直接叠加(与 @life 等价、可混用)
mask = _WillFreeMergeTag(fn, deco, mask)
return mask
@@ -523,6 +601,7 @@ def compute_escape_directives(fd: ast.AST*, out: sizet*) -> int:
if dc.func is None or dc.func.kind() != ast.ASTKind.Name: continue
fname: ast.Name* = ast.Name*(dc.func)
if fname.id is None or string.strcmp(fname.id, "life") != 0: continue
_reject_legacy_life(deco)
if dc.args is None: continue
an: sizet = len(dc.args)
j: sizet = 0
@@ -594,6 +673,7 @@ def compute_life_anno(fd: ast.AST*, out: sizet*) -> int:
if dc.func is None or dc.func.kind() != ast.ASTKind.Name: continue
fname: ast.Name* = ast.Name*(dc.func)
if fname.id is None or string.strcmp(fname.id, "life") != 0: continue
_reject_legacy_life(deco)
if dc.args is None: continue
an: sizet = len(dc.args)
j: sizet = 0
+5
View File
@@ -11,6 +11,7 @@ import sys
import io
import lib.Stub.StubMerger as StubMerger
import lib.core.CompilerFactory as CompilerFactory
import lib.core.Gen.GenApi as GenApi
import lib.core.Gen.Translator as HT
import lib.core.builtin.ViperBuild as ViperBuild
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
@@ -672,6 +673,8 @@ 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.
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)
if resolved is not None:
@@ -721,6 +724,8 @@ novirtual class ImportsGenMixin:
"""Translate the name part of a from-import statement."""
impf: ast.ImportFrom* = ast.ImportFrom*(node)
if impf is None: return 0
# Per-file directive only; nothing is imported into the module namespace.
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)
if resolved is not None:
+9
View File
@@ -232,6 +232,11 @@ 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
# rejected and only the OBS call-site contracts are accepted.
StrictCWA: 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,
# so cross-module return-type resolution must not fail-fast in this context.
@@ -291,6 +296,7 @@ novirtual class Translator:
self.GenericTypeParamNames = None
self.GenericTypeArgs = None
self._declare_only = 0
self.StrictCWA = 0
self._pre_allow_dep_inject = 0
self.SymTab = None
# REPL mode context: unset by default (= non-REPL, all Gen-side interception bypassed)
@@ -517,6 +523,9 @@ novirtual class Translator:
# Module-level translation (user main defined) -> delegate to HandlesMain
def _translate_module_level(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, tree: ast.AST*):
"""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)
# 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)