- Go 90.9%
- PHP 7.9%
- Shell 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| bench | ||
| cmd/pic | ||
| compat | ||
| drivers | ||
| examples | ||
| internal | ||
| runtime | ||
| tasks/cases | ||
| .gitignore | ||
| escape_test.php | ||
| FINAL_SUMMARY.md | ||
| go.mod | ||
| go.sum | ||
| IMPLEMENTATION_SUMMARY.md | ||
| Makefile | ||
| php_out.txt | ||
| pic_out.txt | ||
| README.md | ||
| ROADMAP.md | ||
| root_embed.go | ||
| test_debug.php | ||
| test_input.php | ||
| test_null.php | ||
| test_simple.php | ||
| test_strip_tags.php | ||
| test_strip_tags2.php | ||
| test_strip_tags3.php | ||
| test_stripslashes.php | ||
| test_stripslashes2.php | ||
| test_stripslashes3.php | ||
| test_stripslashes4.php | ||
| test_stripslashes5.php | ||
| test_stripslashes6.php | ||
| test_stripslashes7.php | ||
| test_stripslashes8.php | ||
| test_unbalanced.php | ||
| tmp_file_write.php | ||
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.- 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).
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_onceand 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 underphp -Sand 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:PORTto 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.