Perl
The "full arithmetic + control flow + function calls" tier: arrays (@a), hashes (%h), subroutines, string handling, loops and recursion all work - enough for something the size of a word-frequency counter.
print takes several arguments at once (printed back to back with no separator), and $var is interpolated inside double-quoted strings.
How to run it on your phone
vml run examples/perl/app/wordcount.pl
Compiling takes a while (one to two minutes for C, a few seconds for scripting languages). Once the program is running, the gamepad is at the bottom of the screen - the D-pad and the four action buttons are all there. Tap the back arrow in the top-right corner to return to the Shell.
What to know before you write
- Declare with
my $x/my @a/my %h; you can also use a variable without declaring it (it lands in the globals, value 0) - A newline is a statement separator, so the
;at the end of a line can often be left out - Call
ui_*directly (the front end emits aCALLon the bare name);use vml_ui;is a declaration only - remove it and the program still compiles - Print with
print/say(sayappends a newline for you) $#ais the last index of an array andscalar(@a)its length
Examples
| File | What it shows |
|---|---|
app/wordcount.pl | Word frequency counting (split plus a hash tally plus scalar) |
hello.pl | The smallest program (multi-argument print plus double-quote interpolation) |
test/syntax.pl | A syntax tour: one form used for every feature |
Pitfalls we hit on real devices
- ⚠ There are no runtime type tags: types are inferred at compile time, and parameters read out of
@_plus the return values of unknown library functions are of unknown type (arithmetic goes integer, concatenation treats them as pointers). Soprinting an unknown-type parameter that is really a string prints its address; concatenate it once to be safe. - ⚠ There are two comparison families (numeric
== != < > <= >=and stringeq ne lt gt le ge), and using the wrong one is not an error - it just compares by the other rule. - ⚠
sort/reverse/each/ucfirst/lcfirst/rindexare not implemented and report a compile-time error (never silently); a badpushargument errors too. @arris not interpolated inside a double-quoted string (array interpolation would need to join the elements, and their type cannot be decided at compile time) - usejoin($sep, @arr).- No references / dereferencing / anonymous arrays or hashes / regexes / packages / objects; integers are 32-bit.
- A subroutine takes exactly one argument (the address of the
@_argument array);my ($a, $b) = @_;indexes into it by position. Scoping is flat:myallocates a function-level stack slot, not a block-scoped one. - Float printing goes through the shared runtime with a fixed 6 decimal places (
3.4may show as3.399999- that is the shared dtoa rounding, not the front end miscomputing).
The rest of this page is taken straight from the VML source (third_party/vml/VMLPrepares/PerlCompiler/): the README covers what this front end supports and how to compile it, and the language reference covers the syntax itself. When upstream changes, regenerating this page brings it up to date.
Language reference
The Perl subset implemented by the PerlCompiler frontend
This document is the implementation contract of the Perl frontend: what syntax and semantics are supported, where the silent traps are, and what is deliberately not supported (and reports a compile-time error instead of pretending). The Chinese version is [PERL_LANGUAGE_SPEC.md](PERL_LANGUAGE_SPEC.md).
Version: v1.0 | Date: 2026-10-13 | Revised by: Shenzhen Tanso Intelligent Technology Co., Ltd.
Specification standard
| Field | Value |
|---|---|
| Target standard | Perl 5 subset |
| Publication year | 1994 |
| Completeness | Subset (not itemised) |
| Tests | 24 passing / 0 failing |
Keywords
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
Overview
A subset of Perl 5 is supported, compiled to VML assembly. Variables carry sigils ($ scalar / @ array / % hash; $x, @x and %x are three independent names at the lexical level) and a newline is a statement separator (equivalent to ;). It deliberately does not run the C preprocessor (Perl uses # for line comments, so treating # as a directive marker would turn every comment into an instruction). The frontend has no runtime type tags: it picks between the integer, float and string channels using what can be decided at compile time (parameters read out of @_ and the return values of unknown library functions are of unknown type — see §8). Unimplemented builtins produce a compile-time error; there is no fallback that "quietly runs as 0".
Supported language features
The features this frontend accepts are organised by the sections below; each one states what is accepted and what it means:
- Lexical: comments, numbers, single-quoted strings (only
\\and\'), double-quoted strings with$name/${name}interpolation, identifiers, sigils,$#a, newline as separator — §1 - Variables and declarations:
my/our(synonymous in this subset), separate slots for scalars / arrays / hashes, usable without declaration, flat scoping — §2 - Expressions and operators: arithmetic (
/is float division,«bold»is integer power),.concatenation andxrepetition, two** comparison families (numeric and string), two logical families,++/--— §3 - Statements and control flow:
if/elsif/else/unless,while/until,for/foreach, postfix modifiers,last/next/redo— §4 - Strings: multi-argument
print/say,length/substr/index/uc/lc/chomp/chop/sprintf/int/abs,@arris not interpolated — §5 - Arrays and hashes:
$a[i](negative indices count from the end),$#a,push/pop/shift/unshift,join/split,%h/exists/delete/keys/values— §6 - Subroutines and
use:subdefinition and calls (including thesub f($x, $y)form), call-before-define,use NAME;reading a declaration file — §7 - Compile-time type inference: why it can "guess wrong" (a tradeoff accepted on purpose) — §8
- Deliberately unsupported: regexes, references and dereferencing, packages and objects, package semantics for
local/state, file handles, 64-bit integers — most report a compile-time error — §9
See the sections below for details.
0. Principles
- Bar: you can write an ordinary program — arithmetic, control flow, function calls, arrays, hashes, string handling.
- No silent fallback: there is no builtin that "quietly runs as 0". Unimplemented builtins (e.g.
sort) produce a compile-time error, and a badpushargument does too. Better to refuse than to return a wrong answer. - AOT, no reflection, no hard-coded addresses: only standard VML instructions and
CALLs to named labels are emitted.
1. Lexical
| Form | Notes |
|---|---|
| Comments | # to end of line |
| Numbers | decimal integer, 0x… hex, fractional (1.5). Integers are 32-bit (extra bits truncated) |
| Single-quoted | only \\ and \' are escapes; everything else is literal |
| Double-quoted | \n \t \r \0 \\ \" \$ \@; $name / ${name} interpolate; @name does not (see §5) |
| Identifiers | letters / digits / _ / CJK |
| Sigils | $ scalar, @ array, % hash. A % followed by an identifier is a sigil, otherwise it is modulo |
$#a | last index of array a (length − 1) |
| Newline | is a statement separator (equivalent to ;), but suppressed when the previous token is an operator/comma/open bracket, so multi-line expressions still work |
Keywords: 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. Variables and declarations
my $x/my @a/my %h;ouris a synonym (there is no real package-variable semantics here —ourbehaves likemy).$x/@x/%xare three independent names (the sigil decides what is fetched); the frontend keeps a slot per(sigil, name).- No declaration required (Perl without
use strict): an undeclared scalar lands in the data section as a global initialised to 0. - Scoping is flat (an intentional simplification):
myallocates a function-level stack slot with no block scope; two same-namedmys share one slot. Subroutines share one symbol table (the same trade-off the Lua frontend makes).
3. Expressions and operators
- Arithmetic:
+ - * / % **./is floating-point division (Perl semantics:7/2→3.5); if either side is a float the whole expression goes through doubles.«bold»is integer power** (shared libraryipow). Convert the base yourself for floats.
- Strings:
.concatenation (numbers are converted to decimal text),xrepetition ("ab" x 3),.=append. - Two comparison families (the easiest trap):
- numeric:
== != < > <= >= - string:
eq ne lt gt le ge(viastrcmp) - mixing them up does not error — you just get the other comparison (exactly as in Perl).
cmpreturns-1/0/1.
- numeric:
- Logic:
&& || !andand or notare both accepted, both short-circuit and return 0/1 (not Perl's "last operand"). The ternary?:is supported. ++/--: prefix and postfix; postfix returns the old value. Implemented as the equivalent+1then−2(a single step).unlessisif(not …);untiliswhile(not …).
4. Statements and control flow
if / elsif / else,unless,while,until,for / foreach, and C-stylefor (init; cond; step).- Postfix modifiers:
… if cond;/… unless cond;/… while cond;. last/next(redoapproximated bynext).- A bare
{ … }block is expanded in place.use NAME;is described in §7.
5. Strings
printtakes multiple arguments and prints them back to back with no separator;sayappends a newline. A bareprintprints$_(approximated as 0).print @aprints the element count (element type is not statically known, see §8; useforeachto print elements).- Builtins:
length,substr($s,$pos[,$len])(negative index/length supported),index($s,$sub[,$start]),uc,lc,chomp,chop,sprintf(%d %i %x %s %c %%plus width/left-align; precision is skipped),int,abs. @arrdoes not interpolate inside double quotes (array interpolation would have to join by element and the element type is not statically known). Usejoin($sep, @arr).
6. Arrays and hashes
- Arrays:
@a,$a[i](negative index counts from the end),$#a,scalar(@a),length(@a);push / pop / shift / unshift(the first argument must be an array variable);join(SEP, @a);split(PAT, STR)(PAT is a string, matched literally — equivalent to the single-separator case). - Hashes:
%h,$h{k}(a barewordkbecomes a string),exists($h{k}),delete($h{k}),keys %h,values %h,scalar(%h). Keys are strings. - Element types: array elements / hash values default to integers. When an array/hash has been assigned a list of string literals (or comes from
keys/split), the frontend records it as a "string array/hash" andjoin/foreachtreat it as pointers. Mixing integers and strings in one array is not supported — there are no runtime type tags. sort/reverse/eachare not implemented and report a compile-time error.
7. Subroutines and use
sub add { my ($x, $y) = @_; return $x + $y; } # or sub add($x, $y) { … }
print add(3, 4);
- Calling convention: a subroutine takes exactly one argument — the address of an argument array;
@_is that array. my ($a, $b) = @_;indexes into it (0-based) in the body.- Calling before defining is allowed (the frontend scans
subnames first). - A subroutine without
returnreturns 0. use NAME;reads the declaration fileLib/perl/NAME.pland registers itssub nameentries (function names) andour $name = <number>;entries (compile-time constants). The file is scanned as plain text, never executed. For library functionsuseis documentation-level: the frontend already resolves calls by bare name (ui_2d_clear(0)emitsCALL ui_2d_clear), so removingusestill compiles.
8. Compile-time type inference (why it sometimes "guesses wrong")
There are no runtime type tags. The frontend picks the integer / float / string channel from what is statically known:
- literals, assigned variables, the result of
./x, and string-returning user subroutines → known. - Parameters read from
@_and returns from unknown library functions → unknown: arithmetic treats them as integers, and string concatenation uses them as pointers. - Consequences (accepted):
"v=" . $from_argis fine; butprint $from_arg(printing an unknown-typed parameter on its own) uses the integer channel — if it is really a string you will see an address. For safety, concatenate explicitly or add ausedeclaration.
9. Deliberately unsupported (most report a compile-time error)
| Feature | Status |
|---|---|
sort / reverse / each / ucfirst / lcfirst / rindex | compile-time error "not implemented" |
References \, deref, anonymous array/hash | unsupported |
Regex =~, s///, tr/// | unsupported |
Packages / package / objects / bless | unsupported |
local / state / package semantics of our | unsupported (our is my) |
substr as an lvalue, wantarray, context (scalar vs list) | unsupported |
File handles / open / <> / printf to a handle | unsupported (use shared libs / UI tools) |
qw() | supported (splits into a string list) |
| 64-bit integers | unsupported (32-bit) |
| Float printing format | uses the shared runtime's print_double, fixed 6 decimals (3.4 may show as 3.399999 — that is the shared dtoa's rounding, not a frontend arithmetic error) |
10. Criteria
scripts/vml-perl-probe/run.sh runs bugs/vml_perl_bugs/t*.pl and compares stdout byte for byte. It goes through the real channel, vmlcli <case> --lang perl (registry -> assemble -> link -> run), and deliberately does not bypass the registry (that path carries its own link set and would hide a missing language module set). Measured (with the wiring plus GenLib -m -l perl): 24 pass / 0 fail.
Red-ability has been tested twice:
- Expected-value injection: changing
t02's expectation22 12 85 2to…3makes it fail immediately. - Source injection: changing the postfix
+1inGenerateIncDecto+2turnst15froms=10|c=1intos=6|c=2(fail); reverting turns it green again.
Compiler README
Perl frontend compiler (PerlCompiler)
Compiles a subset of Perl to VML assembly. It is the 23rd frontend under third_party/vml/VMLPrepares/, built like the others: Lexer / Parser / ASTNode / CodeGenerator / PerlPlugin, deriving from CompilerBase and plugged into the host through CompilerPluginExBase.
What the language covers, and what it deliberately does not, is in [
PERL_LANGUAGE_SPEC.en.md](PERL_LANGUAGE_SPEC.en.md). The exact wiring steps (host registration / GenLib / packaging) are in [INTEGRATION.md](INTEGRATION.md).
Positioning
It sits in the "full arithmetic + control flow + function calls" tier — you can write ordinary scripts (arrays / hashes / subroutines / string handling / loops / recursion), not just a "hello world that compiles". Examples/perl/app/wordcount.pl (word frequency counting) is the scale it targets.
Quick start
# 1) build the frontend itself (during development it is also a CLI)
dotnet build third_party/vml/VMLPrepares/PerlCompiler -c Release
# 2) compile a .pl into a **self-contained runnable** VML assembly
dotnet third_party/vml/VMLPrepares/PerlCompiler/bin/Release/net10.0/PerlCompiler.dll \
examples/perl/hello.pl -o /tmp/hello.vml
# 3) run it
dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll /tmp/hello.vml \
--vml-home third_party/vml --timeout 5
Once wired into the host (see INTEGRATION.md) you can just do vml run examples/perl/hello.pl.
Probe suite
scripts/vml-perl-probe/run.sh # all
scripts/vml-perl-probe/run.sh t07 t10 # by prefix
Cases live in bugs/vml_perl_bugs/t*.pl and go through the real channel, vmlcli <case> --lang perl (registry -> assemble -> link -> run); measured 24 pass / 0 fail.
Wire it up first (see
INTEGRATION.md): (1) register perl into vmlcli's registry per items 1/5/7; (2) runGenLib -m -l perlto generateLib/perl/*.vml. Without either one the suite either exits 2 loudly (no perl in the registry) or lets the cases go red (missing language module set -> bare-name.linkedresolves into another language). The suite deliberately does not bypass the registry: that path carries its own link set and would hide a missing-module defect in the real channel.
Files
| File | Role |
|---|---|
TokenType.cs / Token.cs | Token model |
Lexer.cs | Lexer (# comments, three sigils, single/double quoted strings, newline as a statement separator) |
InterpolationParser.cs | $var interpolation and escapes inside double-quoted strings |
ASTNode.cs | AST nodes |
Parser.cs | Parser (with paren-less list operators, postfix if/unless, ternary, ++/--) |
CodeGenerator.cs | Code generation: core + statements (slots / control flow / subs / print) |
CodeGenerator.Expressions.cs | Expressions, type inference, builtin dispatch |
PerlCompiler.cs | Static entry point (read file + use declarations + link stdlib) |
PerlPlugin.cs | Plugin adapter (derives CompilerPluginExBase) |
Program.cs | Development CLI |
Runtime and libraries:
| Path | Role |
|---|---|
Lib/perl/perlrt.c | Language-specific runtime source (dynamic arrays @a / hashes %h / string helpers, pl_ prefix) |
Lib/perl/perlrt.vml | The compiled module (produced by vmlcli --rebuild-lib) |
Lib/perl/builtin.vml | Standard library list (a .linked list the compiler reads automatically) |
Lib/perl/vml_ui.pl | Declaration file for the UI library (constants + function names, for use vml_ui;) |
Design notes (where it differs from other frontends)
- No C preprocessor. Perl uses
#for line comments whileCompileFileStandardruns the C preprocessor as soon as it sees a#. SoPerlCompilerreads the file itself and setsCurrentSourceFileitself. - A subroutine takes one "argument array".
subcompiles to a function taking a single argument — the address of the@_array;my ($a, $b) = @_;indexes into it in the body. This gives variadic subs for free and avoids one branch per arity. The call site isEmitBuildArray(args)+CALL func_<name>. - Arrays/hashes have a C runtime.
Lib/perl/perlrt.cis compiled toLib/perl/perlrt.vml. Layout is in the same family asLib/shared/src/array.c([len, e0, e1, …]); hashes carry an extra capacity header. Growing operations (push / unshift / set) return a new pointer and the frontend writes it back. %is ambiguous:%his a sigil while$a % $bis modulo — the lexer decides by whether an identifier follows.
Known limitations
See the "not supported / intentionally simplified" section of PERL_LANGUAGE_SPEC.en.md. In short: sort / reverse / each / ucfirst / lcfirst / rindex are not implemented and report a compile-time error (no silent fallback); @array does not interpolate inside double quotes; no references / packages / regex / objects; integers are 32-bit.
License
Same as the rest of the repository (VML has forked from upstream, see third_party/vml/FORK.md).