Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ test/unit:
.PHONY: test/mutation
test/mutation:
php vendor/bin/infection --show-mutations=max --threads=max --min-covered-msi=93

.PHONY: playground
playground:
playground/bin/run
2 changes: 1 addition & 1 deletion bin/xphp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/local/bin/php
#!/usr/bin/env php
<?php

declare(strict_types=1);
Expand Down
3 changes: 3 additions & 0 deletions playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/composer.lock
/vendor/
/var/
53 changes: 53 additions & 0 deletions playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# xphp playground

Self-contained sandbox for poking at xphp features. Has its own `composer.json`
that depends on `xphp-lang/xphp-parser` via a Composer path repository pointing
one level up — the same shape any downstream consumer would use.

## Usage

```bash
playground/bin/run
```

The first invocation runs `composer install` (which symlinks the parent
project into `playground/vendor/` via the path repo). After that it compiles
every `.xphp` under `src/`, then runs all the demos in turn.

The Makefile at the repo root has a `playground` target wrapping the same
command:

```bash
make playground
```

## What's in here

```
playground/
├── composer.json # own package, path-repo'd at ../
├── bin/run # compile + run all demos
├── src/
│ ├── Models/ # plain final classes used as type args
│ ├── Containers/ # the generic templates
│ │ ├── Box.xphp # Box<T>
│ │ ├── Pair.xphp # Pair<K, V>
│ │ ├── Map.xphp # Map<K, V>
│ │ ├── Collection.xphp # Collection<T> — uses T[] + ?T sugar
│ │ └── Wrapper.xphp # Wrapper<T> { Box<T> $box; } — transitive
│ └── Demos/ # top-level scripts that exercise each feature bucket
└── var/
├── cache/ # generated specialized classes (gitignored)
└── dist/ # rewritten .xphp → .php (gitignored)
```

Each demo reflects on the compiled class to prove that the generic parameter
became a real native type — `ReflectionProperty::getType()->getName()` returns
the concrete class, not `mixed` or `T`. The reflection output is the load-bearing
assertion: it's what DI containers and serializers actually look at.

## Iterating

Edit any `.xphp` file under `src/`, then re-run `playground/bin/run`. The
compile step is idempotent and only rewrites changed paths' output. To start
fresh, `rm -rf playground/var/` and re-run.
1 change: 1 addition & 0 deletions playground/_site_src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_config.yml
65 changes: 65 additions & 0 deletions playground/_site_src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: "xphp playground — unknown"
layout: default
---

# xphp playground

Browse the `.xphp` sources and the compiled artifacts from a recent run.

| | |
|---|---|
| Branch | `unknown` |
| Commit | [`unknown`](https://github.com/xphp-lang/xphp-parser) |
| Built | `2026-05-21T01:52:58Z` |
| Source | [https://github.com/xphp-lang/xphp-parser](https://github.com/xphp-lang/xphp-parser) |

## Source `.xphp` files (12)
{: id="source"}

Authored input that the xphp compiler consumes. Generic templates live under `Containers/`; the demo scripts under `Demos/` instantiate them.

- [`Containers/Box.xphp`](src/Containers/Box.xphp.md)
- [`Containers/Collection.xphp`](src/Containers/Collection.xphp.md)
- [`Containers/Map.xphp`](src/Containers/Map.xphp.md)
- [`Containers/Pair.xphp`](src/Containers/Pair.xphp.md)
- [`Containers/Wrapper.xphp`](src/Containers/Wrapper.xphp.md)
- [`Demos/ArraySugar.xphp`](src/Demos/ArraySugar.xphp.md)
- [`Demos/MultiType.xphp`](src/Demos/MultiType.xphp.md)
- [`Demos/NestedTransitive.xphp`](src/Demos/NestedTransitive.xphp.md)
- [`Demos/SingleType.xphp`](src/Demos/SingleType.xphp.md)
- [`Models/Food.xphp`](src/Models/Food.xphp.md)
- [`Models/Plastic.xphp`](src/Models/Plastic.xphp.md)
- [`Models/User.xphp`](src/Models/User.xphp.md)

## Rewritten user code (`var/dist/`) (12)
{: id="dist"}

Each `.xphp` source rewritten to native PHP — generic call sites get pointed at the matching specialized class.

- [`Containers/Box.php`](var/dist/Containers/Box.php.md)
- [`Containers/Collection.php`](var/dist/Containers/Collection.php.md)
- [`Containers/Map.php`](var/dist/Containers/Map.php.md)
- [`Containers/Pair.php`](var/dist/Containers/Pair.php.md)
- [`Containers/Wrapper.php`](var/dist/Containers/Wrapper.php.md)
- [`Demos/ArraySugar.php`](var/dist/Demos/ArraySugar.php.md)
- [`Demos/MultiType.php`](var/dist/Demos/MultiType.php.md)
- [`Demos/NestedTransitive.php`](var/dist/Demos/NestedTransitive.php.md)
- [`Demos/SingleType.php`](var/dist/Demos/SingleType.php.md)
- [`Models/Food.php`](var/dist/Models/Food.php.md)
- [`Models/Plastic.php`](var/dist/Models/Plastic.php.md)
- [`Models/User.php`](var/dist/Models/User.php.md)

## Specialized classes (`var/cache/`) (7)
{: id="cache"}

Monomorphized class per unique generic instantiation. Loaded via the `XPHP\Generated\` PSR-4 prefix. Class shortnames are SHA-256-derived hashes of the canonical argument list.

- [`Generated/App/Containers/Box/T_3036c16466fa04e2ed024d6b75086b6a023199482af4f6a278a33d8bd9ebe44f.php`](var/cache/Generated/App/Containers/Box/T_3036c16466fa04e2ed024d6b75086b6a023199482af4f6a278a33d8bd9ebe44f.php.md)
- [`Generated/App/Containers/Box/T_de1e0eaabedbafa176d971782f59025438ff880765b54fb07b0698700f21a0cd.php`](var/cache/Generated/App/Containers/Box/T_de1e0eaabedbafa176d971782f59025438ff880765b54fb07b0698700f21a0cd.php.md)
- [`Generated/App/Containers/Collection/T_d59a159d1113876f898c70f94a3971da6bb3165b1a8f0c07236c92d911df85ed.php`](var/cache/Generated/App/Containers/Collection/T_d59a159d1113876f898c70f94a3971da6bb3165b1a8f0c07236c92d911df85ed.php.md)
- [`Generated/App/Containers/Map/T_ddc02d7f9d24d965819939d5b07227efa95da520d87d6728a4c41906ecce5bca.php`](var/cache/Generated/App/Containers/Map/T_ddc02d7f9d24d965819939d5b07227efa95da520d87d6728a4c41906ecce5bca.php.md)
- [`Generated/App/Containers/Pair/T_307894382bcf10563dfe258ab0eef6cf6d925ef67d412e2b8743703540f18bdd.php`](var/cache/Generated/App/Containers/Pair/T_307894382bcf10563dfe258ab0eef6cf6d925ef67d412e2b8743703540f18bdd.php.md)
- [`Generated/App/Containers/Pair/T_3ca1518773f5e15bb180bc027c8394e76250619a79189c7cb33652fd9815b2ed.php`](var/cache/Generated/App/Containers/Pair/T_3ca1518773f5e15bb180bc027c8394e76250619a79189c7cb33652fd9815b2ed.php.md)
- [`Generated/App/Containers/Wrapper/T_de1e0eaabedbafa176d971782f59025438ff880765b54fb07b0698700f21a0cd.php`](var/cache/Generated/App/Containers/Wrapper/T_de1e0eaabedbafa176d971782f59025438ff880765b54fb07b0698700f21a0cd.php.md)

29 changes: 29 additions & 0 deletions playground/_site_src/src/Containers/Box.xphp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: "src/Containers/Box.xphp"
layout: default
---

[← index](../../index.md) · [Source `.xphp` files](../../index.md#source)

# `src/Containers/Box.xphp`

```php
<?php

declare(strict_types=1);

namespace App\Containers;

class Box<T>
{
public function __construct(public T $item)
{
}

public function get(): T
{
return $this->item;
}
}

```
42 changes: 42 additions & 0 deletions playground/_site_src/src/Containers/Collection.xphp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: "src/Containers/Collection.xphp"
layout: default
---

[← index](../../index.md) · [Source `.xphp` files](../../index.md#source)

# `src/Containers/Collection.xphp`

```php
<?php

declare(strict_types=1);

namespace App\Containers;

class Collection<T>
{
private T[] $items;

public function __construct(T ...$items)
{
$this->items = $items;
}

public function first(): ?T
{
return $this->items[0] ?? null;
}

public function all(): T[]
{
return $this->items;
}

public function count(): int
{
return count($this->items);
}
}

```
41 changes: 41 additions & 0 deletions playground/_site_src/src/Containers/Map.xphp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: "src/Containers/Map.xphp"
layout: default
---

[← index](../../index.md) · [Source `.xphp` files](../../index.md#source)

# `src/Containers/Map.xphp`

```php
<?php

declare(strict_types=1);

namespace App\Containers;

class Map<K, V>
{
/** @var array<int, K> */
private array $keys = [];
/** @var array<int, V> */
private array $values = [];

public function set(K $key, V $value): void
{
$this->keys[] = $key;
$this->values[] = $value;
}

public function firstKey(): ?K
{
return $this->keys[0] ?? null;
}

public function firstValue(): ?V
{
return $this->values[0] ?? null;
}
}

```
26 changes: 26 additions & 0 deletions playground/_site_src/src/Containers/Pair.xphp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: "src/Containers/Pair.xphp"
layout: default
---

[← index](../../index.md) · [Source `.xphp` files](../../index.md#source)

# `src/Containers/Pair.xphp`

```php
<?php

declare(strict_types=1);

namespace App\Containers;

class Pair<K, V>
{
public function __construct(
public K $first,
public V $second,
) {
}
}

```
31 changes: 31 additions & 0 deletions playground/_site_src/src/Containers/Wrapper.xphp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: "src/Containers/Wrapper.xphp"
layout: default
---

[← index](../../index.md) · [Source `.xphp` files](../../index.md#source)

# `src/Containers/Wrapper.xphp`

```php
<?php

declare(strict_types=1);

namespace App\Containers;

use App\Containers\Box;

class Wrapper<T>
{
public function __construct(public Box<T> $box)
{
}

public function unwrap(): T
{
return $this->box->get();
}
}

```
45 changes: 45 additions & 0 deletions playground/_site_src/src/Demos/ArraySugar.xphp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: "src/Demos/ArraySugar.xphp"
layout: default
---

[← index](../../index.md) · [Source `.xphp` files](../../index.md#source)

# `src/Demos/ArraySugar.xphp`

```php
<?php

declare(strict_types=1);

namespace App\Demos;

use App\Containers\Collection;
use App\Models\User;
use ReflectionMethod;

echo '[ArraySugar] Collection<User>: T[] property + ?T return type', PHP_EOL;

$users = new Collection<User>(new User('alice'), new User('bob'), new User('carol'));

echo ' collection->count() = ', $users->count(), PHP_EOL;
echo ' collection->first()->name = ', $users->first()?->name ?? '<null>', PHP_EOL;

$first = (new ReflectionMethod($users::class, 'first'))->getReturnType();
echo ' reflected first(): type = ', $first instanceof \ReflectionNamedType ? $first->getName() : '<not-named>', PHP_EOL;
echo ' reflected first(): nullable = ', $first instanceof \ReflectionNamedType && $first->allowsNull() ? 'yes' : 'no', PHP_EOL;

$empty = new Collection<User>();
echo ' empty collection first = ', $empty->first() === null ? 'null (good)' : 'unexpected', PHP_EOL;

// `T ...$items` is what actually enforces element type at runtime.
try {
$bad = new Collection<User>(new User('alice'), 'not-a-user');
echo ' WRONG_TYPE: variadic accepted bad arg', PHP_EOL;
} catch (\TypeError $e) {
echo ' variadic TypeError fires for non-User arg (good)', PHP_EOL;
}

echo PHP_EOL;

```
Loading
Loading