Perl
「完整运算 + 控制流 + 函数调用」那一档:数组(@a)、哈希(%h)、子程序、字符串处理、循环、递归都能写 —— 覆盖到词频统计那种规模。
print 可以一次给多个参数(照原样连着打,不加分隔);双引号串里 $var 会插值。
在手机上怎么跑
vml run examples/perl/app/wordcount.pl
编译要等一会儿(C 那种要一两分钟,脚本类语言几秒)。程序跑起来后屏幕底部就是手柄,方向键 + 四个动作键都在;点右上角返回可以回到命令行。
写法要点
my $x/my @a/my %h声明;未声明也能用(落到全局,值为 0)- 换行就是语句分隔符,行尾的
;常常可以省 ui_*直接调用(前端按裸名字发CALL);use vml_ui;只是声明,删掉照样编得过- 打字符串用
print/say(say末尾自动补换行) $#a是数组末位下标,scalar(@a)是长度
示例
| 文件 | 演示什么 |
|---|---|
app/wordcount.pl | 词频统计(split + 哈希计数 + scalar) |
hello.pl | 最小程序(print 多参 + 双引号插值) |
test/syntax.pl | 语言小抄:每种形态各用一遍 |
实测踩过的坑
- ⚠ 没有运行期类型标签:类型是编译期推的 —— 从
@_取出来的形参、未知库函数的返回值都属于类型未知(算术按整数算、拼串时当指针)。所以print一个类型未知的形参,若它其实是字符串,打出来是地址;要稳就显式拼一次串。 - ⚠ 比较有两套算子(数值
== != < > <= >=、字符串eq ne lt gt le ge),用错不报错,只是按另一套比。 - ⚠
sort/reverse/each/ucfirst/lcfirst/rindex未实现,且在编译期报错(不静默);push的实参不对也报错。 - 双引号串里
@arr不插值(数组插值要按元素 join,元素类型编译期定不了)—— 要用join($sep, @arr)。 - 没有引用 / 解引用 / 匿名数组哈希 / 正则 / 包 / 对象;整数是 32 位。
- 子程序只收一个实参(就是
@_那个实参数组的地址),my ($a, $b) = @_;在函数体里按下标取;作用域是扁平的(my是函数级栈槽,没有块作用域)。 - 浮点打印走共享运行时,固定 6 位小数(
3.4可能显示成3.399999—— 那是共享 dtoa 的舍入口径,不是前端算错)。
下面的内容是从 VML 源码里直接带的(third_party/vml/VMLPrepares/PerlCompiler/): README 讲这个前端支持什么、怎么编;语言规范 讲语法本身。上游一改,这里重新生成就是最新的。
语言规范
Perl 子集语言规格 —— PerlCompiler 实现了什么
本文是 PerlCompiler 前端的实现口径:支持哪些语法/语义、静默坑在哪、哪些有意不支持 (且在编译期报错,不假装支持)。对应英文版见 [PERL_LANGUAGE_SPEC.en.md](PERL_LANGUAGE_SPEC.en.md)。
版本:v1.0 | 日期:2026-10-13 | 修订者:深圳市探索智能科技有限公司
规范标准
| 字段 | 值 |
|---|---|
| 目标标准 | Perl 5 子集 |
| 发布年份 | 1994 |
| 完成度 | 子集(未逐项统计) |
| 测试 | 24 通过 / 0 失败 |
关键字
my our sub if elsif else unless while until for foreach do last next redo return and or not print say use defined undef eq ne lt gt le ge cmp qw
概述
支持 Perl 5 子集,编译为 VML 汇编代码。变量带 sigil($ 标量 / @ 数组 / % 哈希,$x、@x、%x 在词法层就是三个独立的名字),换行是语句分隔符(与 ; 等价);有意不跑 C 预处理器(Perl 用 # 作行注释,一见 # 就跑预处理会把每条注释当成指令)。前端没有运行期类型标签:按编译期能定的信息在整数 / 浮点 / 字符串三条通道之间选择(从 @_ 取出的形参、未知库函数的返回值属于类型未知,见 §8);未实现的内置在编译期报错,没有「悄悄按 0 跑过去」的兜底。
支持的语言特性
本前端接受的语言特性按下方原有章节组织,每节都写明「收什么写法、是什么语义」:
- 词法:注释、数字、单引号串(只认
\\与\')、双引号串与$name/${name}插值、标识符、sigil、$#a、换行作分隔符 —— §1 - 变量与声明:
my/our(本子集里同义)、标量 / 数组 / 哈希各建各的槽、未声明即可用、扁平作用域 —— §2 - 表达式与运算:算术(
/是浮点除法、«bold»是整数幂)、.拼接与x重复、数值与字符串两套**比较、逻辑两套、++/--—— §3 - 语句与控制流:
if/elsif/else/unless、while/until、for/foreach、后缀修饰、last/next/redo—— §4 - 字符串:多参
print/say、length/substr/index/uc/lc/chomp/chop/sprintf/int/abs、@arr不插值 —— §5 - 数组与哈希:
$a[i](负下标从尾部算)、$#a、push/pop/shift/unshift、join/split、%h/exists/delete/keys/values—— §6 - 子程序与
use:sub定义与调用(含sub f($x, $y)形式)、先调用后定义、use NAME;读声明文件 —— §7 - 类型的编译期推断:为什么会「看走眼」(有意接受的取舍) —— §8
- 有意不支持:正则 / 引用与解引用 / 包与对象 /
local、state的包语义 / 文件句柄 / 64 位整数等,多数在编译期报错 —— §9
详见下方各节。
0. 总则
- 判据:能写"一般程序" —— 运算 / 控制流 / 函数调用 / 数组 / 哈希 / 字符串处理齐全。
- 不静默:本前端没有"悄悄按 0 跑过去"的内置。未实现的内置(如
sort)会在编译期报错;push的实参不对也报错。宁可不编,也不给一个错结果。 - AOT 无反射、无固定地址:发射的只有标准 VML 指令与
CALL到具名标签。
1. 词法
| 形态 | 说明 |
|---|---|
| 注释 | # 到行尾 |
| 数字 | 十进制整数、0x… 十六进制、小数(如 1.5)。整数是 32 位(超出按低 32 位截断) |
| 单引号串 | 只认 \\ 与 \' 两个转义,其余原样 |
| 双引号串 | 认 \n \t \r \0 \\ \" \$ \@;$name / ${name} 插值;@name 不插值(见 §5) |
| 标识符 | 字母 / 数字 / _ / 汉字 |
| sigil | $ 标量、@ 数组、% 哈希。% 后跟标识符才算 sigil,否则是取模算子 |
$#a | 数组 a 的末位下标(= 长度 − 1) |
| 换行 | 是语句分隔符(与 ; 等价);但上一 token 是算子/逗号/开括号时抑制,于是跨行写法照常算一条语句 |
关键字:my our sub if elsif else unless while until for foreach do last next redo return and or not print say use defined undef eq ne lt gt le ge cmp qw。
2. 变量与声明
my $x/my @a/my %h;our同义(本子集里没有真正的包变量语义,our与my行为相同)。- Perl 同名的
$x/@x/%x是三个独立的名字(各自的 sigil 决定取什么)—— 前端按(sigil, 名字)分别建槽。 - 未声明即可用(Perl 默认无
use strict):未声明的标量落到数据段的全局,值为 0。 - 作用域是扁平的(本子集的一个有意简化):
my分配的是函数级栈槽,没有块作用域;两个同名my复用同一个槽。子程序之间共享同一张符号表(同名局部量在第二个函数里复用第一个的偏移,与 Lua 前端同一取舍)。
3. 表达式与运算
- 算术:
+ - * / % **。/是浮点除法(Perl 语义:7/2→3.5),任一侧是浮点则整条走双精度。«bold»是整数幂**(走共享库ipow)。浮点底数请先自己转。
- 字符串:
.拼接(数字会自动转十进制串)、x重复("ab" x 3)、.=追加。 - 比较两套(这一条最容易踩):
- 数值:
== != < > <= >= - 字符串:
eq ne lt gt le ge(内部走strcmp) - 用错不会报错,只会得到"按另一套比"的结果(与 Perl 本身一样)。
cmp返回-1/0/1。
- 数值:
- 逻辑:
&& || !与and or not两套都认,都是短路并返回 0/1(不是 Perl 的 "返回最后一个操作数")。三目?:支持。 ++/--:前缀与后缀都支持;后缀返回旧值。实现是+1再-2的等价式(一步到位)。unless是if(not …);until是while(not …)。
4. 语句与控制流
if / elsif / else、unless、while、until、for / foreach、C 风格for (init; cond; step)。- 后缀修饰:
… if 条件;/… unless 条件;/… while 条件;。 last/next(redo按next近似)。{ … }裸块按顺序展开。use NAME;见 §7。
5. 字符串
print可多参数,不加分隔地连着打;say在末尾补换行。无参print打$_近似(打 0)。print @a打的是元素个数(元素类型在编译期定不了,见 §8;要逐个打请foreach)。- 内置:
length、substr($s,$pos[,$len])(支持负下标/负长度)、index($s,$sub[,$start])、uc、lc、chomp、chop、sprintf(%d %i %x %s %c %%+ 宽度/左对齐,精度只跳过)、int、abs。 - 双引号里
@arr不插值(数组插值要按元素 join,元素类型编译期定不了)。要用join($sep, @arr)。
6. 数组与哈希
- 数组:
@a、$a[i](负下标从尾部算)、$#a、scalar(@a)、length(@a);push / pop / shift / unshift(第一个实参必须是数组变量);join(SEP, @a);split(PAT, STR)(PAT 是字符串,按逐字匹配切,等价于单分隔符情形)。 - 哈希:
%h、$h{k}(k裸词当字符串)、exists($h{k})、delete($h{k})、keys %h、values %h、scalar(%h)。键是字符串。 - 元素类型:数组元素 / 哈希值默认按整数处理。一个数组/哈希被赋过字符串字面量列表(或来自
keys/split)时,前端把它记成"字符串数组/哈希",join与foreach按指针处理。混装(同一数组里既有整数又有字符串)不支持 —— 运行期无语义标签。 sort/reverse/each未实现且在编译期报错。
7. 子程序与 use
sub add { my ($x, $y) = @_; return $x + $y; } # 或 sub add($x, $y) { … }
print add(3, 4);
- 调用约定:只收一个实参 —— 实参数组的地址;
@_就是它。 my ($a, $b) = @_;在函数体里按下标取(0 起)。- 先调用后定义允许(前端先扫一遍
sub名)。 - 无
return的子程序返回 0。 use NAME;读Lib/perl/NAME.pl声明文件,登记其中sub 名(函数名)与our $名 = 数字;(编译期常量)。声明文件纯文本扫描、不执行。对库函数,use是文档级的:前端本来就按裸名字解析(ui_2d_clear(0)直接发CALL ui_2d_clear),删掉use照样编得过。
8. 类型的编译期推断(为什么会"看走眼")
本前端没有运行期类型标签。前端按编译期能定的信息在整数 / 浮点 / 字符串之间选通道:
- 字面量、赋过值的变量、
./x的结果、返回字符串的用户子程序 → 能定。 - 从
@_取出来的形参、未知库函数的返回值 → 类型未知:算术按整数、拼串时 原样当指针。 - 后果(有意接受):
"v=" . $from_arg对;但print $from_arg(单独打一个未知类型的形参)会按整数打 —— 若它其实是字符串,打出来是地址。要稳就显式拼一次串或加use声明。
9. 有意不支持(多数在编译期报错)
| 特性 | 状态 |
|---|---|
sort / reverse / each / ucfirst / lcfirst / rindex | 编译期报错「尚未实现」 |
引用 \、解引用、匿名数组/哈希 | 不支持 |
正则 =~、s///、tr/// | 不支持 |
包 / package / 对象 / bless | 不支持 |
local / state / our 的包语义 | 不支持(our 等同 my) |
substr 作为左值、wantarray、context(标量/列表上下文) | 不支持 |
文件句柄 / open / <> / printf 到句柄 | 不支持(要看文件走共享库 / UI 工具) |
qw() | 支持(拆成字符串列表) |
| 64 位整数 | 不支持(32 位) |
| 浮点打印格式 | 走共享运行时的 print_double,固定 6 位小数(3.4 可能显示成 3.399999,那是共享 dtoa 的舍入口径,不是本前端算错) |
10. 判据
scripts/vml-perl-probe/run.sh 跑 bugs/vml_perl_bugs/t*.pl,逐条比 stdout。判据走的是 vmlcli <用例> --lang perl 这条真实通道(注册表 → 汇编 → 链接 → 运行), 刻意不绕开注册表 —— 绕开那条路自带一份链接集,会把"真实通道缺本语言模块集"盖过去。 实测(接线 + GenLib -m -l perl 之后):通过 24 / 失败 0。
判据可红性已实测三遍:
- 期望值注入:把
t02的期望22 12 85 2改成…3⇒ 该条立刻 ❌。 - 源码注入:把
GenerateIncDec的后缀+1改成+2⇒t15由s=10|c=1变s=6|c=2⇒ ❌;还原后复绿。 - 模块集移除:把
Lib/perl/整个挪走 ⇒t07(依赖perlrt.vml)立刻红。
判据可红性已实测两遍:
- 期望值注入:把
t02的期望22 12 85 2改成…3⇒ 该条立刻 ❌。 - 源码注入:把
GenerateIncDec的后缀+1改成+2⇒t15由s=10|c=1变s=6|c=2⇒ ❌;还原后复绿。
编译器 README
Perl 前端编译器(PerlCompiler)
把 Perl 的一个子集编译成 VML 汇编。它是 third_party/vml/VMLPrepares/ 下的第 23 门前端,与其它前端同构:Lexer / Parser / ASTNode / CodeGenerator / PerlPlugin,继承 CompilerBase,由 CompilerPluginExBase 接进宿主。
语言范围(支持什么、不支持什么、为什么)见 [
PERL_LANGUAGE_SPEC.md](PERL_LANGUAGE_SPEC.md)。接线到宿主 / GenLib / 打包的逐条改动见 [INTEGRATION.md](INTEGRATION.md)。
一句话定位
「完整运算 + 控制流 + 函数调用」 那一档 —— 能写一般的脚本程序(数组 / 哈希 / 子程序 / 字符串处理 / 循环 / 递归),不是"能编过 hello world"的骨架。覆盖到 Examples/perl/app/wordcount.pl(词频统计)这种规模。
快速上手
# ① 编译前端本体(开发期它就是一个 CLI)
dotnet build third_party/vml/VMLPrepares/PerlCompiler -c Release
# ② 编一个 .pl 成**独立可运行**的 VML 汇编(标准库已链进去)
dotnet third_party/vml/VMLPrepares/PerlCompiler/bin/Release/net10.0/PerlCompiler.dll \
examples/perl/hello.pl -o /tmp/hello.vml
# ③ 跑
dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll /tmp/hello.vml \
--vml-home third_party/vml --timeout 5
接进宿主之后(见 INTEGRATION.md)就能直接 vml run examples/perl/hello.pl。
判据套件
scripts/vml-perl-probe/run.sh # 全部
scripts/vml-perl-probe/run.sh t07 t10 # 按前缀挑
用例在 bugs/vml_perl_bugs/t*.pl,走 vmlcli <用例> --lang perl 这条真实通道 (注册表 → 汇编 → 链接 → 运行),实测 通过 24 / 失败 0。
⚠ 跑之前先接线(见
INTEGRATION.md):① 按第 1/5/7 条把 perl 接进 vmlcli 的注册表; ② 跑一次GenLib -m -l perl生成Lib/perl/*.vml。缺任一条套件要么响亮退出 2 (注册表里没 perl),要么让用例去红(缺本语言模块集 → 裸名.linked解析到别的语言)。套件刻意不绕开注册表 —— 绕开那条路自带一份链接集,会把"真实通道缺模块"盖过去。
文件
| 文件 | 作用 |
|---|---|
TokenType.cs / Token.cs | 词法单元 |
Lexer.cs | 词法器(# 注释、三种 sigil、单/双引号串、换行作为语句分隔) |
InterpolationParser.cs | 双引号串的 $var 插值与转义 |
ASTNode.cs | 语法树节点 |
Parser.cs | 语法器(含无括号列表算子、后缀 if/unless、三目、++/--) |
CodeGenerator.cs | 代码生成:核心 + 语句(变量槽 / 控制流 / 子程序 / print) |
CodeGenerator.Expressions.cs | 表达式、类型推断、内置函数分派 |
PerlCompiler.cs | 静态入口(读文件 + use 声明 + 链接标准库) |
PerlPlugin.cs | 插件适配器(派生 CompilerPluginExBase) |
Program.cs | 开发期 CLI |
运行期与库:
| 路径 | 作用 |
|---|---|
Lib/perl/perlrt.c | 语言专属运行时源(动态数组 @a / 哈希 %h / 字符串辅助,前缀 pl_) |
Lib/perl/perlrt.vml | 上面那份编出来的模块(vmlcli --rebuild-lib 产出) |
Lib/perl/builtin.vml | 标准库清单(.linked 列表,编译器自动读) |
Lib/perl/vml_ui.pl | UI 库的声明文件(常量 + 函数名,给 use vml_ui; 用) |
设计要点(与其它前端不同的地方)
- 不走 C 预处理器:Perl 拿
#当行注释,而CompileFileStandard一见#就上 C 预处理器。所以PerlCompiler自己读文件、自己设CurrentSourceFile。 - 子程序收一个「实参数组」:
sub编译成只收一个实参(@_那个数组的地址),my ($a, $b) = @_;在函数体里按下标取。这样天然支持 Perl 的变参,也免去"按形参个数一档档读栈槽"的分支。调用点是EmitBuildArray(args)+CALL func_<名>。 - 数组 / 哈希的运行时是 C:
Lib/perl/perlrt.c编成Lib/perl/perlrt.vml。布局与Lib/shared/src/array.c同族([长度, e0, e1, …]),哈希多开一份容量头。增长型操作(push / unshift / set)返回新指针,前端回写变量。 %的歧义:%h是 sigil、$a % $b是取模 —— 词法器按"后面跟不跟标识符"分辨。
已知限制
见 PERL_LANGUAGE_SPEC.md 的「不支持 / 有意简化」一节。要点: sort / reverse / each / ucfirst / lcfirst / rindex 未实现且在编译期报错(不静默); @数组 在双引号里不插值;没有引用 / 包 / 正则 / 对象;整数是 32 位。
许可 / License
与仓库其余部分一致(VML 已与上游分家,见 third_party/vml/FORK.md)。