test(lifetime): 补 P3 注解形态 + P2 方法级/取地址的自动工程

- 新增 NewLifetimeAnnFieldEscapeTest:h.p: int* = &local → LifetimeError(P3 注解形态)

- 新增 NewLifetimeWillFreeMethodTest:r.Release(p) 后再用 p → LifetimeError(方法级标注,形参位含 self 偏移)

- 新增 NewLifetimeWillFreeAddrTest:stdlib.free(p) 后 &p → LifetimeError(取地址路径)

- NewLifetimeTest.CaseAnnFieldStore:p.x: int = 42 必须真的写进字段(正向)

- 注册进 SequentialRegressionTest / regression_test / run_lifetime_negative_tests + README
This commit is contained in:
TermiNexus committed 2026-10-09 14:55:52 +08:00
1 parent 64b0cc84f6
commit 5fa5a9f37c
11 files changed
+201 -5

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
from stdint import *
# =============================================================================
# 负向工程(**应当编译失败**):P3 —— 注解赋值的**属性目标**同样受"长命左值"约束
#
# §12-P3 的实例:`obj.f: T = <短命右值>`。与普通赋值 `obj.f = <短命右值>` 是**同一条规则**,
# 只是语法形态是 AnnAssign(带类型注解)。此前 translate_annassign 对非 Name 目标直接 return 0,
# 整条语句被**静默丢弃**(既不报错也不写出)——本工程同时守住"不静默"与"P3 必须拦截"两点。
#
# 判据(见 AssignGenMixin._translate_lvalue_annassign):
# · 左值 h.p 的存储属于**堆对象**(h 由 `new heap hlife` 得到)→ 活得比本帧长;
# · 右值 &local 是**本帧栈地址**(LIFE_FRAME)→ 出帧即成悬垂指针。
# 期望报错:LifetimeError。
#
# 对照(必须放行):`local_struct.f: int* = &local2`(左值也在本帧,FRAME)→ 两者同命,合法;
# `h.p: int* = heap_ptr`(右值生命期不短于左值)→ 合法。
# 注意 h 必须在**同一个函数**里 `new`,否则它作为形参进来时 Life=UNKNOWN(保守放行)。
# =============================================================================
novirtual class Holder:
p: int*
export def main() -> int:
h: Holder* = new heap hlife Holder()
local: int = 7
h.p: int* = &local # ← 必须被拒:注解形态的属性赋值把本帧栈地址写进堆对象字段
return 0
@@ -0,0 +1,29 @@
{
"$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json",
"name": "NewLifetimeAnnFieldEscapeTest",
"version": "1.0.0",
"source_dir": "./App",
"build_dir": "./.tpv_build",
"compiler": {
"cmd": "llc",
"flags": ["-filetype=obj", "-relocation-model=pic"]
},
"linker": {
"cmd": "clang++",
"flags": ["-lmsvcrt", "-lucrt", "-lpthread", "-lmingwex", "-lkernel32"],
"output": "NewLifetimeAnnFieldEscapeTest.exe"
},
"includes": [
"../../includes"
],
"target": {
"triple": "x86_64-pc-windows-gnu",
"datalayout": "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
},
"options": {
"slice_level": 3,
"sha1_slice_level": 1,
"target": "llvm",
"strict_mode": true
}
}
+12
View File
@@ -106,6 +106,17 @@ def CaseStack() -> int:
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
@@ -227,6 +238,7 @@ export def main() -> int:
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)
+26
View File
@@ -0,0 +1,26 @@
from stdint import *
import stdlib
# =============================================================================
# 负向工程(**应当编译失败**)
#
# P2 willFree —— **取地址**路径。变量被 @life(willFree(...)) 标注的调用消费后只是"值失效",
# 但 `&p` 同样是把指向已释放对象的指针交出去(后续解引用即 use-after-free),
# 必须与**读取**同一口径拒绝。
# 检查点:ExprGenOps 的 AddrOf 分支(`_FreedCheck`),与 ExprGenName 的读取路径同源。
#
# 本工程:`free(p)` 之后取 `&p`。
# 期望报错:LifetimeError。
# =============================================================================
def Use(x: rawptr*) -> int:
return 0
export def main() -> int:
p: rawptr = stdlib.malloc(8)
stdlib.free(p)
Use(&p) # ← 必须被拒:对已失效变量取地址
return 0
+29
View File
@@ -0,0 +1,29 @@
{
"$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json",
"name": "NewLifetimeWillFreeAddrTest",
"version": "1.0.0",
"source_dir": "./App",
"build_dir": "./.tpv_build",
"compiler": {
"cmd": "llc",
"flags": ["-filetype=obj", "-relocation-model=pic"]
},
"linker": {
"cmd": "clang++",
"flags": ["-lmsvcrt", "-lucrt", "-lpthread", "-lmingwex", "-lkernel32"],
"output": "NewLifetimeWillFreeAddrTest.exe"
},
"includes": [
"../../includes"
],
"target": {
"triple": "x86_64-pc-windows-gnu",
"datalayout": "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
},
"options": {
"slice_level": 3,
"sha1_slice_level": 1,
"target": "llvm",
"strict_mode": true
}
}
+32
View File
@@ -0,0 +1,32 @@
from stdint import *
import stdlib
# =============================================================================
# 负向工程(**应当编译失败**)
#
# P2 willFree —— **方法级**标注路径。`includes/stdlib.vp` 的 `free` 上那处 `@life(willFree(p))`
# 走的是**模块级函数调用**;本工程覆盖另一条独立链路:
# · 注册:StructGenMixin._RegisterClassMethods → _RegisterFuncWillFree(键 "Class.method");
# · 查询:CallGenCall._translate_method_call 的 wf_mask_mc = get_param_willfree_method(...);
# · 形参位映射:实参链含 self(bit 0),故用户第 ai 个实参对应形参位 (ai+1)。
#
# 本工程:`r.Release(p)` 之后 p 即失效,再 `free(p)` 必须被拒(二次释放)。
# 期望报错:LifetimeError。
# =============================================================================
novirtual class Resource:
id: int
@life(willFree(p))
def Release(self, p: rawptr) -> None:
stdlib.free(p)
export def main() -> int:
r: Resource* = new heap hlife Resource(1)
p: rawptr = stdlib.malloc(8)
r.Release(p) # ← 本调用后 p 失效(被调者带 @life(willFree(p)))
stdlib.free(p) # ← 必须被拒:p 已失效(二次释放)
return 0
@@ -0,0 +1,29 @@
{
"$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json",
"name": "NewLifetimeWillFreeMethodTest",
"version": "1.0.0",
"source_dir": "./App",
"build_dir": "./.tpv_build",
"compiler": {
"cmd": "llc",
"flags": ["-filetype=obj", "-relocation-model=pic"]
},
"linker": {
"cmd": "clang++",
"flags": ["-lmsvcrt", "-lucrt", "-lpthread", "-lmingwex", "-lkernel32"],
"output": "NewLifetimeWillFreeMethodTest.exe"
},
"includes": [
"../../includes"
],
"target": {
"triple": "x86_64-pc-windows-gnu",
"datalayout": "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
},
"options": {
"slice_level": 3,
"sha1_slice_level": 1,
"target": "llvm",
"strict_mode": true
}
}
+4 -1
View File
@@ -138,7 +138,10 @@
| NewLifetimeUseAfterFreeTest | [Test/NewLifetimeUseAfterFreeTest/](NewLifetimeUseAfterFreeTest/App/main.vp) | **编译失败**:P2 —— `free(p)` 之后读/传 `p` → `LifetimeError`(使用已释放变量) |
| NewLifetimeWillFreeOkTest | [Test/NewLifetimeWillFreeOkTest/](NewLifetimeWillFreeOkTest/App/main.vp) | **编译成功 + 退出码 0**:P2 合法用法 —— 首次 `free` 本身合法;同名变量**重新赋值**会清除失效态,其后可再用、再 `free`(防误报) |
| NewLifetimeWillFreeKwargTest | [Test/NewLifetimeWillFreeKwargTest/](NewLifetimeWillFreeKwargTest/App/main.vp) | **编译失败**:P2 **关键字实参**路径 —— `stdlib.free(p = p)` 之后再用 `p` → `LifetimeError` |
| NewLifetimeWillFreeMethodTest | [Test/NewLifetimeWillFreeMethodTest/](NewLifetimeWillFreeMethodTest/App/main.vp) | **编译失败**:P2 **方法级标注**路径 —— `r.Release(p)`(带 `@life(willFree(p))`)之后再用 `p` → `LifetimeError`(形参位含 `self` 偏移) |
| NewLifetimeWillFreeAddrTest | [Test/NewLifetimeWillFreeAddrTest/](NewLifetimeWillFreeAddrTest/App/main.vp) | **编译失败**:P2 **取地址**路径 —— `stdlib.free(p)` 之后取 `&p` → `LifetimeError`(与读取同一口径) |
| NewLifetimeFieldEscapeTest | [Test/NewLifetimeFieldEscapeTest/](NewLifetimeFieldEscapeTest/App/main.vp) | **编译失败**:P3 —— `h.p = &local`(堆对象字段收下本帧栈地址)→ `LifetimeError` |
| NewLifetimeAnnFieldEscapeTest | [Test/NewLifetimeAnnFieldEscapeTest/](NewLifetimeAnnFieldEscapeTest/App/main.vp) | **编译失败**:P3 **注解形态** —— `h.p: int* = &local`(AnnAssign 属性目标,同一条规则;也守住"非 Name 目标不得被静默丢弃")→ `LifetimeError` |
单独运行(`regression_test.py` 与 `SequentialRegressionTest.py` 都支持):
@@ -163,7 +166,7 @@ python Test/run_lifetime_negative_tests.py --exe .\TransPyV\output\TransPyV.exe
python Test/run_lifetime_negative_tests.py --exe .\TransPyV\output\TransPyV.exe --only EscapeMethodTest
```
> 默认跑 `EscapeTest` / `EscapeMethodTest` / `EscapeXmodTest` / `NewLifetimeFreeTest` / `NewLifetimeAsyncTest` / `NewLifetimeWillFreeTest` / `NewLifetimeUseAfterFreeTest` / `NewLifetimeWillFreeKwargTest` / `NewLifetimeFieldEscapeTest`:断言**编译失败且输出含 `LifetimeError`**;编译成功 = 漏检(FAIL),报错但方向不符 = SKIP。
> 默认跑 `EscapeTest` / `EscapeMethodTest` / `EscapeXmodTest` / `NewLifetimeFreeTest` / `NewLifetimeAsyncTest` / `NewLifetimeWillFreeTest` / `NewLifetimeUseAfterFreeTest` / `NewLifetimeWillFreeKwargTest` / `NewLifetimeWillFreeMethodTest` / `NewLifetimeWillFreeAddrTest` / `NewLifetimeFieldEscapeTest` / `NewLifetimeAnnFieldEscapeTest`:断言**编译失败且输出含 `LifetimeError`**;编译成功 = 漏检(FAIL),报错但方向不符 = SKIP。
负向项目在 `SequentialRegressionTest.py` 里是**反向判定**(`NEGATIVE_PROJECTS`):编译失败且输出含 `LifetimeError` 才算通过,且**不再进入运行阶段**;编译成功 = 漏检,直接终止。`regression_test.py --escape` 会**剔除**这些项目(该脚本要求"编译成功 + 能跑"),负向断言请用 `SequentialRegressionTest.py --escape` 或上面的 `run_lifetime_negative_tests.py`。
+7 -1
View File
@@ -41,7 +41,7 @@ OVERVIEW = os.path.join(TEST_DIR, "README.md")
# NewLifetimeFreeTest / NewLifetimeAsyncTest / NewLifetimeWillFreeTest /
# NewLifetimeUseAfterFreeTest(负向);
# 仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest
ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeWillFreeOkTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeFieldEscapeTest"]
ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeWillFreeOkTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeWillFreeMethodTest", "NewLifetimeWillFreeAddrTest", "NewLifetimeFieldEscapeTest", "NewLifetimeAnnFieldEscapeTest"]
# 负向项目:**期望编译失败**且输出含指定关键字(反向判定,且不运行可执行文件)。
# 这些项目里的用例故意交出本帧栈地址(或跨模块摘要路径),必须被 M1–M2.3 的
@@ -61,8 +61,14 @@ NEGATIVE_PROJECTS = {
"NewLifetimeUseAfterFreeTest": "LifetimeError",
# P2:关键字实参路径(stdlib.free(p = p) 之后再用 p)
"NewLifetimeWillFreeKwargTest": "LifetimeError",
# P2:方法级标注(r.Release(p) 之后再用 p;形参位含 self 偏移)
"NewLifetimeWillFreeMethodTest": "LifetimeError",
# P2:取地址路径(stdlib.free(p) 之后 &p)
"NewLifetimeWillFreeAddrTest": "LifetimeError",
# P3:obj.f = &local(堆对象字段收下本帧栈地址)
"NewLifetimeFieldEscapeTest": "LifetimeError",
# P3:obj.f: T = &local(注解形态的属性目标,同一条规则;防"非 Name 目标被静默丢弃")
"NewLifetimeAnnFieldEscapeTest": "LifetimeError",
}
# IR 断言:项目名 → 生成 IR 中**必须出现**的子串。
+2 -2
View File
@@ -90,12 +90,12 @@ def find_test_projects():
# NewLifetimeFreeTest / NewLifetimeAsyncTest / NewLifetimeWillFreeTest /
# NewLifetimeUseAfterFreeTest(负向);
# 仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest
ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeWillFreeOkTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeFieldEscapeTest"]
ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeWillFreeOkTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeWillFreeMethodTest", "NewLifetimeWillFreeAddrTest", "NewLifetimeFieldEscapeTest", "NewLifetimeAnnFieldEscapeTest"]
# 负向项目(**期望编译失败**,见 SequentialRegressionTest.NEGATIVE_PROJECTS):
# 本脚本按"编译必须成功 + 能跑"判定,纳入会误报,故 --escape 时剔除;
# 负向断言请用 SequentialRegressionTest(反向判定)或 run_lifetime_negative_tests.py。
NEGATIVE_PROJECT_NAMES = {"EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeFieldEscapeTest"}
NEGATIVE_PROJECT_NAMES = {"EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeWillFreeMethodTest", "NewLifetimeWillFreeAddrTest", "NewLifetimeFieldEscapeTest", "NewLifetimeAnnFieldEscapeTest"}
def find_project_file(proj_dir):
+1 -1
View File
@@ -34,7 +34,7 @@ PROJECT_ROOT = os.path.dirname(TEST_DIR)
# NewLifetimeAsyncTest:M3 §4.2(协程体里的 `new heap slife` → 释放点无法确定,fail-fast)。
# NewLifetimeWillFreeTest / NewLifetimeUseAfterFreeTest:P2(@life(willFree(p)) 标注驱动——
# 二次释放 / 释放后使用,对应正向工程 NewLifetimeWillFreeOkTest)。
NEGATIVE_PROJECTS = ["EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeFieldEscapeTest"]
NEGATIVE_PROJECTS = ["EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest", "NewLifetimeAsyncTest", "NewLifetimeWillFreeTest", "NewLifetimeUseAfterFreeTest", "NewLifetimeWillFreeKwargTest", "NewLifetimeWillFreeMethodTest", "NewLifetimeWillFreeAddrTest", "NewLifetimeFieldEscapeTest", "NewLifetimeAnnFieldEscapeTest"]
EXPECT_KEYWORD = "LifetimeError"
COMPILE_TIMEOUT = 300