- Go 97.2%
- PHP 2.2%
- Shell 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| bench | ||
| cmd/pic | ||
| compat | ||
| drivers | ||
| examples | ||
| internal | ||
| runtime | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
| ROADMAP.md | ||
| root_embed.go | ||
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
- One OS process for many requests. No FPM spawn, no per-request interpreter start.
- PHP compiles to Go, then to a native binary. Request handling is a function call.
- Each request runs on a goroutine. Concurrent requests share the process.
staticvariables, class statics,$GLOBALS,$_SESSION, andpic_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,finallywithreturn - Functions, defaults,
fn()arrow functions, closures withuse - Classes: properties, methods,
staticmethods/properties,new,$this,::,extendsandparent::,interface/implements - Magic methods:
__construct,__destruct,__get,__set,__isset,__call,__toString - Modules:
namespace(unbraced, braced, andnamespace { }),use/use function/use const(including group use),namespace\Name,__NAMESPACE__,ClassName::class, composer PSR-4 autoload at compile time (novendor/autoload.phpneeded) - Arrays:
[...],array(),=>,$a[] =, nested assign - Operators: arithmetic, concat, compare (
=====), logic,??,?:,++--, assign ops echo,print,isset,empty,unset,include/require(_onceis 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(includingphp://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_getinfoand friends with the realCURLOPT_*numbers - Uploads:
$_FILESis filled from multipart posts;move_uploaded_file/is_uploaded_filework like PHP
Dialect: breaking changes
- Names are case-sensitive (
Fooandfooare 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/continuecannot cross atry/catch/finallyboundary (the try body compiles to its own function).- No references (
&$x). Arrays and objects are pointers, so in-place mutate still works. includepaths must be string literals, resolved at compile time.- All functions and classes in the app compile in. You do not need to
includea file only to define a function. require_once/include_oncerun once per process, not once per request.- No
eval, variable-variables,goto, traits,abstract/final, enums, attributes, generators, orinterface 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" + 1does not become11. json_decodereturns arrays (objects in JSON become associative arrays).preg_*uses Go regexp, not PCRE.foreachwalks 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/protectedmembers are enforced: outside access throwsError, subclasses may use protected members, and__get/__set/__issetstill 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_TTLseconds (default 1440, PHP'sgc_maxlifetime);session_gc()sweeps immediately. - Interfaces are a compile-time contract only:
implementsis 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
PDOconnection 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 withmove_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.
picneeds no source checkout: the runtime and PDO drivers are embedded in the binary and extracted to the user cache on demand (PIC_ROOTstill wins when set).- Sessions are file-backed (
PIC_SESSION_DIRoverrides the per-user cache): a restart no longer drops logins. pic serve --watchrestarts 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.