aheinze / scriptlite
An ECMAScript (ES5/ES6 subset) bytecode-compiled interpreter
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/aheinze/scriptlite
Requires
- php: >=8.3
Requires (Dev)
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-03-02 13:07:18 UTC
README
A sandboxed interpreter for a subset of ECMAScript, written in PHP. Embed user-provided scripts in your PHP application for data processing, configuration logic, computed fields, template expressions, workflow rules, and more — without giving scripts access to the filesystem, network, database, or any PHP internals. It covers the most useful parts of the language (variables, functions, closures, arrays, objects, regex, error handling, destructuring, etc.) while intentionally omitting modules, classes, async/await, generators, and other heavy runtime features.
Scripts run in a sealed environment: they can only use the ECMAScript built-ins listed below and any globals you explicitly pass in. There is no require, no eval, no process, no globalThis — just pure computation on the data you provide.
Use cases
- User-defined formulas — let users write
price * quantity * (1 - discount)in a CMS or spreadsheet-like app - Configuration logic — express feature flags, A/B rules, or pricing tiers as scripts instead of hardcoded PHP
- Data transformation — map, filter, and reshape API payloads or database rows with user-supplied logic
- Computed fields — derive values in a form builder or report engine using expressions like
items.reduce((s, i) => s + i.total, 0) - Workflow / automation rules — evaluate conditions and actions defined by end users at runtime
- Template expressions — safely evaluate interpolated expressions in user-generated content
Execution backends
- Bytecode VM — a stack-based virtual machine with 62 opcodes and register file optimization
- PHP transpiler — compiles ECMAScript to PHP source that OPcache/JIT can optimize natively (~31x faster than the VM)
Quick start
use ScriptLite\Engine; $engine = new Engine(); // Evaluate and get the result $result = $engine->eval(' function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } fib(10); '); // $result === 55 // Or compile once, run many times $compiled = $engine->compile($script); $result = $engine->run($compiled); // Or transpile to PHP for maximum performance $php = $engine->transpile($script); $result = $engine->evalTranspiled($php);
Language support
Types: numbers (int/float), strings, booleans, null, undefined, arrays, objects, regex, Date
Operators: arithmetic (+ - * / % **), increment/decrement (++ --, prefix and postfix), comparison (== != === !== < <= > >=), logical (&& || !), bitwise (& | ^ ~ << >> >>>), nullish coalescing (??), ternary (? :), optional chaining (?., ?.[], ?.()), typeof, void, delete, in, instanceof, assignment (= += -= *= /= %= **= ??= &= |= ^= <<= >>= >>>=)
Control flow: if/else, while, for (including multi-var init: for (let i = 0, j = 10; ...)), for...of, for...in, do...while, switch/case/default, break, continue, return, comma operator in for-updates
Error handling: try/catch/finally, throw, optional catch binding (catch { })
Variables: var (function-scoped, hoisted), let (block-scoped), const (block-scoped, immutable), array destructuring (var [a, b, ...rest] = arr), object destructuring (var {name, age: a} = obj) with defaults, nested destructuring (var {user: {name, age}} = obj, var [a, [b, c]] = arr), destructuring in function parameters (function f({x, y}) {})
Functions: declarations, expressions, arrow functions (=> with expression and block bodies), closures with lexical scoping, recursion, new / constructors / this, rest parameters, spread syntax, default parameters, destructuring parameters with nesting and defaults
Object literals: shorthand properties ({x, y}), computed property names ({[expr]: value})
Template literals: `hello ${name}` with expression interpolation and nesting
String escapes: \n, \t, \r, \\, \0, \uXXXX, \u{XXXXX}, \xXX
Built-ins:
console.log()Math.floor,Math.ceil,Math.abs,Math.max,Math.min,Math.round,Math.random,Math.PI,Math.E,Math.sqrt,Math.pow,Math.sin,Math.cos,Math.tan,Math.asin,Math.acos,Math.atan,Math.atan2,Math.log,Math.log2,Math.log10,Math.exp,Math.cbrt,Math.hypot,Math.sign,Math.trunc,Math.clz32,Math.LN2,Math.LN10,Math.LOG2E,Math.LOG10E,Math.SQRT1_2,Math.SQRT2Number(),Number.isInteger(),Number.isFinite(),Number.isNaN(),Number.parseInt(),Number.parseFloat()String(),String.fromCharCode()parseInt(),parseFloat(),isNaN(),isFinite(),encodeURIComponent(),decodeURIComponent(),encodeURI(),decodeURI()NaN,Infinity,undefinedDate,Date.now()JSON.stringify(),JSON.parse()
Number methods: toFixed, toPrecision, toExponential, toString (with radix)
Array methods: push, pop, shift, unshift, map, filter, reduce, reduceRight, forEach, every, some, find, findIndex, findLast, findLastIndex, indexOf, includes, join, concat, slice, splice, sort, reverse, flat, flatMap, fill, at
String methods: split, toUpperCase, toLowerCase, trim, trimStart, trimEnd, charAt, substring, startsWith, endsWith, repeat, replace (with string or callback), replaceAll, match, matchAll, search, indexOf, includes, slice, padStart, padEnd, at
Object methods: hasOwnProperty, Object.keys, Object.values, Object.entries, Object.assign, Object.is, Object.create, Object.freeze
Regex: literals (/pattern/flags), RegExp constructor, test(), exec(), flags g i m
PHP interop
Pass PHP variables into the script via the second argument to eval(), run(), or evalTranspiled(). Results are automatically converted back to PHP types.
Primitives, arrays, and closures
$engine = new Engine(); // Scalars are passed through directly $result = $engine->eval('name + " is " + age', [ 'name' => 'Alice', 'age' => 30, ]); // $result === "Alice is 30" // PHP indexed arrays become arrays $result = $engine->eval('items.map(x => x * 2)', [ 'items' => [1, 2, 3], ]); // $result === [2, 4, 6] // PHP associative arrays become objects $result = $engine->eval('config.host + ":" + config.port', [ 'config' => ['host' => 'localhost', 'port' => 3000], ]); // $result === "localhost:3000" // PHP closures become callable functions $result = $engine->eval('transform("hello")', [ 'transform' => fn(string $s) => strtoupper($s), ]); // $result === "HELLO"
PHP object instances
PHP objects are automatically wrapped so scripts can read/write properties and call methods. Method arguments are auto-coerced to match PHP type hints (ECMAScript numbers are floats, but PHP methods may expect int, string, etc.).
class Account { public function __construct( public string $owner, public float $balance, ) {} public function deposit(float $amount): float { $this->balance += $amount; return $this->balance; } public function withdraw(float $amount): float { $this->balance -= $amount; return $this->balance; } } $acc = new Account('Alice', 1000); $engine->eval(' acc.deposit(250); acc.withdraw(75); ', ['acc' => $acc]); // The original PHP object is mutated: // $acc->balance === 1175.0
Objects returned from methods are also wrapped, so chained access works. PHP closures and arrays returned from methods are converted to their ECMAScript equivalents.
Return value conversion
| ECMAScript type | PHP type |
|---|---|
| number (int) | int |
| number (float) | float |
| string | string |
| boolean | bool |
| null | null |
| undefined | null |
| array | array (indexed) |
| object | array (associative) |
Transpiler path
The same globals work with the transpiler. The transpile step only needs variable names (so the scope tracker captures them correctly); actual values are provided at execution time:
// One-shot: transpile and execute in a single call $result = $engine->transpileAndEval($script, ['acc' => $acc, 'multiplier' => 2]); // Transpile once, run many times with different values $callback = $engine->getTranspiledCallback($script, ['acc', 'multiplier']); $result = $callback(['acc' => $acc1, 'multiplier' => 2]); $result = $callback(['acc' => $acc2, 'multiplier' => 3]); // Or step by step for full control: $php = $engine->transpile($script, ['acc', 'multiplier']); $result = $engine->runTranspiled($php, ['acc' => $acc]); // temp file (worker-safe) $result = $engine->evalTranspiled($php, ['acc' => $acc]); // eval (leaks in long-running workers) // Or save to a file for OPcache: $engine->saveTranspiled($php, '/tmp/script.php'); $__globals = ['acc' => $acc, 'multiplier' => 2]; $result = include '/tmp/script.php';
Security model
Scripts execute in a fully sandboxed environment:
- No filesystem access — no
require,import,fs, or file I/O of any kind - No network access — no
fetch,XMLHttpRequest, or sockets - No PHP internals — no
eval,exec,system, or access to PHP's global scope - No ambient globals — no
process,globalThis,window, ordocument - Explicit data boundary — scripts can only see globals you pass in via the
$globalsparameter - Pure computation — the only side effects are mutations to objects/arrays you explicitly provide
The attack surface is limited to CPU and memory consumption. For untrusted input, combine with PHP's set_time_limit() / memory_limit to cap resource usage.
Architecture
ECMAScript source
│
├── Lexer ──▶ Token stream
│
├── Parser (Pratt) ──▶ AST
│
├─┬── Compiler ──▶ Bytecode ──▶ VM (stack machine)
│ │
│ └── PhpTranspiler ──▶ PHP source ──▶ eval (OPcache/JIT)
| Directory | Purpose |
|---|---|
src/Lexer/ |
Zero-copy tokenizer, regex literal support |
src/Ast/ |
AST node types + Pratt parser |
src/Compiler/ |
Single-pass AST → bytecode compiler |
src/Vm/ |
Stack-based bytecode VM |
src/Runtime/ |
Runtime objects (JsArray, JsObject, JsClosure, JsRegex, JsDate, Environment) |
src/Transpiler/ |
AST → PHP source code transpiler with type inference |
VM opcodes
The VM uses 62 int-backed enum opcodes organized by category: stack ops, arithmetic, comparison, bitwise, variables (including register-optimized GetReg/SetReg), control flow, functions, exception handling, scope, and property access. The match() on int-backed enums compiles to a jump table under OPcache/JIT.
Non-captured local variables (var declarations and parameters) are allocated to an integer-indexed register file at compile time, bypassing the Environment hash table for ~13x faster variable access on hot paths. Variables captured by inner closures remain in the Environment scope chain to preserve correct closure semantics.
Transpiler
The transpiler maps ECMAScript constructs directly to PHP equivalents:
- Objects → PHP associative arrays
- Arrays → PHP indexed arrays
- Functions → PHP closures with
use (&$captured)for scope capture - Constructors → closures that build and return arrays
- Methods → inlined PHP built-in calls (
array_map,explode,preg_replace, etc.)
Tests
php vendor/bin/phpunit tests/
783 PHPUnit tests (1747 assertions) across 34 test files covering arithmetic, arrays, arrow functions, break/continue, constructors, control flow, destructuring (including nested and function params), do-while, for...of/for...in, functions, globals, JSON, number/string objects, objects, operators, optional chaining, regex, scoping, string methods (including replace with callbacks), switch, template literals, try/catch/finally, spread/rest, extended operators (increment/decrement, exponentiation, bitwise, void, delete, in, instanceof), comma operator, reduceRight, fuzzing, and edge cases.
Benchmark
php bench.php
Runs 10 workloads (sieve of Eratosthenes, fibonacci with memoization, quicksort, matrix multiplication, string/regex processing, functional pipeline, constructors, CSV parsing, histogram, recursive tree) and compares execution modes:
| Mode | Execution time | vs Native PHP |
|---|---|---|
| VM (bytecode interpreter) | ~80 ms | ~108x |
| Transpiled PHP (eval'd) | ~2.6 ms | ~3.5x |
| Native PHP (hand-written) | ~0.75 ms | 1x |
License
MIT