PHP Is Compiled. An experiment to rewrite PHP so that it's really fast.
  • Go 90.9%
  • PHP 7.9%
  • Shell 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jnana fd203de7b2 session 70 (task-36-named-args): partial progress, the model's work
Verdict: partial   mcode_exit=1 own_case_failing=130/131
corpus=130/131
The task is not finished, and this is the work that is: the build, the tests and every case that was
already committed still pass, so keeping it costs nothing and re-running the task costs 35-45 minutes of
GPU. Its own new case does not pass yet, and a failing case is not committed with it.
Left in the capture for the next session on this task: compat/cases/named_args.php
2026-10-21 11:34:51 -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 session 18 (task-19-build-temp-leak-run): partial progress, the model's work 2026-10-19 02:57:25 -04:00
compat readonly classes, with reasoning no longer preserved in the history 2026-10-21 08:55:14 -04:00
drivers Embed the pic source tree in the binary; no PIC_ROOT needed 2026-09-09 10:40:52 -04:00
examples examples/shelf: clear every .db in the oracle's copies, not just the top level 2026-10-14 18:18:21 -04:00
internal session 70 (task-36-named-args): partial progress, the model's work 2026-10-21 11:34:51 -04:00
runtime readonly classes, with reasoning no longer preserved in the history 2026-10-21 08:55:14 -04:00
tasks/cases session 49 (task-35-trait-conflict): partial progress, the model's work 2026-10-20 21:17:26 -04:00
.gitignore gitignore: scratch leftovers (.commandcode/, test.php) that kept showing up as untracked 2026-10-14 22:52:04 -04:00
escape_test.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
FINAL_SUMMARY.md session 8 (task-03b-file-write): the model's work, verified clean 2026-10-18 16:05:59 -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
IMPLEMENTATION_SUMMARY.md session 2 (task-02a-ob-core): the model's work, verified clean 2026-10-18 14:15:02 -04:00
Makefile Ship pic v0.1: compile a PHP dialect to a persistent Go process. 2026-09-04 21:38:16 -04:00
php_out.txt session 2 (task-02a-ob-core): the model's work, verified clean 2026-10-17 16:24:03 -04:00
pic_out.txt session 2 (task-02a-ob-core): the model's work, verified clean 2026-10-17 16:24:03 -04:00
README.md docs: writing an app for pic - re-read state per request, and move off :8080 2026-10-15 07:44:11 -04:00
ROADMAP.md examples/shelf: a multi-file JSON API with a differential oracle 2026-10-14 14:31:18 -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
test_debug.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_input.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_null.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_simple.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_strip_tags.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_strip_tags2.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_strip_tags3.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes2.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes3.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes4.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes5.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes6.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes7.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_stripslashes8.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
test_unbalanced.php session 19 (escape_url_parity): recovered from its capture (recovered-partial) 2026-10-19 03:56:36 -04:00
tmp_file_write.php session 8 (task-03b-file-write): the model's work, verified clean 2026-10-18 16:05:59 -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.
  • 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).

Writing an app for pic

The dialect list above is mechanical. These two bite silently, and both were found by generating real applications and running the same source under php -S and pic:

  • State has to be re-read per request. require_once and any top-level code run once per process, so an object built at the top of the entry script keeps serving its first read to every later request. An app that caches state at top level and reads its file inside the handler is correct under php -S and serves stale data under pic - measured with a generated blog API, where posts created after the first request never appeared and every new post was handed id 1.
  • The default listen address is :8080, which is also a common default for a local model server. Pass --listen 127.0.0.1:PORT to move it, or the server exits with "address already in use" before answering anything.

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),
                   shelf (multi-file JSON API + the php/pic differential check)

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.