PHP Is Compiled. An experiment to rewrite PHP so that it's really fast.
  • Go 97.2%
  • PHP 2.2%
  • Shell 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-13 19:00:04 -04:00
bench Beat PHP on all benches and score 100% on a PHP stdout corpus. 2026-09-21 06:46:41 -04:00
cmd/pic Ship pic v0.1: compile a PHP dialect to a persistent Go process. 2026-09-04 21:38:16 -04:00
compat Beat PHP on all benches and score 100% on a PHP stdout corpus. 2026-09-21 06:46:41 -04:00
drivers Embed the pic source tree in the binary; no PIC_ROOT needed 2026-09-09 10:40:52 -04:00
examples Close out Slice 3: notes example app and docs 2026-09-09 02:34:48 -04:00
internal foreach: clone array before iteration for copy-on-iterate semantics 2026-10-13 18:39:38 -04:00
runtime Resolve names the way PHP does inside namespaces. 2026-10-13 16:42:48 -04:00
.gitignore Phase 4: in-repo bench harness and README numbers 2026-09-09 15:26:03 -04:00
go.mod Embed the pic source tree in the binary; no PIC_ROOT needed 2026-09-09 10:40:52 -04:00
go.sum Embed the pic source tree in the binary; no PIC_ROOT needed 2026-09-09 10:40:52 -04:00
Makefile Ship pic v0.1: compile a PHP dialect to a persistent Go process. 2026-09-04 21:38:16 -04:00
README.md Resolve names the way PHP does inside namespaces. 2026-10-13 16:42:48 -04:00
ROADMAP.md Resolve names the way PHP does inside namespaces. 2026-10-13 16:42:48 -04:00
root_embed.go Embed the pic source tree in the binary; no PIC_ROOT needed 2026-09-09 10:40:52 -04:00

pic

PHP is compiled. Compile PHP to a persistent Go process. There is no PHP-FPM. The process does not die after a request.

You need a Go toolchain (1.22 or later) on the machine that compiles. The output binary does not need PHP.

Open work is in ROADMAP.md.

Run now

go build -o pic ./cmd/pic
./pic serve examples/counter --listen :8080

Then open http://127.0.0.1:8080/. Refresh the page. The hits value grows because the process stays up.

CLI script:

./pic run examples/cli/hello.php
./pic run examples/cli/hello.php sailor

Compile a binary:

./pic build examples/api -o ./api-server
./api-server

Commands

Command Action
pic run FILE.php [args] Compile and run a CLI script
pic serve [DIR] Compile the app and serve HTTP
pic build [DIR] -o FILE Compile to a binary
pic dump FILE.php Print generated Go
pic version Print the version

Serve flags: --listen :8080, --public DIR, --watch.

If the app has a public/ directory, that directory is the HTTP root. Other .php files still compile (for include). If a path is not a PHP file and is not a static file, index.php handles the request (front controller).

Why this is faster than regular PHP

  1. One OS process for many requests. No FPM spawn, no per-request interpreter start.
  2. PHP compiles to Go, then to a native binary. Request handling is a function call.
  3. Each request runs on a goroutine. Concurrent requests share the process.
  4. static variables, class statics, $GLOBALS, $_SESSION, and pic_cache_* live across requests.

The runtime still boxes PHP values, but scalar kernels compile provably pure int/string code to typed Go with no boxing, arrays are hash-indexed (O(1) lookups), and the JSON codec talks to Values directly. The large win is the persistent process.

Benchmarks vs vanilla PHP

bench/run.sh runs head-to-head microbenchmarks (php CLI vs native pic binaries). Latest medians on this machine (PHP 8.5.10):

workload PHP (ms) pic (ms) pic vs PHP
int += loop, 20M 951 6 158.5x faster
string append 200K + strlen 24 3 8.0x faster
json_encode 20K rows x20 36 27 1.3x faster
2M array build + foreach sum 168 7 24.0x faster
app-shaped (JSON/PDO/session) 58 46 1.3x faster

Kernel-covered loops beat PHP by 1-2 orders of magnitude. Counted loops that build objects and assoc arrays fill a pre-sized slab, so the json and app workloads also beat PHP on this machine. Details live in bench/.

Persistence model (breaking)

In PHP-FPM, almost all memory dies at the end of the request. In pic it does not.

State Lives across requests
Locals in the page / handler no (reset each request)
$_GET, $_POST, $_SERVER, $_COOKIE no
Function static $x yes
Class static properties yes
$GLOBALS yes
$_SESSION (after session_start()) yes (in memory)
pic_cache_get / pic_cache_set yes
Loaded functions and classes yes (compiled into the binary)

exit and die end the request, not the process.

Dialect: what works

  • Tags: <?php, <?=, ?>, inline HTML
  • Types: null, bool, int, float, string, array, object, closures
  • Control: if / elseif / else, while, do while, for, foreach, switch, break, continue, return
  • Exceptions: try / catch / finally / throw, multiple catches, rethrow, finally with return
  • Functions, defaults, fn() arrow functions, closures with use
  • Classes: properties, methods, static methods/properties, new, $this, ::, extends and parent::, interface / implements
  • Magic methods: __construct, __destruct, __get, __set, __isset, __call, __toString
  • Modules: namespace (unbraced, braced, and namespace { }), use / use function / use const (including group use), namespace\Name, __NAMESPACE__, ClassName::class, composer PSR-4 autoload at compile time (no vendor/autoload.php needed)
  • Arrays: [...], array(), =>, $a[] =, nested assign
  • Operators: arithmetic, concat, compare (== ===), logic, ??, ?:, ++ --, assign ops
  • echo, print, isset, empty, unset, include / require (_once is process-once)
  • Superglobals: $_GET $_POST $_SERVER $_COOKIE $_SESSION $_FILES $_ENV $_REQUEST $argv
  • Common builtins: strings, arrays, json_encode / json_decode, header, http_response_code, file_get_contents (including php://input), preg_match / preg_replace (Go regexp), date, hashes, URL helpers, pic_cache_*
  • Passwords: password_hash / password_verify / password_needs_rehash (bcrypt, $2y$, cost option; hashes verify against real PHP)
  • PDO with prepared statements, transactions and PDOException: drivers for SQLite, PostgreSQL and MySQL are linked per app by DSN scheme (sqlite:, pgsql:, mysql:)
  • Time: DateTime / DateTimeImmutable / DateTimeZone / DateInterval, createFromFormat, date_create, date_default_timezone_*
  • HTTP client: curl_init / curl_setopt / curl_exec / curl_getinfo and friends with the real CURLOPT_* numbers
  • Uploads: $_FILES is filled from multipart posts; move_uploaded_file / is_uploaded_file work like PHP

Dialect: breaking changes

  • Names are case-sensitive (Foo and foo are different).
  • Exceptions need an object to throw; a bare throw 'msg' is not catchable by class. Uncaught exceptions are a 500 ("pic fatal: uncaught exception ..."), not a process exit.
  • break / continue cannot cross a try / catch / finally boundary (the try body compiles to its own function).
  • No references (&$x). Arrays and objects are pointers, so in-place mutate still works.
  • include paths must be string literals, resolved at compile time.
  • All functions and classes in the app compile in. You do not need to include a file only to define a function.
  • require_once / include_once run once per process, not once per request.
  • No eval, variable-variables, goto, traits, abstract/final, enums, attributes, generators, or interface extends.
  • Every referenced class and function must resolve to a loaded file or a builtin: unknown names are compile errors, not runtime includes.
  • No short open tag <?.
  • Type juggling is smaller than PHP. "10 cats" + 1 does not become 11.
  • json_decode returns arrays (objects in JSON become associative arrays).
  • preg_* uses Go regexp, not PCRE.
  • foreach walks the live array. It does not copy first.
  • Undefined locals start as null. There are no notices.
  • header() always applies. Output is buffered until the request ends.
  • private / protected members are enforced: outside access throws Error, subclasses may use protected members, and __get/__set/__isset still intercept inaccessible reads/writes.
  • Calling a userland function or method with too few required arguments throws ArgumentCountError, like PHP 8.
  • Session files expire after PIC_SESSION_TTL seconds (default 1440, PHP's gc_maxlifetime); session_gc() sweeps immediately.
  • Interfaces are a compile-time contract only: implements is checked when pic builds, and interfaces emit no runtime code.
  • Array property defaults are copied per instance, but arrays passed around elsewhere are shared pointers (no copy-on-write).
  • PDO DSNs must be string literals (new PDO("sqlite:app.db")), because pic links the database driver at compile time. The first build of an app that uses a driver downloads that driver's Go modules.
  • A PDO connection belongs to its request: the sqlite file handle (and any unread cursor) is released when the request ends. Upload temp files are deleted at request end too, unless moved with move_uploaded_file.
  • Assignments are expressions like in PHP (while ($row = $stmt->fetch()) works).
  • Bare constants in a namespace fall back to the global constant (PHP's rule), so password_hash($p, PASSWORD_DEFAULT) works in namespaced code.
  • Tight loops over provable int/string code compile to typed Go ("scalar kernels"): a 20M-iteration sum runs in ~5 ms, string-appends build with a linear builder. Code with calls, arrays, objects or floats keeps the general path, and named kernel functions fall back to it whenever an argument's kind differs - results never change, only the code path.
  • pic needs no source checkout: the runtime and PDO drivers are embedded in the binary and extracted to the user cache on demand (PIC_ROOT still wins when set).
  • Sessions are file-backed (PIC_SESSION_DIR overrides the per-user cache): a restart no longer drops logins.
  • pic serve --watch restarts gracefully: the old process drains in-flight requests before the rebuilt one binds the port.

Extra builtins: pic_cache_get($key), pic_cache_set($key, $value), pic_cache_delete($key).

Layout

cmd/pic            CLI
internal/lexer     tokenizer
internal/parser    PHP dialect AST
internal/compiler  PHP AST to Go
internal/build     go build wrapper
internal/source    source-tree resolver (checkout or embedded copy)
runtime            request, values, stdlib, pdo core, curl, dates
drivers/           PDO drivers (picdb build-tagged, linked per app)
examples/          hello, counter, api, cli, notes (PDO JSON API)

Build from source

go test ./...
go build -o pic ./cmd/pic

If pic cannot find this source tree, set PIC_ROOT to the repo root. The compiler reads runtime/ from that tree when it emits a Go module.