Files
2026-10-17 21:57:03 +08:00

278 lines
10 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from stdint import *
import win32.win32console
import stdio
# =============================================================================
# M3 owned slife:`new heap slife …` 的 RAII 自动释放(函数级出口统一 free)
# 设计见 Debugger/ownership-and-new.md §4.2(释放点)/ §4.3(硬约束)。
#
# 语义:`new heap slife` 得到 malloc 的对象,**由编译器独占释放**——在引入该绑定的
# 最内层作用域出口(函数的所有 return / 隐式收尾)`free` 它;赋值处若已有旧对象则
# 先"覆盖释放"(循环里反复 new 因此不累积);手动 free 被编译期拒绝(见 NewLifetimeFreeTest)。
#
# 本工程是**正向**工程:编译必须成功、运行必须退出码 0。每个 Case 返回 0 才算通过,
# main 累加失败数并在末尾打印结论。
#
# IR 断言:本工程**刻意不写任何用户级 free**(否则断言会被用户代码蒙混过关)。
# SequentialRegressionTest 的 IR_EXPECT 会检查生成的 IR 里出现 `llvm.call @free`
# (temp/**.ll 是 LLVM dialect 文本,调用写作 `llvm.call @free(%9) : (!llvm.ptr) -> ()`),
# 那只能来自 M3 的出口释放——缺释放(泄漏)是运行期看不出来的,必须直接断言 IR。
# =============================================================================
novirtual class Point:
x: int
y: int
# (1) 普通出口:字段在释放前可读,函数返回时 free(p)
def CaseBasic() -> int:
p: Point* = new heap slife Point(1, 2)
if p.x != 1: return 1
if p.y != 2: return 2
return 0
# (2) 循环内反复 new:靠"覆盖释放"不累积。
# 第二次赋值前必须先 free 旧值(缺了=泄漏,多了/漏判=double-free 崩)
def CaseLoop(n: int) -> int:
i: int = 0
while i < n:
p: Point* = new heap slife Point(i, i + 1)
if p.x != i: return 1
i += 1
return 0
# (3) 别名:P4 后 `q = p` 是**所有权转移**(q 接管、p 交出)——出口只释放 q 恰好一次
# (p 的影子已置 0,出口对它 free(NULL))。释放两次会 abort。
def CaseAlias() -> int:
p: Point* = new heap slife Point(3, 4)
q: Point* = p
if q.x != 3: return 1
return 0
# (4) 真·嵌套函数:内层在编译期与外层共享同一张登记表,必须各自隔离——
# 内层出口只能释放内层对象(否则外层对象被提前/重复释放)
def CaseNestedDef() -> int:
a: Point* = new heap slife Point(5, 6)
def Inner() -> int:
b: Point* = new heap slife Point(7, 8)
return b.y
r: int = Inner()
if r != 8: return 1
if a.x != 5: return 2
return 0
# (5) 多出口:early return 的两条路径都要释放。
# 注意返回**错误码**而不是字段值:成功分支若 `return p.x`(=9),main 里的 `fails +=`
# 会把它当成失败数累加(现场踩过:19 = 9 + 10,看着像 19 个用例错了,其实全过)。
def CaseEarlyReturn(flag: int) -> int:
p: Point* = new heap slife Point(9, 10)
if flag != 0:
if p.x != 9: return 1
return 0
if p.y != 10: return 2
return 0
# (6) void 函数 + 裸 return:裸 return 与隐式收尾两条出口都要释放
def CaseVoidBare(flag: int) -> None:
p: Point* = new heap slife Point(11, 12)
if flag != 0:
return
stdio.printf("void-slife x=%d\n", p.x)
# (7) 只有分支里才 new:flag=0 时该语句运行期不执行,出口必须 free(NULL)。
# 缺"入口块置空"就会 free 栈垃圾 → 堆损坏/abort
def CaseBranchOnly(flag: int) -> int:
p: Point* = None
if flag != 0:
p = new heap slife Point(13, 14)
if p.x != 13: return 1
return 0
# (8) stack slife:帧内 alloca,随帧消失,**不得**释放(对栈地址 free 会 abort)
def CaseStack() -> int:
p: Point* = new stack slife Point(15, 16)
if p.x != 15: return 1
return 0
# (11) P3 注解形态的属性目标:`p.x: int = 42` 必须**真的写出**。
# 此前 translate_annassign 对非 Name 目标直接 return 0,整条语句被静默丢弃
# (既不报错也不写),这里守"写出"这一半;"长命左值收短命右值"由
# NewLifetimeAnnFieldEscapeTest 守另一半。
def CaseAnnFieldStore() -> int:
p: Point* = new heap slife Point(1, 2)
p.x: int = 42
if p.x != 42: return 1
return 0
# (9) §3.6 析构调度:slife 释放时**先**调 `__del__`、**再** free(对齐 TPC MemoryOps)。
# 用一个计数全局量验证析构确实被调度,且每个释放点恰好一次。
dtor_hits: int = 0
novirtual class DtorPoint:
x: int
def __del__(self) -> None:
global dtor_hits
dtor_hits += 1
# 出口释放 → 恰好 1 次 __del__
def CaseDtor() -> int:
p: DtorPoint* = new heap slife DtorPoint(3)
if p.x != 3: return 1
return 0
# 循环内反复 new → 2 次覆盖释放 + 出口 1 次 = 3 次 __del__(析构跟释放点一一对应)
def CaseDtorOverwrite() -> int:
i: int = 0
while i < 3:
p: DtorPoint* = new heap slife DtorPoint(i)
i += 1
return 0
# (10) P1 异常/传播出口:raise 真正**离开函数**前必须释放(用 __del__ 计数验证恰好一次)。
# 对照点:被本函数 except 捕获的 raise 不得提前释放(对象还在用)——见 CaseRaiseCaught。
class MyError(Exception):
pass
def CaseRaiseExitCallee() -> int:
p: DtorPoint* = new heap slife DtorPoint(5)
if p.x != 5: return 1
raise MyError("boom") # 离开函数前:__del__ + free
def CaseRaiseExit() -> int:
before: int = dtor_hits
try:
CaseRaiseExitCallee()
except MyError:
pass
if dtor_hits != before + 1: return 1 # 多一次=double free,少一次=异常路径泄漏
return 0
# 对照:异常被**本函数**捕获时不得提前释放(对象在 except 体里仍可用),出口才释放。
# 注意:释放发生在**被调函数返回那一刻**,所以 dtor 计数必须在**调用方**检查——
# 在被调函数自己体内、return 之前检查(原写法)计数必然还没变,恒判失败(测试自身的 bug,
# 与 CaseRaiseExit 的差别正在于那边对象属于被调函数)。
def CaseRaiseCaughtInner() -> int:
ok: int = 0
try:
p: DtorPoint* = new heap slife DtorPoint(6)
raise MyError("caught")
except MyError:
# 对象必须**还活着**:若编译器在 raise 处就释放了,这里就是 use-after-free
if p.x == 6: ok = 1
if ok == 0: return 2
return 0
def CaseRaiseCaught() -> int:
before: int = dtor_hits
rc: int = CaseRaiseCaughtInner()
if rc != 0: return rc
if dtor_hits != before + 1: return 1 # 内部函数出口恰好释放一次
return 0
# P4 别名所有权转移:`q = p` 后 **q 接管生命周期**(影子标志从 p 移到 q)。
# 于是对 p 重新赋值不得释放对象(否则 q 立刻悬垂);出口只释放 q 恰好一次。
def CaseOwnershipTransfer() -> int:
before: int = dtor_hits
p: DtorPoint* = new heap slife DtorPoint(4)
q: DtorPoint* = p
p = None # P4:所有权已在 q,这一步**不得**触发析构
if dtor_hits != before: return 1 # 未转移时这里会析构一次(对象被提前释放)
if q.x != 4: return 2 # 对象必须仍然有效(值仍在)
return 0
# P4 自别名:`q = p; q = p` 不能把两个槽共持的对象释放掉
# (覆盖释放前做运行时判等,相同则跳过)。
def CaseSelfAliasTransfer() -> int:
before: int = dtor_hits
p: DtorPoint* = new heap slife DtorPoint(5)
q: DtorPoint* = p
q = p # 自别名重复赋值:不得析构
if dtor_hits != before: return 1
if q.x != 5: return 2
return 0
# 失败可诊断:每个 case 失败时打印名字与返回码(否则只有一个总数,无法定位是哪条挂了)
def Check(name: str, rc: int, fails: int) -> int:
if rc != 0:
stdio.printf(" FAIL %s: rc=%d\n", name, rc)
return fails + 1
return fails
export def main() -> int:
win32.win32console.SetConsoleCP(65001)
win32.win32console.SetConsoleOutputCP(65001)
fails: int = 0
fails = Check("CaseBasic", CaseBasic(), fails)
fails = Check("CaseLoop", CaseLoop(200), fails)
fails = Check("CaseAlias", CaseAlias(), fails)
fails = Check("CaseNestedDef", CaseNestedDef(), fails)
fails = Check("CaseEarlyReturn(1)", CaseEarlyReturn(1), fails)
fails = Check("CaseEarlyReturn(0)", CaseEarlyReturn(0), fails)
fails = Check("CaseBranchOnly(0)", CaseBranchOnly(0), fails)
fails = Check("CaseBranchOnly(1)", CaseBranchOnly(1), fails)
fails = Check("CaseStack", CaseStack(), fails)
fails = Check("CaseAnnFieldStore", CaseAnnFieldStore(), fails)
CaseVoidBare(1)
CaseVoidBare(0)
# §3.6 析构调度:出口释放恰好 1 次;循环 3 轮(2 次覆盖 + 1 次出口)共 3 次
fails = Check("CaseDtor", CaseDtor(), fails)
if dtor_hits != 1:
stdio.printf(" FAIL dtor_hits after CaseDtor: %d (want 1)\n", dtor_hits)
fails += 1
fails = Check("CaseDtorOverwrite", CaseDtorOverwrite(), fails)
if dtor_hits != 4:
stdio.printf(" FAIL dtor_hits after CaseDtorOverwrite: %d (want 4)\n", dtor_hits)
fails += 1
# P1 异常出口:传播出去(离开函数)恰好 1 次;被本函数捕获时不得提前释放、出口 1 次
fails = Check("CaseRaiseExit", CaseRaiseExit(), fails)
fails = Check("CaseRaiseCaught", CaseRaiseCaught(), fails)
# P4 别名所有权转移:函数内对 p 改写不得析构;出口释放 q 恰好一次
before_ot: int = dtor_hits
fails = Check("CaseOwnershipTransfer", CaseOwnershipTransfer(), fails)
if dtor_hits != before_ot + 1:
stdio.printf(" FAIL dtor_hits after CaseOwnershipTransfer: %d (want %d)\n", dtor_hits, before_ot + 1)
fails += 1
# P4 自别名:重复 `q = p` 不得释放共持对象;出口仍是恰好一次
before_sa: int = dtor_hits
fails = Check("CaseSelfAliasTransfer", CaseSelfAliasTransfer(), fails)
if dtor_hits != before_sa + 1:
stdio.printf(" FAIL dtor_hits after CaseSelfAliasTransfer: %d (want %d)\n", dtor_hits, before_sa + 1)
fails += 1
if fails != 0:
stdio.printf("NewLifetimeTest FAILED, %d case(s) wrong\n", fails)
return 1
stdio.printf("NewLifetimeTest OK: owned-slife RAII cases passed\n")
return 0