diff --git a/NewLifetimeFreeTest/App/main.vp b/NewLifetimeFreeTest/App/main.vp new file mode 100644 index 0000000..644d125 --- /dev/null +++ b/NewLifetimeFreeTest/App/main.vp @@ -0,0 +1,28 @@ +from stdint import * +import w32.win32console +import t, c +from t import CInt, CExport +import stdlib + + +# ============================================================================= +# 负向工程(**应当编译失败**,对照工程:NewLifetimeTest) +# +# §4.3 判据 5:`slife` 对象由编译器独占释放 —— 禁止手动 free。 +# 理由:出口已经会自动 free 一次,再手动 free 就是 double-free。 +# 期望报错:LifetimeError。 +# ============================================================================= + + +novirtual class Point: + x: int + y: int + + +def main() -> CInt | CExport: + w32.win32console.SetConsoleCP(65001) + w32.win32console.SetConsoleOutputCP(65001) + + p: Point* = new heap slife Point(1, 2) + stdlib.free(p) # ← 必须被拒:slife 由编译器在函数出口释放 + return 0 diff --git a/NewLifetimeFreeTest/project.json b/NewLifetimeFreeTest/project.json new file mode 100644 index 0000000..3ecf55d --- /dev/null +++ b/NewLifetimeFreeTest/project.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json", + "name": "NewLifetimeFreeTest", + "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": "NewLifetimeFreeTest.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 + } +} diff --git a/NewLifetimeTest/App/main.vp b/NewLifetimeTest/App/main.vp new file mode 100644 index 0000000..09321b8 --- /dev/null +++ b/NewLifetimeTest/App/main.vp @@ -0,0 +1,131 @@ +from stdint import * +import w32.win32console +import t, c +from t import CInt, CExport +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) 别名:q 只是借用 p 的对象,出口必须只释放一次(释放两次会 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 + + +def main() -> CInt | CExport: + w32.win32console.SetConsoleCP(65001) + w32.win32console.SetConsoleOutputCP(65001) + + fails: int = 0 + fails += CaseBasic() + fails += CaseLoop(200) + fails += CaseAlias() + fails += CaseNestedDef() + fails += CaseEarlyReturn(1) + fails += CaseEarlyReturn(0) + fails += CaseBranchOnly(0) + fails += CaseBranchOnly(1) + fails += CaseStack() + CaseVoidBare(1) + CaseVoidBare(0) + + 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 diff --git a/NewLifetimeTest/project.json b/NewLifetimeTest/project.json new file mode 100644 index 0000000..e1e6bd5 --- /dev/null +++ b/NewLifetimeTest/project.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/TermiNexus/TransPyC/main/schemas/project-schema.json", + "name": "NewLifetimeTest", + "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": "NewLifetimeTest.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 + } +} diff --git a/README.md b/README.md index 61830e6..a97864b 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,8 @@ ## 一·附、生命期 / 逃逸(escape)相关测试 -验证 M1–M2.4 的生命期检查(栈地址逃逸、借用传播、函数逃逸摘要)与 `new` 描述符分配。 -因历史原因前 4 个位于**仓库根**(不在 `Test/` 下),`NewTest` 位于 `Test/`: +验证 M1–M3 的生命期检查(栈地址逃逸、借用传播、函数逃逸摘要)、`new` 描述符分配, +以及 `new heap slife` 的 RAII 自动释放(M3)。因历史原因前 4 个位于**仓库根**(不在 `Test/` 下): | 项目 | 位置 | 期望结果 | |---|---|---| @@ -131,6 +131,8 @@ | EscapeMethodTest | 仓库根 `EscapeMethodTest/` | **编译失败**:方法 / `__init__` / 嵌套函数交出栈地址 → `LifetimeError`(三用例互斥,一次开一条) | | EscapeXmodTest | 仓库根 `EscapeXmodTest/` | **编译失败**:跨模块摘要(`.vpi` 落盘 `# esc=`)路径 → `LifetimeError` | | NewTest | [Test/NewTest/](NewTest/App/main.vp) | 编译通过并输出 4 行:`stack`(显式 slife) / `heap` / `stack默认slife`(验证**缺省规则**:省略生命期时 stack→slife、heap/memgr→hlife)/ `memgr` | +| NewLifetimeTest | [Test/NewLifetimeTest/](NewLifetimeTest/App/main.vp) | 编译通过、退出码 0(**M3 owned slife RAII**:出口释放 / 循环内覆盖释放 / 别名只释放一次 / 嵌套函数登记隔离 / early return / void 裸 return / 仅分支里 new 时 `free(NULL)` / stack slife 不释放)。**另含 IR 断言**:IR 中必须出现 `llvm.call @free` | +| NewLifetimeFreeTest | [Test/NewLifetimeFreeTest/](NewLifetimeFreeTest/App/main.vp) | **编译失败**:对 `slife` 对象手动 `stdlib.free(p)` → `LifetimeError`(§4.3 判据5,编译器独占释放) | 单独运行(`regression_test.py` 与 `SequentialRegressionTest.py` 都支持): @@ -138,10 +140,16 @@ python Test/regression_test.py --escape # 只跑上表全部项目 python Test/regression_test.py --only NewTest # 只跑指定项目(逗号分隔可多个) python Test/SequentialRegressionTest.py --escape --exe --rebuild-includes +python Test/SequentialRegressionTest.py --only NewLifetimeTest --exe ``` > `--escape` / `--only` 会在 `Test/<名>` 与**仓库根** `<名>` 两处查找 `project.json` / `project.vpj`,所以根级 escape 项目也能直接跑;`--escape` 会忽略 README 的难度顺序。 +**IR 断言**:`SequentialRegressionTest.py` 的 `IR_EXPECT`(项目名 → 生成 IR 中必须出现的子串)在编译成功后、运行前,把 `{build_dir}` 下所有 `.ll` 拼起来做子串检查;找不到 `.ll` 视为断言失败(fail-fast,不静默跳过)。 +> `{build_dir}/temp/**.ll` 里是 **LLVM dialect 文本**(mlir-opt 的输入),调用形如 `llvm.call @free(%9) : (!llvm.ptr) -> ()`(不是经典 IR 的 `call void @free`),所以断言串用 `llvm.call @free`(`llvm.func @free` 是声明行,不会被匹配)。 +> 之所以加 IR 断言:M3 的"出口释放"只靠"运行没崩"证明不了——**漏了释放是纯泄漏,进程照样退出码 0**;必须直接断言释放调用出现过。 +> 因此纳入该断言的项目**不得自己写 free**(否则用户代码的 `free` 会蒙混过关):`NewLifetimeTest` 正是刻意不含任何用户级 `free`。 + **负向断言**("应报 `LifetimeError`")需要 TransPyV 编译——`NegativeTest/run_negative_tests.py` 用的是 TPC,而 `new` 描述符与 `LifetimeError` 只在 TPV 侧实现,故单独提供: ```bash @@ -149,7 +157,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`:断言**编译失败且输出含 `LifetimeError`**;编译成功 = 漏检(FAIL),报错但方向不符 = SKIP。 +> 默认跑 `EscapeTest` / `EscapeMethodTest` / `EscapeXmodTest` / `NewLifetimeFreeTest`:断言**编译失败且输出含 `LifetimeError`**;编译成功 = 漏检(FAIL),报错但方向不符 = SKIP。 负向项目在 `SequentialRegressionTest.py` 里是**反向判定**(`NEGATIVE_PROJECTS`):编译失败且输出含 `LifetimeError` 才算通过,且**不再进入运行阶段**;编译成功 = 漏检,直接终止。`regression_test.py --escape` 会**剔除**这些项目(该脚本要求"编译成功 + 能跑"),负向断言请用 `SequentialRegressionTest.py --escape` 或上面的 `run_lifetime_negative_tests.py`。 diff --git a/SequentialRegressionTest.py b/SequentialRegressionTest.py index 4e99759..f3dcfcf 100644 --- a/SequentialRegressionTest.py +++ b/SequentialRegressionTest.py @@ -36,9 +36,10 @@ OVERVIEW = os.path.join(TEST_DIR, "README.md") # ============================================================ # 单独运行支持:--escape / --only # ============================================================ -# escape 相关项目(生命期/逃逸检查 + M2.4 new 描述符): -# Test/ 下:NewTest;仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest -ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest"] +# escape 相关项目(生命期/逃逸检查 + M2.4 new 描述符 + M3 owned slife RAII): +# Test/ 下:NewTest、NewLifetimeTest(正向)、NewLifetimeFreeTest(负向); +# 仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest +ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeFreeTest"] # 负向项目:**期望编译失败**且输出含指定关键字(反向判定,且不运行可执行文件)。 # 这些项目里的用例故意交出本帧栈地址(或跨模块摘要路径),必须被 M1–M2.3 的 @@ -49,6 +50,20 @@ NEGATIVE_PROJECTS = { "EscapeTest": "LifetimeError", "EscapeMethodTest": "LifetimeError", "EscapeXmodTest": "LifetimeError", + # M3 §4.3 判据5:对 slife 对象手动 free → LifetimeError + "NewLifetimeFreeTest": "LifetimeError", +} + +# IR 断言:项目名 → 生成 IR 中**必须出现**的子串。 +# 动机:M3 owned slife 的"出口释放"只靠"运行不崩"是证明不了的(漏了释放 = 纯泄漏, +# 进程照样退出码 0),必须直接断言生成的 IR 里出现了释放调用。 +# 形式注意:`{build_dir}/temp/**.ll` 里是 **LLVM dialect 文本**(mlir-opt 的输入), +# 调用长这样 `llvm.call @free(%9) : (!llvm.ptr) -> ()`,不是经典 IR 的 `call void @free`; +# 用 `llvm.call @free` 才能匹配到调用(`llvm.func @free` 是声明那一行,不会被匹配)。 +# 约定:纳入 IR 断言的项目**不得**自己写 free,否则用户代码的 free 会把断言蒙混过关 +# (NewLifetimeTest 正是因此不含任何用户级 free)。 +IR_EXPECT = { + "NewLifetimeTest": ["llvm.call @free"], } @@ -178,6 +193,45 @@ def find_output_dirs(proj_dir, proj_config): return uniq +def find_ll_files(proj_dir, proj_config): + """收集项目构建目录下的所有 .ll(TPV 布局:{build_dir}/temp//.ll)""" + bd = proj_config.get('build_dir') or '.tpv_build' + root = os.path.abspath(os.path.join(proj_dir, bd)) + if not os.path.isdir(root): + return [] + out = [] + for dp, _dn, fns in os.walk(root): + for fn in fns: + if fn.endswith('.ll'): + out.append(os.path.join(dp, fn)) + return out + + +def check_ir_expect(proj_dir, proj_config, name): + """IR 断言:在生成的 .ll 里检查 IR_EXPECT[name] 的每个子串都出现。 + + 返回 (ok, 说明)。未配置 IR_EXPECT 的项目恒 (True, "")。 + 找不到任何 .ll 视为**失败**(fail-fast:静默跳过等于没断言)。 + """ + wants = IR_EXPECT.get(name) + if not wants: + return True, "" + lls = find_ll_files(proj_dir, proj_config) + if not lls: + return False, f"IR 断言失败:{name} 的构建目录下找不到任何 .ll(检查 build_dir 配置)" + text = "" + for p in lls: + try: + with open(p, 'r', encoding='utf-8', errors='replace') as f: + text += f.read() + except Exception as e: + return False, f"IR 断言失败:读取 {p} 出错: {e}" + missing = [w for w in wants if w not in text] + if missing: + return False, f"IR 断言失败:{name} 生成的 .ll 中未见 {missing}(共 {len(lls)} 个 .ll)" + return True, f"IR 断言通过({len(wants)} 项 / {len(lls)} 个 .ll)" + + def run_executable(exe_path): """运行单个可执行文件,返回 (returncode, stdout, stderr, timed_out) @@ -326,6 +380,15 @@ def main(): print(f"\n在 {name} 读取 project.json 失败,立即终止(请修正该文件后重跑)。") return 1 + # ---------- IR 断言(可选,见 IR_EXPECT) ---------- + ir_ok, ir_msg = check_ir_expect(proj_dir, proj_config, name) + if not ir_ok: + print(f" [IR] {ir_msg}") + print(f"\n在 {name} IR 断言失败,立即终止。") + return 1 + if ir_msg: + print(f" [IR] {ir_msg}") + output_dirs = find_output_dirs(proj_dir, proj_config) exes = [] for od in output_dirs: diff --git a/regression_test.py b/regression_test.py index e6cd0af..5dff832 100644 --- a/regression_test.py +++ b/regression_test.py @@ -85,14 +85,15 @@ def find_test_projects(): # ============================================================ # 单独运行支持:--escape / --only # ============================================================ -# escape 相关项目(生命期/逃逸检查 + M2.4 new 描述符): -# Test/ 下:NewTest;仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest -ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest"] +# escape 相关项目(生命期/逃逸检查 + M2.4 new 描述符 + M3 owned slife RAII): +# Test/ 下:NewTest、NewLifetimeTest(正向)、NewLifetimeFreeTest(负向); +# 仓库根下:EscapeTest/EscapeOkTest/EscapeMethodTest/EscapeXmodTest +ESCAPE_PROJECTS = ["EscapeTest", "EscapeOkTest", "EscapeMethodTest", "EscapeXmodTest", "NewTest", "NewLifetimeTest", "NewLifetimeFreeTest"] # 负向项目(**期望编译失败**,见 SequentialRegressionTest.NEGATIVE_PROJECTS): # 本脚本按"编译必须成功 + 能跑"判定,纳入会误报,故 --escape 时剔除; # 负向断言请用 SequentialRegressionTest(反向判定)或 run_lifetime_negative_tests.py。 -NEGATIVE_PROJECT_NAMES = {"EscapeTest", "EscapeMethodTest", "EscapeXmodTest"} +NEGATIVE_PROJECT_NAMES = {"EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest"} def find_project_file(proj_dir): diff --git a/run_lifetime_negative_tests.py b/run_lifetime_negative_tests.py index 21e07eb..b05c11e 100644 --- a/run_lifetime_negative_tests.py +++ b/run_lifetime_negative_tests.py @@ -30,7 +30,8 @@ PROJECT_ROOT = os.path.dirname(TEST_DIR) # 期望"编译失败 + 报 LifetimeError"的项目(名字;目录优先 Test/<名>,其次仓库根 <名>)。 # 注意:EscapeMethodTest / EscapeXmodTest 内部有多条互斥用例(编译在第一个错误处终止), # 每个项目只保证"至少触发一条 LifetimeError"。 -NEGATIVE_PROJECTS = ["EscapeTest", "EscapeMethodTest", "EscapeXmodTest"] +# NewLifetimeFreeTest:M3 §4.3 判据5(对 slife 对象手动 free)。 +NEGATIVE_PROJECTS = ["EscapeTest", "EscapeMethodTest", "EscapeXmodTest", "NewLifetimeFreeTest"] EXPECT_KEYWORD = "LifetimeError" COMPILE_TIMEOUT = 300