TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

25 KiB

2026-08-04

code-wiki 渲染路径修正

  • 用户将 code-wiki skill 的 render.py 渲染目录从 .workbuddy/wiki 改为 .agents/wiki
  • 实测发现:render.py 的文档注释改了,但 render()(第243行)与 browse()(第328行)函数体仍硬编码 .workbuddy。已把这两处改为 .agents 并重新渲染成功。
  • 结论:code-wiki 产物目录现统一为 .agents/wiki/(catalog.xml 在 .agents/wiki/data/,页面 md 在 .agents/wiki/pages/)。下次 render 直接 python render.py render <ws> --lang zh,无需再复制。
  • 重新渲染结果:[done] rendered 22 pages -> .agents/wiki/index.html

修复 Windows 资源编译 RC2135

  • 报错:app_resource.rc(1) : error RC2135 : file not found: by(资源编译阶段)。
  • 根因:src/Build/ResourceCompilationTrait.php 写 rc 文件时强制加了 UTF-8 BOM("\xEF\xBB\xBF" . $rcContent),而 src/Generator/ResourceFileGenerator.php 生成的注释用 //。BOM 顶在文件头导致 rc.exe 第1行识别不出 // 注释,把 Generated by TypePHP 当语句、by 当文件名 → RC2135(老版/部分 Windows SDK 的 rc.exe 有此坑)。
  • 修复:① 去掉 BOM(直接写 $rcContent);② 生成内容里的 // 注释全改为 ;(rc 全版本支持的注释符)。中文靠 #pragma code_page(65001) 正常处理,无需 BOM。.h 头文件里的 // 不动(rc 不处理)。
  • 重要:此修复需重新编译 tpc.exe 才生效——当前运行的预编译 tpc.exe 每次构建都会按自身逻辑重新生成 rc 文件,改 build/app_resource.rc 不持久。
  • 临时绕过(不重编也能跑通):把 project.yml 的 resource: 整段(第10~24行 icon + version-info)注释掉,hasResource() 返回 false,资源编译被跳过,可先验证后续链接;编好带修复的 tpc.exe 后再恢复该段即可重新带图标/版本。
  • 已应用临时绕过:project.yml 的 resource: 整段已改为全注释(带"待带修复的 tpc.exe 重新编译后恢复"标记)。用户可立即重跑 tpc.exe 验证链接链路。

LNK1104 链接失败(输出 tpc.exe 被锁)

  • 报错:LINK : fatal error LNK1104: 无法打开文件"tpc.exe"(乱码是 GBK 被当 UTF-8 解码的"无法打开文件")。
  • 根因:project.yml name: tpc → 输出 tpc.exename 无路径 → outputDir 为空,TypePHP 把最终 exe 输出到 cwd 相对路径(getTargetFileName() 返回 'tpc.exe'/OUT:"tpc.exe")。用户从编译器自身目录 D:\git\php\tpc_v1095_windows_x86_64 启动 tpc.exe(cwd=该目录),于是 /OUT:"tpc.exe" 落到正在运行的 D:\git\php\tpc_v1095_windows_x86_64\tpc.exe 上,系统拒绝改写正在执行的 exe → LNK1104。
  • 佐证:无残留 tpc 进程;build\tpc.exe 不存在;build\ 可写;中间产物 rc 在 tpc_v1095/build/ → cwd 即编译器目录。
  • 修复:从项目目录 D:\git\php\aot-compiler 启动编译器(cd 过去再用绝对路径调 tpc.exe),输出落到 aot-compiler/tpc.exe,避开与运行中编译器同名冲突。备选:给 project.yml 换输出名/输出目录(不建议改 name,会影响生成符号)。

tpc.exe 编译 hello.php 时 OOM(VirtualAlloc 失败)

  • 现象:.\tpc.exe examples/hello.php 跑到打印第3条 cl 命令(typephp_fiber_generator.cc)后,VirtualAlloc() failed [0x8 内存资源不足] x2,随后 Fatal error: Out of memory (allocated 8388608) (tried to allocate 140736835400432 ≈128TB) in src/compiler.php on line 4
  • 诊断结论:不是机器内存不够。实测本机 96GB 物理内存(空闲 74GB)、页面文件空闲 81GB、20 核。VirtualAlloc 拒绝是因为请求了一个被算坏的 ~128TB 尺寸(符号扩展/未初始化),OOM 报告把这个坏尺寸打印出来——属症状非根因。
  • Windows 上 Platform/Windows::supportsPcntlParallelCompile() 返回 false(Windows.php:226),Translator.php:1441compileSourceFile() 顺序编译passthru 逐个跑 cl),不并行-j 在此无效。
  • 隔离测试:--dry(仅 prepare+convert 生成 C++)能干净退出(exit 0,不 OOM)→ bug 只在 compile/build(链接)阶段。源码 compiler.php:8ini_set('memory_limit','-1'),且报错走的是 Zend MM "out of system memory" 路径(OS 拒 VirtualAlloc),非撞 memory_limit。
  • 诡异点:该 tpc.exe 能编译整个 tpc 工程(project.yml)却编不了 151B 的 hello.php → 疑似"单文件 .php→exe"模式的专属 bug(parseArgv 对单文件默认也是 BUILD_MODE_BIN,与 project.yml 同模式,故非 buildMode 差异)。
  • 待办/建议给用户:① .\tpc.exe project.yml -O2 -j 8(从 aot-compiler 目录)确认工程模式仍正常,隔离是否单文件模式专属;② 用 v1095 预编译 tpc.exeexamples/hello.php 对比,判断是"当前源码回归"还是"v1095 误编译";③ 若需进一步定位,查单文件 build/link 路径里计算分配尺寸的代码(读取文件/字符串长度处)。

确认:崩的是"S1+S2 优化版",v1095 基础版不崩

  • 用户确认:tpc_v1095(基础版,未做编译速度优化)编译 examples/hello.php 不崩;刚编出来的 tpc.exe(带 S1+S2:ac7813d)崩 128TB。→ 回归在 S1+S2,或 v1095 误编译了 S1+S2 源码。
  • S1+S2(ac7813d)只改 3 文件:CompilerBase.php/Preprocessor.php/Translator.php。compile 阶段新增代码只有 S2 的 generated 缓存:hasGeneratedObjectFileCache/getGeneratedObjectCacheKey/getGeneratedHeaderDependencies/isGeneratedSourceFile/compileFile$isGenerated 分支。
  • 已逐行审 S2 缓存代码:均读小 metadata、算 sha256、拼字符串,无直接大块分配;且前两个 generated 文件(hello.cc/extension-hello.cc)已成功跑过缓存检查+编译(日志里 cl 命令已打印),说明新函数本身能跑。
  • S2 设计缺陷(确定要修)getGeneratedHeaderDependencies()FilesystemIterator 扫描整个 build/include 收集所有 *_arginfo.h。但 build/include上一次 tpc 工程编译的残留污染(含 php_src_CompilerBase_arginfo.h 423KB、php_tpc_func_decl.hphp_src_Translator_arginfo.h 等几十个 tpc 工程头)。后果:① 编译 hello.php 时缓存 key 竟依赖无关 tpc 工程头文件(增量缓存正确性错误);② 每个 generated 单元算 key 都要 hash_update_file 这些大残留头,浪费且可能触发异常路径。
  • 本机 Bash 无法复现:缺 vcvars/cl 时该 AOT 二进制静默 exit 0 无输出,必须在用户带 VS 环境的命令行里测。
  • 给用户的两个隔离测试(在 VS 命令行执行):
    • 测试1:.\tpc.exe examples/hello.php --force(关掉两套缓存检查)。若通过→缓存路径是触发点。
    • 测试2(若测试1通过):rmdir /s /q build.\tpc.exe examples/hello.php 跑两次。若第1次(缓存miss)过、第2次(缓存hit,完整算key并 hash_update_file 污染头)崩→bug 在缓存命中路径的 key 计算;若第1次就崩→在 compile/link 本身(更可能是 v1095 误编译)。
  • 用户实测测试2:清空 build 后首次(缓存 miss)就崩 → 直接排除"缓存命中路径"(缓存命中路径只在第2次才触发 hash_update_file 扫描污染头;首次 $objectFile 不存在,hasGenerated/hasMiscObjectFileCache 在 1215/1123 行 early-return false,根本走不到 key 计算)。
  • 进一步逐行审查首次 compile 路径全部可能大分配点:
    • writeFile 改返回 bool:7 处调用(gen_stub/ResourceCompilationTrait/Translator 498/663/736/1100/1785)均安全,无副作用。
    • parseCachedAst/cloneAst:仅 prepare/convert 用,--dry 已验证正常。
    • compileFile 缓存 if 块(1354-1360):首次恒 false,benign。
    • NativeBuilder::compile()(31-41):Windows 顺序模式走 passthru($command,$status)不把 cl 输出累积进数组,无"读输出撑爆"路径。
    • compileSourceFile 循环(1489-1532):每文件 compileFile(false) → miss → compile() → passthru 跑 cl → 打印命令。前3个文件成功(cl 命令已打印=status 0),崩在第4个文件进入时。
  • 最终结论(高置信):S1+S2 在首次 compile 路径上没有任何大分配来源,128TB 在 PHP 源码层无出处;而 v1095 基础版编 hello.php 正常、我们的版本崩 → 判定为 v1095 编译器对我们当前源码(含 S1+S2)的误编译(miscompilation):v1095 在 AOT 编译改动后的 Translator.php 时,把 compileFile/编译循环邻近某处 size 符号扩展/算坏(典型 ~2⁴⁷)。--dry 不进 compile 循环故不触发。
  • 决定性验证 = Bootstrap 自举:用当前 tpc.exe(它编工程成功,编 hello.php 崩)重编 tpc 工程得自举版 tpc.exe-B,再用它编 hello.php。
    • 通过 → 确认 v1095 误编译,自举换了编译上下文/偏移绕开,后续用自举版做 typephp-think 速度对比即可。
    • 仍崩 → 源码确有隐藏 bug,需在 compileFile 入口加 STDERR 文件名日志 + NativeBuilder::compile 前后标记,再编一次精确定位第4个文件及崩因。
  • 待修(不影响崩溃):getGeneratedHeaderDependencies() 通配 *_arginfo.h 会纳入 build/include 残留 tpc 工程头(php_src_*),应限定为当前 $this->targetName 前缀(str_starts_with($name,'php_'.$targetName.'_') && str_ends_with($name,'_arginfo.h'))。等自举成功后再单独修并验证,避免重编混入变量。

Bootstrap 自举也崩 → 推翻"纯误编译"早期判断

  • 用户实测:.\tpc.exe project.yml -O2 -j 8(用我们的 tpc.exe 重编工程,即自举)同样崩在 typephp_fiber_generator.cc,与基础版崩溃签名完全一致(2⁴⁷)。
  • 关键修正:自举崩不能区分"v1095 误编译"与"源码真 bug"——因为自举二进制由我们的 tpc.exe(本身可能已被 v1095 误编译)再编一次,缺陷会继承传递。所以自举崩对两种假设都"符合",不是决定性证据。
  • 进一步精读定位:
    • getLanguageFromExtension('.cc') 返回 null → misc 文件走 getCompileCommandOptions() → 合法 cl /c 命令(解释了日志里 misc 文件没抛 RuntimeException,而是正常打印 cl 命令)。
    • getMiscObjectCacheKey() 只做 hash('sha256', buildCompileFileCommand(...) . "\0" . serialize($abi));而 buildCompileFileCommand(generated 文件也用)在 generated 路径已验证正常 → misc 专属 metadata 写入路径不是 2⁴⁷ 来源
    • 仓库真实状态:HEAD=ac7813d(S1+S2);工作树仅我的 RC 修复 + project.yml 注释,无其它碰编译链路的改动。
    • S1 是前端 AST 缓存(misc 文件不是 PHP,根本不进前端)→ 已排除。S2 是唯一落在 compile 阶段的改动。

二分开关:已禁用 S2 缓存逻辑(compileFile)

  • src/Translator.phpcompileFile() 里,把 S2 增量对象缓存逻辑整段去掉(缓存命中跳过 + invalidateMisc/GeneratedObjectCache + writeMisc/GeneratedObjectCacheMetadata),退回 S2 之前的纯编译行为。加了 // [bisect] 注释标记,定位清楚后删除。
  • 目的:若用 v1095 重新编译(带此开关的源码)后 .\tpc.exe examples/hello.php 不再崩 → 坐实"S2 那段代码被 v1095 AOT 误编译",下一步改写 S2 避开该结构(如去掉 SPL RecursiveIteratorIterator、改用更简单 size 计算)。
  • 若仍崩 → 不是 S2,需扩大范围(revert 全部 S1+S2、或排查 S1+S2 之外的更早提交/phpx 依赖更新 2129145)。
  • 用户重编命令(必须用 v1095,因为我们的 tpc.exe 也会崩,不能用来重编):
    • D:\git\php\aot-compiler 目录:D:\git\php\tpc_v1095_windows_x86_64\tpc.exe project.yml -O2 -j 8(RC 修复已在源码、resource 已注释,且无输出冲突 → 可编译+链接成功)。
    • 再测:.\tpc.exe examples/hello.php

坐实:S2 触发 v1095 误编译 + 已保守化重写

  • 用户实测:用 v1095 重编"二分开关版"(S2 在 compileFile 的调用整段去掉)源码,产出的 tpc2.exeexamples/hello.php 不再崩,成功产出 hello.exe(7 文件编译+链接全过)。→ 坐实 S2 那段增量对象缓存代码被 v1095 AOT 误编译(去掉调用即不再触发 2⁴⁷)。S1 已确认安全(--dry 正常)。
  • 首次编译实际会执行的 S2 高危路径:writeGeneratedObjectCacheMetadatagetGeneratedObjectCacheKey。该函数原版用了 hash_init/hash_update/hash_update_file/hash_final 哈希资源流,并调用 getGeneratedHeaderDependencies()(含 FilesystemIterator SPL 迭代器)。这两类结构在 v1095 AOT 编译下最易被误编译成坏 size(2⁴⁷ 符号扩展)。hasMiscObjectFileCache 原版也含 RecursiveIteratorIterator/RecursiveDirectoryIterator
  • 已重写 S2 为保守版(src/Translator.php),消除全部高危结构:
    1. getGeneratedObjectCacheKey:去掉 hash_init/update/update_file/final 流,改用 hash('sha256', implode("\0", $parts)) 单次 + 每个依赖头 hash_file('sha256', $header) 单次。
    2. getGeneratedHeaderDependencies:去掉 FilesystemIterator,改用 scandir 一层遍历;并把 *_arginfo.h 通配限定为当前 target 前缀str_starts_with($name,'php_'.$targetName.'_')),顺手修了"扫描整个 build/include 含 tpc 工程残留头"的设计缺陷。
    3. hasMiscObjectFileCache:去掉 RecursiveIteratorIterator,新增 collectHeaderFiles()(scandir 递归,无 SPL 迭代器)替代。
    4. getMiscObjectCacheKey 保持原 hash('sha256', ...) 单次调用(已验证安全)。
    5. 恢复 compileFile 里的 S2 调用(缓存命中跳过 + invalidate + writeMetadata),删除二分 [bisect] 标记。
  • 验证:php -l src/Translator.php 无语法错误;Grep 确认 Translator.php 内已无 RecursiveIteratorIterator/RecursiveDirectoryIterator/FilesystemIterator/hash_init/hash_update/hash_update_file/hash_final 残留。
  • 未动的同类结构FileScanner.phpBuild/PrecompiledHeaderManager.phpgen_stub.php 仍有 SPL 迭代器 + hash 资源流,但不是本次崩溃元凶(v1095 编整个工程时这些文件都被正常 AOT 编译且没崩),保持最小改动原则暂不碰。
  • 下一步验证(用户执行,必须用 v1095 重编,因为我们的 tpc.exe 自己也崩)
    cd /d D:\git\php\aot-compiler
    D:\git\php\tpc_v1095_windows_x86_64\tpc.exe project.yml -O2 -j 8   # 产新 tpc.exe:含保守版S2 + 源码内修复版ResourceFileGenerator(自身无图标,因resource注释)
    rmdir /s /q build
    .\tpc.exe examples\hello.php      # 第一次:应不崩出 hello.exe(验证S2修复)
    .\tpc.exe examples\hello.php      # 第二次:应命中缓存([cache] skip),更快(验证S2加速)
    
    • 若第一次仍崩 → 误编译源不在已改这几处,需更细二分(如逐个恢复 hasMisc/hasGenerated 与 write*Metadata)。
    • 若两次都过 → S2 修复生效。之后可恢复 project.yml 的 resource 段,用新 tpc.exe 编 project.yml 验证 RC2135 也修好且带图标。

关键反转:保守化重写的 S2 仍崩 128TB → 判定为 v1095 跨函数误编译

  • 用户实测:用 v1095 重编"保守版 S2"(去 SPL 迭代器 + 去 hash 资源流,compileFile 已恢复 S2 调用)源码,产出的 tpc2.exeexamples/hello.php 仍然崩 128TB,崩溃签名与原始版完全一致(tried to allocate 140736835400432 ≈2⁴⁷)。
  • 推翻了"是 SPL 迭代器 / hash 资源流被误编译"的假设。进一步定位:崩溃发生在首次编译 hello.cc(generated 文件)打印 cl 命令后、进入 getNativeBuilder()->compile() 时。首次编译对 hello.cc 实际执行的 S2 代码只有 isPhpxMiscFile()isGeneratedSourceFile()hasGeneratedObjectFileCache()(obj 不存在→early-return false)、invalidateGeneratedObjectCache();而我新写的保守 hash()/hash_file() 此时还没轮到(它在编译后才写的 metadata 里)。→ 崩点根本到不了哈希,说明它不是某个 helper 的内部写法问题。
  • 最终判定:v1095 在 AOT 编译 compileFile 里那段 S2 包裹代码时,连带把后续 getNativeBuilder()->compile() 编坏了(典型跨函数误编译:A 函数误编→B 函数运行时崩)。窄重写 helper 救不了,因为触发点是"这段 S2 代码被编译/被调用"这件事本身,而不是具体哪行语句。
  • 这是 v1095 编译器(AOT)的 bug,不是我们源码的逻辑 bug。在源码层面做局部改写无法可靠规避。
  • 决策:禁用 S2(从 compileFile 移除全部 S2 调用,退回纯编译态,等同于已证明稳定的 bisect 版),保留 S1(合并两次解析,主要提速点,已确认安全)。S2 的 helper 方法(hasMisc/hasGenerated/getXxxCacheKey/writeXxxMetadata/collectHeaderFiles)作为"死代码"保留(已证明:定义但不被 compileFile 调用就安全),待编译器修复后复用。
  • 当前 compileFile(src/Translator.php ~1378)已加注释明确 S2 禁用原因;php -l 通过。
  • 后续可选项(暂不阻塞)
    1. 用 v1095 重编当前源码(S1 开、S2 关)→ 应稳定,可正常编 hello.php 与 project.yml。
    2. 想保留 S2 加速的可试验:用更低优化级别(如 project.yml -O1 而非 -O2)重编 tpc.exe。若 v1095 的误编译是优化级别相关,低级别可能绕开 → 届时再恢复 compileFile 的 S2 调用测试。但 -O1 会让 tpc.exe 自身变慢,需权衡。
    3. 向 v1095 上游报告该 AOT 误编译(含最小复现:带 S2 包裹的 compileFile → 2⁴⁷ VirtualAlloc)。
  • 当前 HEAD 仍为 ac7813d 之上的未提交改动(RC2135 修复 + S2 禁用 + resource 段已注释)。待用户验证稳定后,建议提交 S1 部分(已稳定)并将 S2 作为"被编译器 bug 阻塞"的待办保留。

typephp-think 编译报 Duplicate class(重复类 fatal)

  • 现象:.\tpc.exe(S1 开/S2 关的稳定版)编译 D:\git\php\typephp-think 时,Preprocessor->prepareFileDuplicate class think\exception\ClassNotFoundException(首次撞在 think-container/src/exception/ClassNotFoundException.php)。
  • 不是 S1/S2 引入Preprocessor.php:697 的重复检测 isset($this->symbolDeclInFile[...]) 是 prepare 阶段的全局符号表(第733行赋值、无 resetClass 重置、ac7813d 未动过它)。我们的编译器对跨文件重名类是全局 fatal,比 v1095(别的电脑能编过的预编译版)更严格 → 这正是"别的电脑能编过"的原因(v1095 容忍重复类)。
  • 根因 = 工程的重名类 + 补丁机制不会解决它们typephp-thinkpatches/ 目录镜像 vendor/,由 patch.phpcopyPatchesSafely())在 composer post-autoload-dump 时把 patches/* 覆盖拷贝vendor/* 并写 .patches_applied。但 patches/topthink/ 只含 framework/think-dumper/think-filesystem/think-orm/think-trace不含 think-container;覆盖拷贝不会删文件。所以重名类依然存在。用户"今天只更新代码没打补丁"使 vendor 回到未打补丁态(需重跑 php patch.php)。
  • vendor/ 内真实重名类只有 4 个(编译器实际会撞的;patches/vendor 成对的不算,因编译器不扫 patches/):
    1. think\exception\ClassNotFoundException:framework vs think-container(两份字节一致,删 think-container 那份安全)。
    2. think\Exception:framework vs think-orm/stubs/Exception.php(stub,删 stub 安全)。
    3. think\Facade:think-container vs think-orm/stubs/Facade.php(stub,删 stub 安全)。
    4. think\route\Dispatch:framework 自身两份(route/Dispatch.php 规范基类 vs route/dispatch/Dispatch.php 命名空间写错成 think\route、全工程无引用 think\route\dispatch\Dispatch → 死代码,删后者安全)。
  • 决策:放宽编译器重复检测,而非删 vendor(为公平对比:v1095 容忍重复类,我们的 tpc.exe 也应如此,避免改输入导致 our-tpc vs v1095 不对等)。改 Preprocessor.php
    • prepareClass(~697):重复时 $this->warning(...) + resetClass() + return ''(不再 fatal,保留首个声明)。
    • parseInterface(~1507):重复时 $this->warning(...) + return;(不再 fatal)。
    • warning 方法签名 warning(Node $node, string $msg): void(CompilerDiagnosticTrait)。php -l 通过。
  • 验证:重编 tpc.exe(v1095)→ 编 typephp-think,应越过该 Duplicate class 继续。若再撞别的重名,说明还有未覆盖的 vendor 内部重复(再扫描确认)。
  • 备选(不推荐):直接删上述 4 个冗余 vendor 文件。缺点:composer 会还原 + 让我们与 v1095 的输入不一致,破坏速度对比公平性。

typephp-think 编译报 Cannot re-assign $str(string→array 类型冲突 fatal)

  • 现象:.\tpc.exetypephp-think 走到 symfony/var-dumper/Dumper/CliDumper.php:207 时,Preprocessor/Translator 在 convert 阶段 fatal:Cannot re-assign $str from php::Array to php::Str。该行是 dumpString(Cursor $cursor, string $str, bool $bin, int $cut)$str = $bin && str_contains($str,"\0") ? [$str] : explode("\n",$str); —— 形参 string $str 先当字符串用,后在 207 行被赋值为数组。
  • 根因 = 我们的编译器对局部变量/形参做了"单静态类型"假设AssignOpTrait::parseAssignFinally(~337)对已存在变量调用 checkVarAssignExpr($left, 现有类型, 新值类型)CompilerBase::checkVarAssignExpr 对"不兼容"的两次赋值直接 fatal。但 PHP 是动态类型,变量中途换类型是合法的——v1095(别的电脑能编过的预编译版)对此类变量用运行时 variant(php::Var)承载,所以不崩。我们之前对 vendor 的容忍(Duplicate class→warning)没覆盖到这种"标量→数组"类型变更。
  • 关键正确性约束:不能简单把 fatal 降级为 warning——否则 C++ 里 php::Str str; 后面接 str = <数组>; 会是类型错误甚至静默误编译。正确做法是:被重新赋以不兼容类型的变量,从一开始(声明处)就声明为运行时 variant php::Var
  • 修复(src 三处)
    1. CompilerBase.php:新增 protected array $variantVars = [];(resetFunction 里清空);新增 areTypesAssignable($existing,$new): bool,与 checkVarAssignExpr 的兼容规则逐字一致(VAR/REF/同类型/双 native/big 类型 都兼容,其余冲突),仅返回 bool 不 fatal,供预扫描判定。
    2. Translator.php:新增 computeVariantVars() + collectAssignTypes()/collectAssignTypesNode()(递归走函数体,跳过嵌套 function/closure/arrow,收集每个简单变量赋值 RHS 的 detectTypeOfExpr 类型,并以形参 argInfo->type 作种子);parseFunction() 在 SSA 之后调用它,若某变量类型集合存在不兼容对 → 记入 variantVars,并把这些形参的 argInfo->type 改为 php::Var、重拼 functionDef->params(用 genArgumentDeclaration)。
    3. Parser/AssignOpTrait.phpparseAssignFinally 首次声明局部变量时,若 $varvariantVars 中 → finalVarType = Type::VAR(声明即 variant),后续赋值走 variant 路径、checkVarAssignExpr 因 VAR 提前返回,不再 fatal。
  • 兼容性:决策逻辑与旧 checkVarAssignExpr 等价——原来能通过的不兼容赋值(int↔float、同类型重复、含 VAR/big)仍不触发;原来会 fatal 的(str↔arr、str↔int 等)现在正确升级为 variant,而非静默误编译。已用独立脚本验证决策矩阵。
  • 限制:预扫描只覆盖 Function_/ClassMethod(走 parseFunction);Closure/ArrowFunctionparseStmts 不经过 computeVariantVars,其形参若中途换类型仍可能 fatal(vendor 里少见,遇到再补)。php -l 三个文件均通过。
  • 验证状态:本机无法端到端重编验证——缺 PHP/Zend 头文件(php.h 不存在),且当前 PHP_HOME=D:\git\php\tpc_v1092_windows_x86_64(陈旧残留,非 PHP 安装)导致 detectPhpLibs 找不到 php8ts.lib。用户的真实构建环境(含 PHP dev 头 + 正确的 PHP_HOME)在另一台机器,已多次成功重编。须由用户执行重编+重跑验证(见下条命令)。
  • 用户重编+重跑序列(必须用 v1095,因我们的 tpc.exe 自身也需它编译;S2 已禁用故稳定)
    cd /d D:\git\php\typephp-think && php patch.php
    cd /d D:\git\php\aot-compiler
    set PHP_HOME=<用户正确的 PHP TS dev 安装目录>   # 例 D:\Program Files\PhpWebStudy-Data\app\php-8.3.24-ts(且其 dev\php8ts.lib 需在 $phpDir/lib 或 $phpDir 下,必要时拷贝)
    D:\git\php\tpc_v1095_windows_x86_64\tpc.exe project.yml -O2 -j 8
    rmdir /s /q build
    .\tpc.exe examples\hello.php          # 确认自举稳定
    cd /d D:\git\php\typephp-think
    D:\git\php\aot-compiler\tpc.exe project.yml -O2 -j 8   # 应越过 Duplicate class 与 Cannot re-assign,继续编译
    
  • 注意:本机实测 php-8.3.24-ts 根目录有 php8embed.lib、但 php8ts.lib 只在 dev/ 子目录;detectPhpLibs 只查 $phpDir/$phpDir\SDK\lib/$phpDir\lib,故需把 dev\php8ts.lib 拷到 php-8.3.24-ts\lib\(或根)才能让 v1095 的 tpc.exe 找到核心库——这是用户环境的常规布局操作。