PHP
能写一般程序的子集:<?php … ?> 标签(标签外按 HTML 原样输出,没有任何开标签时整份文件当 PHP)、$变量、echo / print、完整算术 / 比较 / 逻辑 / 位运算、if / while / do-while / for / foreach / switch、函数(含默认参数、递归、global)、下标数组([] 与 array() 两种字面量)。
「完整运算 + 控制流 + 函数调用」三件都齐。
在手机上怎么跑
vml run examples/php/fib.php
编译要等一会儿(C 那种要一两分钟,脚本类语言几秒)。程序跑起来后屏幕底部就是手柄,方向键 + 四个动作键都在;点右上角返回可以回到命令行。
写法要点
ui_*直接写就行(其余一切名字都按名字发CALL,链接期由前缀表把对应的.vml拉进来)echo可以一次给多个参数(无分隔连着打);字符串拼接是.- 数组字面量
[10, 20, 30]与array(10, 20, 30)等价;$a[] = v是追加 require/include在本前端是声明(无操作),删掉照样编得过- 变量(
$name)与关键字大小写不敏感;注释有//#/* */三种
示例
| 文件 | 演示什么 |
|---|---|
fib.php | 斐波那契(递归 + 迭代 + 数组) |
hello.php | 最小程序(echo + 字符串拼接 + 算术) |
sort.php | 冒泡排序(数组读写 + 嵌套循环 + 交换) |
实测踩过的坑
- ⚠ 关联数组
["k" => 1]不支持,而且编译期报错(不静默丢键);对象 / 类 / 接口 / 闭包 / 匿名函数 /try-catch/isset/unset/empty/goto也都不支持。 - ⚠ 数组元素不带类型:
echo $arr[i]一律走整数通道 ⇒ 字符串元素打出来是指针值(用例24-knownred-elemfmt.php钉着这条)。 - ⚠ 字符串与数字的隐式拼接不转换:
"x" . 5得"x"(不是"x5")—— 运行期那一格分不出「整数」与「串指针」,硬转反而更错。 - ⚠
switch每个 case 自动break(不支持落穿);case 的值必须是编译期整数常量(字符串 case / 范围 case 编译期报错)。 - ⚠ 用户函数的浮点形参 / 返回值按整数传(先
D2I截断)—— 浮点只在顶层与内置函数之间完整可用,跨用户函数会丢小数。 - 未定义变量按 0 处理(不报
Undefined variable,与真 PHP 有意不同);break 2;/continue 2;的层数被忽略(按 1 处理)。
下面的内容是从 VML 源码里直接带的(third_party/vml/VMLPrepares/PhpCompiler/): README 讲这个前端支持什么、怎么编;语言规范 讲语法本身。上游一改,这里重新生成就是最新的。
语言规范
PHP 语言子集规格(都来码 / Dolaima)
适用:VMLPrepares/PhpCompiler。判据的唯一出处是 scripts/vml-php-probe/run.sh (当前 通过 25 / 失败 0 / 已知红 1)—— 本文里凡是写"支持"的,那条都有一条能红的用例;写"不支持"的,多数有一条 EXPECT-ERR 用例钉着。
版本:v1.0 | 日期:2026-10-13 | 修订者:深圳市探索智能科技有限公司
规范标准
| 字段 | 值 |
|---|---|
| 目标标准 | PHP 5/7 子集 |
| 发布年份 | 2004 |
| 完成度 | 子集(未逐项统计) |
| 测试 | 25 通过 / 0 失败 / 1 已知红 |
关键字
if elseif else endif while endwhile do for endfor foreach endforeach as switch endswitch case default break continue return function global echo print true false null nil and or xor not array
概述
支持 PHP 5/7 子集,编译为 VML 汇编代码。既支持 <?php … ?> 段与 HTML 混排(?> 之后的内容按 HTML 原样输出;没有任何开标签时整份文件当 PHP),也支持纯 PHP 文件;值是静态分档的:每个值在编译期归到 Int(32 位 / 4 字节)或 Float(双精度),/ 一律双精度、其余算术任一侧是浮点就走双精度。内置函数专用发射,其余一切名字一律发 CALL <原名>、由链接期的 SharedPrefixMap 按前缀拉库(ui_ → vmlui、tty_ → tty…),库里没有这个名字是链接期报错、不静默;require / include 只是声明、不加载任何东西。与其它前端有一处有意不同:不跑预处理器(PHP 里 # 是注释符)。
支持的语言特性
本前端接受的语言特性按下方原有章节组织,每节都写明「收什么写法、是什么语义」:
- 文件与标签:
<?php … ?>/<?= … ?>/ 没有开标签时整份当 PHP —— §一 - 词法:变量
$name、注释(//、#、/* */)、数字、单 / 双引号字符串与插值、标识符(关键字大小写不敏感) —— §二 - 语句:
echo/print、赋值与复合赋值、下标读写与[]追加、if/elseif/else({ }与: … endif;两套)、while/do…while、for、foreach、switch、break/continue、function/return、global、require/include、HTML 段 —— §三 - 表达式与运算:优先级表、算术、比较(含
===)、逻辑两套、位运算、.拼接、三元与?:/??、自增自减 —— §四 - 类型档位:
Int/Float的判定规则与echo的分流 —— §五 - 内置函数:
strlen/count/strtoupper/strtolower/substr/strpos/implode/explode/intval/floatval/abs/max/min/rand/sprintf/printf/array_push/array_pop/intdiv等 —— §六 - 数组:
[元素个数, e0, e1, …]布局与指针语义 —— §七 - 不支持项:关联数组、对象与类、闭包、
try/catch、isset/unset/empty、goto等(多数响亮报错) —— §八 - 已知差异:能跑、但与真 PHP 不同的 10 条 —— §九
详见下方各节。
一、文件与标签
| 写法 | 行为 |
|---|---|
<?php … ?> | PHP 段执行;?> 之后的内容按 HTML 原样输出,且 ?> 会吃掉紧跟的一个换行 |
<?= 表达式 ?> | 等价于 <?php echo 表达式 ?> |
| 没有任何开标签 | 整份文件当 PHP(本仓例程的主用法) |
判据:cases/17-tagmode.php。
「有没有开标签」是文本级预判(找
<?php/<?=)。若你在源代码的字符串字面量里写"<?php",会被当成开标签 —— 已记档的取舍,别指望它。
二、词法
- 变量:
$name([A-Za-z_][A-Za-z0-9_]*)。未赋值就读不报错,按 0 处理(子集行为,与 PHP 的Warning: Undefined variable有意不同)。 - 注释:
// …、# …、/* … */(不嵌套)。 - 数字:十进制、
0x十六进制、小数、1e3。全部按 32 位整数 存;只有出现在需要浮点的场合(见第五节)才走双精度。 - 字符串:
- 单引号
'…':只认\\与\',其余原样(PHP 语义)。 - 双引号
"…":认\n \t \r \0 \\ \" \$;$var会插值(\$是不插值的字面美元号)。
- 单引号
- 标识符:裸名(函数名)。关键字大小写不敏感。
三、语句
| 语句 | 备注 |
|---|---|
echo e1, e2, …; / print e; | 无分隔符;print 按语句处理(不返回 1) |
$x = 值; $x op= 值; | op ∈ + - * / % .;复合赋值等价于 $x = $x op 值 |
$a[i] = 值; $a[] = 值; | [] 是追加 |
if / elseif / else | 两套写法都支持:{ } 与 : … endif; |
while do…while | 也支持 : … endwhile; |
for (init; cond; step) | cond 里用 , 分隔会被当成 && |
foreach ($a as $v) / as $k => $v | 只对下标数组(键是 0-based 整数) |
switch / case / default | case 值必须是编译期整数常量;每个 case 自动 break(见「已知差异」) |
break; continue; | break 2; 这种层数被忽略(按 1 处理) |
function f($a, $b = 默认值) { … } return 值; | 递归可用;默认值在调用点就地求值补位 |
global $a, $b; | 之后函数体里的 $a/$b 指向顶层(全局)变量 |
require/include[_once] '文件'; | 声明,无操作(库函数按名字解析,见第六节) |
| HTML 段 | 等价于一条 echo "原文"; |
四、表达式与运算
优先级从低到高(同 PHP 大体一致):or → xor → and → 赋值(右结合)→ ?: / ?? → || → && → | → ^ → & → 相等 → 关系 → 移位 → + - . → * / % → 一元 ! - + ~ ++ -- → **(右结合)→ 后缀 [] ++ -- 与调用。
- 算术:
+ - * / % «bold»。«/»走共享ipow(整数幂)。 - 比较:
== != === !== < <= > >= <>。===/!==与==/!=同义(没有类型标签可分)。 - 逻辑:
&& || !与and or not xor(两套都是短路并归一成 0/1)。 - 位运算:
& | ^ ~ << >>。 - 字符串拼接:
.(见「已知差异」)。 - 三元:
a ? b : c与简写a ?: c;??按「左为 0/空则取右」实现。 - 自增自减:前缀与后缀都支持(后缀返回旧值)。
五、类型档位(决定发什么指令)
本前端是静态分档的:每个值在编译期归到 Int(32 位通用寄存器 / 4 字节栈槽)或 Float(D0 / 8 字节栈槽)。
- 字面量:带小数点或指数 ⇒
Float,否则Int。 /一律双精度(PHP 语义:17/25得0.68,不是0)。- 其余算术:任一侧是
Float就走双精度(ADDD/SUBD/MULD/DIVD)。 - 字符串与数组本质是指针,走 4 字节。
echo的分流:String→print_str;Float→print_float(先D2F);其余 →print_int。
⚠ 与
Lib/的调用约定对齐:整数保持 32 位/4 字节 —— 全按双精度会当场对不上 库的 4 字节形参区(Lua 前端当初就踩过这条,这里沿用同一结论)。
六、内置函数
专用发射(前端自己排版实参):
| 函数 | 底层 | 说明 |
|---|---|---|
strlen($s) | CALL strlen | |
count($a) / sizeof($a) | 读 [base] | 数组长度([len, e0, …] 布局) |
strtoupper($s) / strtolower($s) | alloc + str_toupper/str_tolower | 返回新串 |
substr($s, $start[, $len]) | alloc + str_substr | $len 省略 = 到末尾 |
strpos($h, $n) | 单字符字面量 → str_indexof;否则 strstr − 基址 | 找不到回 -1 |
implode($sep, $arr) | join_str | ⚠ 只对整数元素(库的实现是把元素当整数转十进制串) |
explode($sep, $s) | str_split + 本前端数组布局 | 分隔符取 $sep 的首字符 |
intval / floatval | 就地转换 | |
abs / max / min | CALL abs/max/min | 整数版;max/min 支持多参(折叠成两两调用) |
rand() / rand($min, $max) | random / random_range | |
sprintf($fmt, …) | printf2…printf5 / sprintf | 值个数 0–5 |
printf($fmt, …) | 同上 + print_str | |
array_push($a, $v) | 就地追加 | 数组字面量已留 64 字节余量 |
array_pop($a) | 弹末尾 | 空数组回 0 |
intdiv($a, $b) | DIV | |
print_r / var_dump | 按 echo 处理 | 子集简化 |
require / include 家族 | 无操作 | 但 require 可不带括号收裸表达式 |
其余一切按名字直接 CALL:ui_2d_clear(...) → CALL ui_2d_clear,链接期由 SharedPrefixMap 的 ui_ → vmlui(tty_ → tty、str → string …)拉库。库里没有这个名字 ⇒ 链接期报 未定义的函数 'x'(引用 N 次),不会静默。
七、数组
布局与 Lib/shared/array.c 同源:[元素个数, e0, e1, …],每格 4 字节。数组名本身是指针(4 字节),所以能当参数传、能 return。
$a = [10, 20, 30]; // 等价于 array(10, 20, 30)
echo $a[1], "\n"; // 20
$a[2] = 3; // 按下标写
$a[] = 30; // 追加(长度 +1)
echo count($a); // 4
八、不支持项(有意不做,多数会响亮报错)
| 项 | 现状 |
|---|---|
关联数组 ["k" => 1] | 编译期报错「关联数组(键 => 值)暂不支持」——不静默丢键 |
{$expr} 插值、$a[i] 插值 | 不支持;双引号串里只认 $名字 |
| 对象 / 类 / 接口 / trait | 不支持 |
闭包 / 匿名函数 / use | 不支持 |
try / catch / throw | 不支持 |
switch 的字符串 case、范围 case | 编译期报错(只收编译期整数常量) |
| 用户函数的 浮点形参 / 返回值 | 按整数传递(先 D2I)—— 见「已知差异」 |
isset / unset / empty / list / die / exit | 不支持 |
include_path / 命名空间 / 自动加载 | 不支持(库按名字解析) |
goto | 不支持 |
九、已知差异(能跑,但与真 PHP 不同 —— 一条条列出来)
- 数组元素不带类型:
echo $arr[i]一律走整数通道 ⇒ 字符串元素打出来是指针值。用例cases/24-knownred-elemfmt.php标KNOWN-RED钉着这条。 - 字符串与数字的隐式拼接不转换:
"x" . 5得"x"(不是"x5")—— 运行期那一格分不出"整数"与"串指针",硬转反而更错。 switch每个 case 自动break:不支持落穿(fallthrough)。写了break;也无害。- 用户函数跨边界按整数:浮点实参先
D2I(截断),返回值同理。 ⇒ 浮点在顶层与内置函数之间完整可用,跨用户函数会丢小数。 break 2;/continue 2;的层数被忽略。$a[i]++之类会求值下标两次(有副作用的表达式会有差异)。- 未定义变量按 0(不报
Undefined variable)。 echo一个数组打的是指针值。implode只处理整数元素(库的join_str如此)。print不返回 1(当语句处理)。
十、判据
scripts/vml-php-probe/run.sh # 通过 25 / 失败 0 / 已知红 1
走的是真实通道:每个用例都由 vmlcli <case.php> --lang php 一条命令「编译 → 汇编 → 链接(含 Lib/php 模块集)→ 运行」。(早先"自己调 PhpCompiler.dll 出 .vmb、再喂给 vmlcli"的写法**绕开了前端注册表与 Lib/<lang>/ 的解析**,把三类缺陷一次性盖住了 —— 见 bugs/vml_php_bugs/README.md 的 PHP-BUG-06。)
两条能红的回归判据(改坏了立刻红,都实测过):
cases/23-ctrlflow-vmb.php—— 把CodeGenerator.PlaceLbl改回AddLabel(标签发LABEL伪指令),.vmb的符号表整体错位 ⇒ 期望6 30/ 实得空输出。cases/03-strings.php—— 把拼接缓冲尺寸里的len(a)丢掉 ⇒ 实得一大串ababab…(堆块重叠、自我复制)。
平台级缺陷的完整记录(现象 / 根因 / 测量 / 规避)见 [bugs/vml_php_bugs/README.md](../../../bugs/vml_php_bugs/README.md)。
编译器 README
PHP 编译器(VMLPrepares/PhpCompiler)
路径:third_party/vml/VMLPrepares/PhpCompiler/ 扩展名:.php 标准库:Lib/php/(builtin.vml = 库聚合器;vml_ui.php = 文档级声明表) 判据:scripts/vml-php-probe/run.sh(真实通道:vmlcli <case> --lang php)—— 实测 通过 25 / 失败 0 / 已知红 1 语言子集与差异:[PHP_LANGUAGE_SPEC.md](PHP_LANGUAGE_SPEC.md)(EN) 接线:[INTEGRATION.md](INTEGRATION.md)(本次按约定不动共享文件)
是什么
一个能写一般程序的 PHP 子集前端:<?php … ?> 标签(标签外按 HTML 原样输出)、 $变量、echo/print、完整算术/比较/逻辑/位运算、if/elseif/else(含 endif 备用语法)、 while / do-while / for / foreach(含 as $k => $v)、switch、break/continue、函数(含默认参数、递归、global)、下标数组([] 与 array() 两种字面量)、单/双引号字符串(双引号支持 $var 插值)、// # /* */ 注释,以及一批内置函数(见 spec)。
「完整运算 + 控制流 + 函数调用」三件都齐 —— Examples/php/ 里的三个例程可跑(含递归、数组传参与返回、冒泡排序)。
怎么跑
接线已完成(见 INTEGRATION.md),所以走的是真实通道 —— 与手机端 MauiVml 等价的「编译 → 汇编 → 链接(含 Lib/php 模块集)→ 运行」:
dotnet build scripts/vmlcli/vmlcli.csproj -c Release
export VML_HOME=$PWD/third_party/vml
# ① 编 + 跑(一条命令)
dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll third_party/vml/Examples/php/hello.php --lang php
# ② 只看前端产出的汇编(不链接、不跑)
dotnet third_party/vml/VMLPrepares/PhpCompiler/bin/Release/net10.0/PhpCompiler.dll third_party/vml/Examples/php/hello.php -o /tmp/hello.vml
# ③ 语言特性套件
scripts/vml-php-probe/run.sh
⚠ 判据必须从真实通道进(这一条是踩过坑的):上一版套件是"自己调
PhpCompiler.dll编成.vmb、再vmlcli <vmb>"。那条路绕开了前端注册表与Lib/<lang>/的解析,于是「vmlcli没注册 php」「vmltool.config.xml缺 php 行」「Libs=缺array.vml」这类缺陷一条都照不出来(自己拼的链接集恰好补齐了缺的模块)。细节见 [bugs/vml_php_bugs/](../../../bugs/vml_php_bugs/README.md) 的 PHP-BUG-06。
库函数怎么来
两条路,都不是靠"读声明文件":
- 内置表(
CodeGenerator.Builtins.cs):strlen/count/substr/strtoupper/strtolower/strpos/implode/explode/intval/floatval/abs/max/min/rand/sprintf/printf/array_push/array_pop/intdiv/print_r/var_dump/require家族。这些要前端自己排版实参(字符串要 dst 缓冲、数组要按[len, e0, …]走),所以不走名字解析。 - 按名字直接发
CALL:其余一切(ui_2d_clear/tty_puts/ 任何库导出名)。链接期由CompilerHelper的SharedPrefixMap(ui_→vmlui、str→string…)把对应.vml拉进来 —— 与 Lua / Ruby / Python 那几门同一条口径。
require 'vml_ui.php'; 是声明(本前端把 require/include 当无操作语句): 删掉它程序照样编得过、跑得对(LoadKind = 文档级,见 docs/UI库导入约定.md)。 Lib/php/vml_ui.php 因此是给人看的注释表。
与其它前端的一处有意不同:不跑预处理器
CompilerHelper.CompileFileStandard 在源码出现 # 时起预处理器,而 PHP 里 # 是注释符 —— 两者在天上打架。⇒ PHP 有意绕开那个模板:只有源码里真出现 #include 时才起预处理器。
目录
PhpCompiler/
├── TokenType.cs / Token.cs 词法单元(Token 实现 ITokenPosition,位置由基类一处取)
├── Lexer.cs 继承 LexerBase;`<?php ?>` 段切分、双引号串插值与 `\$` 占位
├── ASTNode.cs 节点(自带行/列)
├── Parser.cs 继承 ParserBase;优先级爬升 + 备用语法(`endif`/`endwhile`…)
├── CodeGenerator.cs 类型档位表 + 符号表/作用域 + 帧槽(PlaceLbl 是本前端的硬约束)
├── CodeGenerator.Expressions.cs 表达式(含字符串拼接、下标读写、短路逻辑、三元)
├── CodeGenerator.Statements.cs 语句(含 foreach/switch/函数定义/默认参数补位)
├── CodeGenerator.Builtins.cs 内置函数专用发射
├── PhpCompiler.cs / PhpPlugin.cs 静态入口 / 插件(继承 CompilerPluginExBase)
├── Program.cs 开发期 CLI(`--vmb` / `-o` / `--info` / `--dump`)
├── PhpCompiler.csproj
├── README.md / README.en.md
└── PHP_LANGUAGE_SPEC.md / PHP_LANGUAGE_SPEC.en.md
已知缺陷 / 限制
见 [bugs/vml_php_bugs/](../../../bugs/vml_php_bugs/README.md)(平台级 2 条 + 前端自己的错 2 条 + 限制 2 条),以及 spec 的「不支持项」。最重要的一条平台级缺陷:.vmb 的符号表会被 LABEL 伪指令 整体偏移一格 —— 本前端靠「标签只登记、不发伪指令」规避,且有一条能红的回归判据(scripts/vml-php-probe/cases/23-ctrlflow-vmb.php)。