Skip to content

fix(compiler): make numeric string arithmetic match PHP semantics - #136

Closed
alwaysLinger wants to merge 1 commit into
swoole:masterfrom
alwaysLinger:fix/float-numeric-string-arithmetic
Closed

alwaysLinger wants to merge 1 commit into
swoole:masterfrom
alwaysLinger:fix/float-numeric-string-arithmetic

Conversation

@alwaysLinger

Copy link
Copy Markdown

问题

同一算术表达式,数字串字面量形态与变量形态行为不一致——变量形态正确,字符串形态偏离:

var_dump("1e2" / 3);
$y = "1.5" * 2;
var_dump($y);
PHP(正确行为)    float(33.333333333333336)  float(3)
TypePHP 修复前     int(33)                    int(3)    ← 数值被静默截断
TypePHP 修复后     float(33.333333333333336)  float(3)  ← 与 PHP 一致

PHP 手册规定:纯十进制整数且在 int64 范围内的数字串("5"、"10")按 int 参与算术,其余数字串("1e2"、"1.5")按 float。此偏离不在官方不兼容清单内。

根因

三条路径都未把数字串字面量当作数值操作数:

  • 类型推断:src/CompilerBase.php detectTypeOfExpr() 算术分支,一侧为原生 Int 时无条件返回 Type::INT——赋值目标被声明为 php::Int 槽位,double 值写入即截断。
  • 代码生成:src/CompilerBase.php parseNumericIdentifier() 用 (string) floatval("1e2") 转换数字串,得到 "100"(无小数点),发出的 C++ 是整型字面量。
  • 常量求值:src/Parser/BinaryOpTrait.php constantNumericValue() 不处理 Node\Scalar\String_,字符串派生的常量绕过溢出检查与折叠。

修复

  • 新增 isIntNumericString() / isFloatNumericString()(src/Parser/TypeDetectionTrait.php):is_numeric + 纯整数语法正则 + int64 范围检查,与 PHP 数字串分类语义一致。
  • detectTypeOfExpr():一侧为浮点数字串时,+ - * / ** 结果提升为 Type::FLOAT(%/移位/位运算排除);varint 模式下整型语法数字串视同 int 操作数参与常量求值。
  • parseNumericIdentifier():浮点数字串经仓库现有 shortestFloatRepr() 发射,保留 C++ double 形态(1e+2);溢出为 INF 时退回运行时字符串路径。
  • constantNumericValue():读取数字串字面量(整型语法 → intval,其余有限数字串 → floatval)。现有常量溢出防护因此覆盖字符串常量:native 模式下 "9223372036854775807" * 2 与 int 字面量写法一致地被编译期拒绝;varint 模式下折叠为 PHP float。

行为变化

  • native:"9223372036854775807" * 2 从“编译通过、运行时未定义行为”变为编译期拒绝——与 int 字面量写法一致。
  • varint:整型数字串溢出从“错误 float”折叠为正确 PHP float;"5" / 2 从 int(2) 修正为 float(2.5)(官方文档 varint_types 职责:“溢出转浮点与小数除法”)。
  • bigint:"9223372036854775807" * 2 从运行时崩溃(ValueError: Invalid BigInt numeric string)修复为精确 BigInt 运算。

保持不变

  • 整型语法数字串的 native 语义:"10" / 2 → int(5)、"5" + 1 → int(6)——逐字节与修复前一致。
  • 变量路径($f = "1e2"; $f / 3)原本正确,未触碰。
  • %/移位/位运算不涉及。

验证

Linux x86-64 Docker,PHP 8.4.26 NTS,GCC:

  • 约 70 例行为矩阵与 php 基准逐字节对拍(含大写 E、负指数、.5/5.、首尾空白、INF、int64 边界、幂/比较/移位/位运算/复合赋值),native 与 varint 双模式,-O0/-O2/-O3 一致;use bigint_types 独立对拍:修复前运行时崩溃、修复后精确 BigInt。
  • 残余差异仅 3 例,均为下方披露的范围外项,修复前后行为相同。
  • 测试有效性:stash 补丁后 PHPT 与 phpunit 断言均变 FAIL,恢复后 PASS。
  • 回归面:tests/compiler/{operator,basic,std-array,class} 188 项无新增失败(唯一失败 pow-int-overflow 在未修改基线上同样失败,为容器 precision=14 环境差异);全量 phpunit 2409 项仅 2 例失败(容器缺 python3,基线相同)。

…heir arithmetic matches PHP semantics across all modes
@matyhtf

matyhtf commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

结论:不建议合入当前方案

评审基于 head b5ba47c95b4b0666e9ff6e62ccc20e689d785f25。

TypePHP 中,字符串字面量始终是 string。编译器不能因为字符串内容看起来像整数、小数或科学计数法,就把它当作数值操作数参与类型推断、常量求值或原生代码生成。字面量和同类型变量必须遵循一致规则。

1. 字符串参与数值运算需要显式转换

"5"、"1.5"、"1e2" 都是字符串。需要数值运算时,应使用 (int)、(float)、toInt() 或 toFloat() 明确转换:

function main(): void {
    $s = "1e2";       // string
    $f = (float) $s;  // float
    var_dump($f / 3);
}

无法满足静态操作数类型要求的表达式,应由 TypePHP 编译器报告类型错误,不能依赖字符串内容决定是否接受,更不能生成非法 C++。

字符串自身支持的操作仍保持字符串语义,例如两侧均为字符串的 &、|、^。显式使用 std::any() / toAny() 进入动态语义后,运行时运算由 PHPX/Zend 处理。use varint_types 只影响整数存储和运算,不应把字符串变成数值操作数。

2. 高精度类型接受字符串构造参数是明确的转换接口

std::bigInt($text)、std::bigFloat($text)、std::decimal($text) 已经通过构造调用指定目标高精度类型。

  • 参数 $text 仍是 string。
  • 结果的静态类型由构造函数决定。
  • 字符串内容决定解析出的值,以及构造能否成功,不决定参数或结果的静态类型。
  • 字符串可以来自字面量或运行时变量,类型规则一致。

显式构造之后,可以在保持构造函数语义的前提下进行常量优化。调整字符串类型规则时,应保留这些高精度类型的字符串构造能力。

3. 本 PR 需要调整的实现

  1. 移除根据 isIntNumericString() / isFloatNumericString() 改变静态运算推断的路径,包括 leftIntLike / rightIntLike。
  2. constantNumericValue() 不应直接把 String_ 当数值常量求值;数值折叠应发生在显式转换或明确的数值构造之后。
  3. parseNumericIdentifier() 不应根据字符串内容生成数值字面量。master 已存在的同类捷径也需要按这一原则清理。
  4. 对显式转换后确定为整数的常量,复用 genIntegerLiteral() 处理整数边界和平台后缀。
  5. 测试应验证字符串类型保留、字面量与变量规则一致、非法静态运算的类型诊断、显式转换后的正确行为,以及高精度类型的字符串构造。原生 PHP 的数字字符串隐式转换结果不应成为这些静态表达式必须兼容的契约。

4. 已确认的代码生成问题

以下是当前实现的行为证据,不表示这些未显式转换的数值运算应被 TypePHP 接受。

整数边界: "-9223372036854775808" % 3 在原生 PHP(ZendVM)与 TypePHP(master 分支)中输出 int(-2),应用 PR 后输出 int(1),use varint_types 下也错误。

src/CompilerBase.php:2434–2435 直接生成 -9223372036854775808。本机 GCC 11.4/C++17 将该表达式解析为 __int128,无法匹配 PHPX 的整数 Variant 构造模板,最终进入 bool 构造。"-9223372036854775808" / 2 也会输出 bool(true)。

位运算和移位: "1.0" & 1、"2e0" << 1 在 master 能编译运行,应用 PR 后生成:

((1e+0) & (1L))
((2e+0) << (1L))

src/CompilerBase.php:2431–2432 将数字字符串统一输出为 double 字面量,导致 C++ 编译错误;use varint_types 同样无法构建。按照静态类型规则,应在 TypePHP 类型检查阶段诊断这些未显式转换的混合操作数。

验证范围

  • Linux x64、PHP 8.4.14 ZTS/embed、GCC 11.4,默认优化等级。
  • 本 PR 新增的两个 PHPT:2/2 PASS。
  • PHPUnit FloatNumericStringArithmeticTest|CompilerBaseApiTest:126 tests、685 assertions,全部通过。
  • 上述边界用例已实际编译运行,并与 TypePHP(master 分支)对比。
  • 未运行完整测试集,未测试 Windows/macOS/PHP 8.5。

@matyhtf matyhtf closed this Oct 5, 2026
@matyhtf

matyhtf commented Oct 5, 2026

Copy link
Copy Markdown
Member

820610a

已重构。str 与 int/float 运算时,会提升为 var 类型,当前行为已与 php 完全一致。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants