Skip to content
Skip to article

PHP, on a Whim #0: A Language for PHP’s “What Ifs”

A few of you may have noticed that I recently published Whim, a programming language I have been working on for the past few months.

Before you rush off to find the download link, stop for a second.

Well, the download link is right here. You can open it. Just read this first.

Whim is an experimental playground for me. It is public, so you can play with it too, but public does not mean supported, stable, secure, sensible, or ready for whatever important thing you were about to throw at it.

It is not a replacement for PHP. It is not another PHP implementation. It is not trying to be compatible with PHP.

It is a place where I can try ideas I have wished for while writing PHP, ideas that currently live in PHPDoc, ideas from rejected or unfinished RFCs, ideas stolen from other languages, and ideas that might turn out to be complete shit once someone actually has to use them.

Most importantly, Whim is not something you should use to shame PHP internals.

We will get to that.

It started with a book collecting dust#

About two years ago, I bought a book that had been recommended to me: Crafting Interpreters.

I skimmed through it when it arrived, told myself I would properly read it later, and then left it sitting there collecting dust.

That lasted until January 2026. I had nothing better to do, so I finally picked it up and started reading.

I followed the book and implemented the Lox programming language. The book builds one interpreter in Java and another in C. I used Rust because it is the low-level language I am most comfortable with, and following the C implementation while writing Rust was easy enough.

I called my version Nox.

The basic implementation took me about two days. The book intentionally leaves a bunch of features as exercises for the reader, so I started implementing those too.

Then I kept going.

I added things, removed things, rewrote parts of the runtime, changed the syntax, optimized bits of the VM, and generally played around with it until it barely looked like Lox anymore.

A couple of months later, Nox looked suspiciously like Rust. You could take a small Rust snippet like this:

fn add(a: u32, b: u32) -> u32 {
  a + b
}

fn fib(n: u32) -> u32 {
    if n < 2 {
        return n;
    }

    add(fib(n - 1), fib(n - 2))
}

and compile it with Nox.

That code was valid Rust, but it was also valid Nox.

At first, that felt cool. Then I had to ask myself what I was actually doing.

The answer was basically "Rust, but with my own VM."

That was not a very interesting question.

Rust was not the language I wanted to argue with#

I like Rust. I use it a lot. But I cannot remember many times when I was writing Rust and thought, "damn, if only this language had X."

PHP is different.

I use PHP all the time, and I cannot count how many times I have wanted it to have a richer type, a different construct, a stricter rule, or something that already exists in static analyzers but not in the language itself.

I also work on Mago, a static analyzer, formatter, and linter for PHP. If you are reading this blog, there is a decent chance you already know what Mago is.

Working on Mago means spending an unhealthy amount of time staring at the type system PHP developers already use through PHPDoc: generics, shaped arrays, literal types, integer ranges, refinements, aliases, and a lot more.

I wanted to know what those things would feel like if they were actually part of the language.

Not just syntax that a static analyzer understands. Real language features, enforced by the runtime, used by the standard library, visible through reflection, and annoying me when I made a bad design decision.

So I changed the question.

Instead of making a Rust-shaped language and bolting random dynamic features onto it, I could make Nox look and behave more like PHP. Then I could use it to explore PHP's "what ifs."

What if PHP had a much richer type system?

What if pattern matching could destructure objects, tuples, shapes, types, and ranges?

What if functions were first-class symbols?

What if arrays had been split into vectors, dictionaries, and tuples?

What if some of PHP's most dynamic features simply did not exist?

What if I could implement a rejected RFC, use it in a real program, and find out whether it was actually a good idea?

That sounded much more interesting.

From Nox to Elissa#

The first step was replacing Nox's frontend with something PHP-shaped.

I already had Mago's PHP parser, so I copied it over and started hacking it apart.

Nox already had structures that were basically classes. It had simple unit enums, no traits, and implementation blocks that lived directly under the structure or enum declaration. Mapping that basic object model onto PHP-like syntax was not too difficult.

Adding $ to variables actually made parts of the language easier.

In Nox, a bare name such as foo could mean a local, a function, another symbol, or a constant, depending on where it appeared. Once locals became $foo, a lot of that ambiguity disappeared. $foo is a variable. new Foo is trying to construct a class. Other names can be resolved according to the syntax around them.

The hard part was not making Whim look like PHP.

The hard part was making it behave like something from the PHP family.

Nox had a normal module system. Writing something like mod foo meant "go find foo.nox or foo/mod.nox, resolve it, and include it in the program."

That model had to go.

The PHP model is much stranger, but it is also one of the things that makes PHP feel like PHP. A require happens while the program is running. It can sit inside a condition. The file being loaded can depend on runtime information. An autoloader can get called only after resolving a class-like symbol fails.

My first version of require!("foo.els") only worked with literal strings known ahead of time. Turning it into a real runtime operation took much more work.

The compiler could no longer assume that it had seen the whole program before execution. The module system disappeared. Namespaces stopped being modules and became prefixes attached to names. The VM, symbol resolver, file loader, and autoloader all had to cooperate.

At that point, this was no longer a syntax swap.

By May 2026, the language had become something else, so I renamed it Elissa, keeping with the Carthage Software theme.

After that, development was extremely scientific and carefully planned.

It mostly went like this:

Let me add this.

Then:

Oh, this is nice. Let me add that too.

Or:

I proposed something like this for PHP three years ago and it was rejected. Fuck it, let me add it here.

There was no grand language design document and barely any roadmap. Some ideas came from PHPDoc. Some came from rejected PHP RFCs. Some came from RFCs that are still drafts or active discussions. Some came from other languages. Some appeared because I needed them for a program.

Others appeared because I had far too much free time.

In August 2026, while cleaning up the project so I could open-source it, I decided to rename it again.

Elissa sounded too serious.

Elissa sounded like I had a plan, a roadmap, and perhaps a small committee somewhere.

I did not.

I wanted a name that made the nature of the project obvious. This language exists so I can try something on a whim, use it for a while, and discover whether it was clever, useless, horrible, or all three.

So Elissa became Whim.

My local directory is still called elissa, because of course it is.

So, is Whim a PHP dialect?#

Maybe, if you stretch the word "dialect" until it starts screaming.

You could call it PHP-inspired. You could call it a very distant relative. Just do not hear any of those descriptions and assume compatibility.

If Hack is PHP's cousin, Whim is the fifth cousin removed from your dad's side who you did not know existed, but who somehow keeps stalking you.

Whim looks familiar from a distance. It has $variables, classes, interfaces, namespaces, closures, attributes, exceptions, and plenty of standard-library concepts that PHP developers will recognize.

Then you get closer and realize that it behaves very differently.

Most Whim code will not run as PHP. Most PHP code will not run as Whim. Whim has no variable variables, no eval, no dynamic property access, and no dynamic method access. Names are case-sensitive. There is no single PHP array type pretending to be a list, map, tuple, ordered dictionary, queue, and whatever else you asked it to be that day.

Whim also merges symbol tables that PHP keeps separate.

In PHP, you can have a function and a class with the same name. In Whim, you cannot. A name refers to one symbol, and that symbol might be a function, class, constant, type alias, or something else.

That model is useful in Whim, especially once the type system starts referring to symbols directly.

It would also break the world if PHP adopted it.

That distinction is going to come up a lot in this series.

A feature working in Whim does not prove that it can be added to PHP. It does not prove that PHP should add it. It does not even prove that it was a good idea in Whim.

It only means I implemented it, so now we have something real to poke at instead of arguing about a hypothetical forever.

It got a little out of hand#

Whim started as an interpreter from a book, but it is not a tiny AST walker anymore.

Whim source code is compiled into bytecode. That bytecode goes through an optimizer, then the VM executes it.

Most of the standard library is written in Whim itself. The Whim binary embeds both the standard library's source code and its compiled bytecode.

There is a formatter and an AST-based linter. Both are smaller versions of mago fmt and mago lint.

There is also a small language server. It provides syntax highlighting, formatting, parser and linter diagnostics, folding ranges, and selection ranges.

That is roughly where the magic stops.

There is no project-wide indexing, no go-to-definition, no find-references, no debugger, no profiler, no performance tooling, and no static analyzer. Whim has a ridiculous type system, but it is currently enforced at runtime.

There is a Tree-sitter grammar and a Zed extension. The Zed extension loads the grammar and starts the language server. It is useful, but it is not secretly a full IDE.

The standard library, on the other hand, is absurdly large for a language I keep calling a toy.

It covers async programming, channels, compression, filesystems, processes, networking, TCP, UDP, TLS, HTTP, SMTP, regular expressions, Unicode, JSON, BSON, CSV, dates, time, random number generation, password handling, reflection, databases, and a lot of other stuff I probably did not need to add yet.

Most of it started as a fairly direct rewrite of PSL into Whim.

It stopped being direct pretty quickly.

Whim has generics, sealed types, richer collection types, and a much larger runtime type system. That let me make APIs stricter and express guarantees that PSL can only describe through PHPDoc, or cannot describe at all in PHP.

Other parts of the standard library are less like PSL ports and more like evolved versions of PHP extensions.

Whim\Database, for example, was written from scratch. It supports PostgreSQL and SQLite, uses non-blocking operations, has connection pooling and transactions, and is basically my attempt at making something nicer to use than PDO.

Whim\Reflection was also designed from scratch around Whim's actual language model. It understands generics, type aliases, newtypes, literal types, integer ranges, shaped collections, unions, intersections, callable captures, and the rest of the type system.

It also cannot be used to reach through visibility and mess with private or protected state.

In Whim, private actually means private. Wild concept.

Both of those deserve their own posts, so I will stop before this introduction turns into documentation for the entire standard library.

But can I actually use the damn thing?#

At some point, compiler examples stopped being enough.

You can make almost any feature look great in a ten-line snippet written specifically to make that feature look great. The real test is whether you still like it after using it hundreds of times across a standard library, several packages, and an application you have to maintain.

So I started building things with Whim.

I created a small collection of packages under the Trifle organization, including trifle/clock, trifle/markdown, and trifle/seal.

I made these packages for three reasons.

First, I needed to test Whim's built-in Git-based package manager. That thing deserves its own post.

Second, I wanted to find out whether I could actually use Whim for normal work, rather than only writing cute examples for language features.

Third, I needed somewhere to put useful code that did not belong in the standard library.

Whim's package manager installs dependencies directly from Git repositories. A project has a manifest, whim install fetches the dependencies using Git, and Whim generates a lockfile.

There is no central registry.

I do not want to operate a registry, moderate package names, manage accounts, recover passwords, fight spam, or wake up one morning responsible for the Whim equivalent of npm. Git repositories are enough.

The Trifle packages are not evidence of a thriving ecosystem. There is no ecosystem. It is mostly me testing whether all of this works when code lives outside the main repository.

The most useful test so far has been play.whim.sh.

From the outside, the playground looks simple. You paste some Whim code, then run, format, or lint it.

The backend is more involved.

The playground is written entirely in Whim. It handles requests concurrently, uses Whim's async features and coroutines, talks to a database, runs an HTTP server, and coordinates the sandboxed execution of user-submitted code.

Docker provides the actual sandbox around that code. nginx sits in front of Whim's HTTP server. The whole thing runs on an isolated $5 Hetzner VPS.

It is isolated from my other projects because I am not completely insane.

Building the playground put pressure on parts of Whim that small examples never touched. It exposed performance problems, exercised the async runtime, forced several standard-library APIs to work together, and showed me what maintaining a real Whim application feels like.

There is also a second small application written in Whim that I use and maintain for myself.

So yes, there are technically two "production" applications running Whim.

No, that does not mean Whim is production-ready.

Should you use Whim in production?#

No.

Seriously, no.

Do it only if both of these are true:

  1. You have a lot of free time.
  2. You do not care very much if the machine running it gets compromised.

Whim has not had a professional security audit.

I have checked for obvious problems. I try not to write obvious vulnerabilities. I looked at the HTTP server and I hope there is no hidden RCE sitting in there.

That is not a security audit.

I am not a security engineer. I do not have a blue team on my payroll. I genuinely do not know what security problems might exist in the compiler, VM, HTTP server, package manager, standard library, or any of the other parts I wrote while experimenting.

When I call Whim experimental, I do not only mean that an API might get renamed next week. I also mean you should not assume it is safe to expose to hostile input or trust with data that matters.

The playground is public because submitted code runs inside Docker sandboxes on a VPS isolated from anything I care about. I still try to be careful, but that is very different from trusting Whim with important systems or private data.

And no, releasing Whim does not mean I am going to start using it instead of PHP for my own serious projects.

The website where you are reading this post is written in PHP, and it is staying that way. The same application has a back office where I manage invoices, clients, contacts, business expenses, and other information that actually matters to me.

I do not trust Whim with that.

Whim can be capable enough to build interesting software while still being something I would never trust with my business records.

Those two things are not contradictory.

Do not expect support#

Whim is open source, but it is not a community-maintained language.

Pull requests are completely disabled.

You can fork Whim under its licenses, change it, build your own version, or take it in a completely different direction. Just do not send patches back. I do not have the time or the desire to review external changes.

Issues are open. You can report a bug or suggest an idea.

Opening an issue does not create a promise that I will reply, fix it, prioritize it, or even look at it soon. An issue might sit there for months. It might get closed without a change. It might never become a priority.

There is no support plan, no fixed roadmap, and no guaranteed response time.

There is also no serious backward-compatibility promise. I might change syntax, semantics, bytecode, standard-library APIs, or the package format because I found a design I like more.

Open source means you can inspect it and fork it. It does not mean I owe the project a governance model, release schedule, compatibility policy, or customer support department.

You are welcome to try Whim, write code with it, make packages, inspect the implementation, and tell me what weird things you find.

Just do not build expectations around it.

Please do not use Whim to shame PHP#

This is the bit I care about most.

I do not want someone to look at the Whim repository, see me add three features in a day and publish ten releases in a week, then write something like:

HI PHP INTERNALS! LOOK! A LANGUAGE JUST LIKE PHP CAN DO THIS! WHY DO WE HAVE TO WAIT YEARS FOR ONE FEATURE?

Shut up. Do not say that.

Whim and PHP operate under completely different conditions.

I can wake up tomorrow, change Whim's syntax, remove a feature, merge two symbol categories, rename half the standard library, and release it before lunch.

The number of people with a valid claim that I broke their production system is basically zero.

PHP cannot work like that.

PHP has decades of existing code, an enormous package ecosystem, widely deployed applications, extensions that depend on engine behavior, operating-system distributions that package it, static analyzers and IDEs that model it, frameworks built around its quirks, and people who reasonably expect old code to keep running.

A change that looks tiny in Whim can break thousands of projects in PHP.

PHP also has a process. People discuss changes, argue about semantics, build implementations, review them, vote on them, document them, and then maintain them for years.

That process can be slow. It can be exhausting. Sometimes it is deeply annoying. I have proposed RFCs, so I know.

It is still there for a reason.

A rejected RFC does not mean PHP internals made the wrong call. Implementing the same idea in Whim does not magically prove that the RFC should have passed.

It might give us new evidence. It might uncover a better design. It might show that one of the original concerns was not actually a problem.

It might also show that the idea was bad.

What it does not do is erase PHP's compatibility constraints, implementation costs, maintenance burden, or need for consensus.

Whim is not trying to replace PHP.

Whim is not trying to become compatible with PHP.

Not every feature in Whim belongs in PHP. A lot of them could never be added to PHP without unacceptable backward-compatibility breaks.

Even when a feature could theoretically make it into PHP, implementing it in Whim is not proof that it can be added to PHP safely, efficiently, compatibly, and in a form that people are willing to maintain for the next twenty years.

A one-person experimental project is a dictatorship.

PHP is not.

What Whim can actually do for PHP#

None of that makes Whim useless to PHP's future.

I think it can be genuinely useful.

Whim gives us somewhere to be wrong cheaply.

Instead of arguing about a feature entirely in the abstract, I can implement it. I can use it throughout a large standard library. I can build packages and applications with it. I can see how it interacts with reflection, generics, runtime types, async execution, error handling, and everything else.

Then I can find out whether I still like it after living with it.

For example, Whim has final local variables:

final $message = 'hello';

The feature works.

It is also almost completely useless.

It adds another keyword to variable declarations, requires Whim to track whether a local can be assigned again, and I do not think I used it once while writing the playground.

That is useful information.

A feature can be coherent. It can be easy to explain. It can work exactly as designed. It can still fail to justify its own existence.

Whim also treats return, break, and continue as expressions rather than only statements.

I like that feature. It makes some code nicer. It also required changes throughout the compiler and VM that are not obvious from looking at the syntax.

Something similar might be possible in PHP, but the fact that the syntax looks simple tells you almost nothing about the implementation work or compatibility concerns.

Then there is Whim's merged symbol table.

I like the model. It makes parts of Whim's type system cleaner. It also has basically no path into PHP because PHP code already depends on functions, classes, and constants living in separate namespaces.

So we already have three different outcomes:

  • a feature that works but turned out to be mostly useless;
  • a feature that works well but required much deeper changes than the syntax suggests;
  • a feature that works in Whim but would break far too much existing PHP code.

That is the point of the experiment.

Through this series, I want to go beyond the surface-level reaction of "that looks neat, I want it."

For each feature, I want to explain what it is, why I added it, how it works, what was difficult, what its limitations are, how it interacts with the rest of the language, whether PHP could support something similar, and why PHP might still decide not to.

Sometimes a Whim feature might inspire a future PHP RFC.

Sometimes implementing it might demonstrate exactly why PHP should never have it.

Both outcomes are useful.

What comes next#

Future posts will cover pattern matching, generics, literal and range types, sealed types, aliases and newtypes, RAII, vectors, dictionaries, tuples, the package manager, async programming, the standard library, database and reflection APIs, and Whim's broader type system.

There will also be a post about something Whim deliberately does not have: traits.

That one will probably be called "The Missing Trait."

You can try Whim in the online playground, install it locally, browse the Trifle packages, or dig through the package repositories directly:

The next post should arrive next week.

Or in two weeks.

Or whenever I feel like writing it.

Like Whim itself, this blog series is not a serious project.

Comments

Have a question or something to add? Sign in with GitHub to join the discussion.

View discussions on GitHub ↗