fix(nll): 谓词改为按字段显式递归(AST 表达式节点不填 children,导致 new stack [2]char 被误判"提及")

根因([NLL-DBG] var=l 已证实):_set_parent_list 只设父指针、不填 children,故 New/Attribute/Call 等表达式节点
的 children 基本都是 None。原实现把"children==None"当作"提及/拿不到子树",于是 k 里第一条
`l = new stack [2]char` 就把整条判据否掉(var=h confined=0)。

现在 _NllMentions / _NllExprUnsafe 均按字段显式递归:Name/Constant/Attribute/Subscript/Call/New/UnaryOp/
BinOp/IfExp/Tuple|List|Set;认不出的种类保守(提及=1 / 不安全=1)。_NllStmts 的"认不出的语句"
分支改为按子节点判(无子节点的 pass 之类放行)。
This commit is contained in:
TermiNexus committed 2026-10-09 21:23:51 +08:00
1 parent e82e2ac768
commit 5a10a2ea10
1 file changed
+81 -25
+81 -25
View File
@@ -721,8 +721,22 @@ novirtual class StructGenMixin:
al[0] = c + 1
return 0
def _NllListMentions(self, lst: list[ast.AST*]*, al: sizet*) -> int:
"""列表里任一元素提及别名 → 1"""
if lst is None: return 0
n: sizet = len(lst)
i: sizet = 0
while i < n:
if self._NllMentions(lst.get(i), al) != 0: return 1
i += 1
return 0
def _NllMentions(self, node: ast.AST*, al: sizet*) -> int:
"""表达式/子树里是否出现别名集里的变量(保守:拿不到子树就算"出现")"""
"""表达式/子树里是否出现别名集里的变量 → 1。
**必须按字段显式递归**:AST 里 `_set_parent_list` 只设父指针、**不填 `children`**,
所以表达式节点的 `children` 基本是 None(New 已确认如此),用 children 做通用遍历会把
`new stack [2]char` 这类整块判成"提及"。认不出的种类一律保守算"提及"。"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
@@ -730,21 +744,38 @@ novirtual class StructGenMixin:
if nm.id is not None and self._NllInSet(al, nm.id) != 0: return 1
return 0
if k == ast.ASTKind.Constant: return 0
# 属性访问:变量只可能出现在基对象里(`.attr` 是静态字符串),必须显式只递归 .value。
# 否则若该节点的 children 不含 value,就会落进下面的"拿不到子树 = 算提及"分支 →
# 把 `stdio.printf(...)` 之类误判成提及/不安全,整条 NLL 判据被否掉(现场:该误判
# 使 `h = new heap S()` 永不降级,g(l, h) 照旧报逃逸)。
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
return self._NllMentions(at.value, al)
# 变量只可能在基对象里(`.attr` 是静态字符串)
return self._NllMentions(ast.Attribute*(node).value, al)
if k == ast.ASTKind.Subscript:
sb: ast.Subscript* = ast.Subscript*(node)
if self._NllMentions(sb.value, al) != 0: return 1
return self._NllMentions(sb.slice, al)
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
if self._NllMentions(cl.func, al) != 0: return 1
return self._NllListMentions(cl.args, al)
if k == ast.ASTKind.New:
nn: ast.New* = ast.New*(node)
if self._NllMentions(nn.func, al) != 0: return 1
if self._NllMentions(nn.memgr, al) != 0: return 1
return self._NllListMentions(nn.args, al)
if k == ast.ASTKind.UnaryOp:
return self._NllMentions(ast.UnaryOp*(node).operand, al)
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = ast.BinOp*(node)
if self._NllMentions(bo.left, al) != 0: return 1
return self._NllMentions(bo.right, al)
if k == ast.ASTKind.IfExp:
ie: ast.IfExp* = ast.IfExp*(node)
if self._NllMentions(ie.test, al) != 0: return 1
if self._NllMentions(ie.body, al) != 0: return 1
return self._NllMentions(ie.orelse, al)
if k == ast.ASTKind.Tuple or k == ast.ASTKind.List or k == ast.ASTKind.Set:
return self._NllListMentions(ast.Tuple*(node).elts, al)
ch: list[ast.AST*]* = node.children
if ch is None: return 1
n: sizet = len(ch)
i: sizet = 0
while i < n:
if self._NllMentions(ch.get(i), al) != 0: return 1
i += 1
return 0
if ch is not None: return self._NllListMentions(ch, al)
return 1 # 认不出的种类:保守算"提及"
def _NllAliasSource(self, value: ast.AST*, al: sizet*) -> int:
"""RHS 是否"恰好是一个别名变量"(`w = v`)→ 1"""
@@ -765,10 +796,28 @@ novirtual class StructGenMixin:
if self.has_param_escapes(fnm.id) == 0: return -1
return self.get_param_escapes(fnm.id)
def _NllListUnsafe(self, lst: list[ast.AST*]*, al: sizet*) -> int:
"""列表里任一元素处于不安全位置 → 1"""
if lst is None: return 0
n: sizet = len(lst)
i: sizet = 0
while i < n:
if self._NllExprUnsafe(lst.get(i), al) != 0: return 1
i += 1
return 0
def _NllExprUnsafe(self, node: ast.AST*, al: sizet*) -> int:
"""表达式里别名是否被放到"不安全位置"→ 1(取址 / 当被调者 / 交给会逃逸的形参位)"""
"""表达式里别名是否被放到"不安全位置"→ 1。
不安全位置 = 取址 / 当被调者 / 交给会逃逸的形参位 / **认不出的表达式种类**(保守)。
裸读(Name)、读字段或元素(Attribute/Subscript)、常量都不是不安全位置。
同样按字段显式递归(AST 的 `children` 对表达式基本为空,见 _NllMentions 注释)。"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name: return 0
if k == ast.ASTKind.Constant: return 0
if k == ast.ASTKind.Attribute: return 0
if k == ast.ASTKind.Subscript: return 0
if k == ast.ASTKind.UnaryOp:
# 取址(以及任何一元运算):保守视为不安全
uo: ast.UnaryOp* = ast.UnaryOp*(node)
@@ -791,14 +840,21 @@ novirtual class StructGenMixin:
if mask < 0: return 1
if ((mask >> idx) & 1) != 0: return 1
return 0
if k == ast.ASTKind.New:
# 构造实参里的调用同样要按不安全位置检查
return self._NllListUnsafe(ast.New*(node).args, al)
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = ast.BinOp*(node)
if self._NllExprUnsafe(bo.left, al) != 0: return 1
return self._NllExprUnsafe(bo.right, al)
if k == ast.ASTKind.IfExp:
ie: ast.IfExp* = ast.IfExp*(node)
if self._NllExprUnsafe(ie.test, al) != 0: return 1
if self._NllExprUnsafe(ie.body, al) != 0: return 1
return self._NllExprUnsafe(ie.orelse, al)
ch: list[ast.AST*]* = node.children
if ch is None: return 0
n: sizet = len(ch)
i: sizet = 0
while i < n:
if self._NllExprUnsafe(ch.get(i), al) != 0: return 1
i += 1
return 0
if ch is not None: return self._NllListUnsafe(ch, al)
return 1 # 认不出的表达式种类:保守按"不安全"处理
def _NllStmts(self, stmts: list[ast.AST*]*, al: sizet*, def_node: ast.AST*, depth: int) -> int:
"""遍历一个块;1=块内所有别名使用都帧内安全 / 0=发现外泄(放弃降级)
@@ -869,9 +925,9 @@ novirtual class StructGenMixin:
if self._NllStmts(fr.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(fr.orelse, al, def_node, depth + 1) == 0: return 0
else:
# 认不出的语句(pass/assert/del/嵌套 def 等):整棵子树不提及别名就放行,
# 提及则保守拒绝(嵌套 def 捕获闭包变量走这条)。
if self._NllMentions(st, al) != 0: return 0
# 认不出的语句(pass/assert/del/嵌套 def 等):其子节点(若有)按不安全位置检查;
# 无子节点 ⇒ 不可能外泄(`pass` 之类放行)。嵌套 def 捕获闭包变量会被判不安全。
if self._NllListUnsafe(st.children, al) != 0: return 0
return 1
def NllFrameConfined(self, trans: HT.Translator*, var_name: str, def_node: ast.AST*) -> int: