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
37 changes: 25 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,27 @@ needs a reason, not a refactor. Each is explained in full in the README.
analyses itself with its own `extension.neon` for the same reason: a rule that crashes or a
generator that drifts fails here rather than in somebody's project.

13. **`Monomorphizer` is the only class that talks to z-engine.** Everything else describes
what should happen in this package's own vocabulary (`SubstitutionRequest`, `SlotAddress`);
`Monomorphizer` translates that into z-engine's value objects at the single point where the
engine is asked. Keeping the dependency in one place is what makes it possible to say
exactly when engine state is touched. Two corollaries: z-engine is consumed through its
documented API only — never `Core::$executor`/`Core::$compiler`, never a method marked
`@internal` — and nothing here re-derives z-engine's own environment rules. The one
sanctioned exception outside `Monomorphizer` is `EngineCapabilities`, whose
`class_exists()` on a public z-engine class name is deliberate feature detection.
13. **`Monomorphizer` is the only class that talks to z-engine for *specialization*.**
Everything else describes what should happen in this package's own vocabulary
(`SubstitutionRequest`, `SlotAddress`); `Monomorphizer` translates that into z-engine's
value objects at the single point where the engine is asked. Keeping the dependency in one
place is what makes it possible to say exactly when engine state is touched. Two
corollaries: z-engine is consumed through its documented API only — never
`Core::$executor`/`Core::$compiler`, never a method marked `@internal` — and nothing here
re-derives z-engine's own environment rules. The one sanctioned exception for feature
detection is `EngineCapabilities`, whose `class_exists()` on a public z-engine class name
is deliberate.

**Amended (maintainer-directed):** `Lisachenko\Generics\Native\` is the *second* sanctioned
touchpoint, and the only one that is not about specialization at all — a native vector's
hot path is memory access, which has nothing to translate into `SubstitutionRequest` terms.
It may use `ZEngine\Type\StringEntry` (including `getRawValue()`, which z-engine's own
AGENTS discourages for dependents: the maintainer sanctioned consuming the existing API
here rather than adding a class to z-engine) and `ZEngine\Core::cast`, and nothing else.
Both stay confined to `NativeVector`'s private low-level methods — `acquire()` is the only
place in the package outside `Monomorphizer` where FFI is reached for — so the same
property still holds: you can name every line that touches the engine. Adding a third
touchpoint is a design decision, not a refactor.

14. **Nothing in this package boots the engine.** Z-Engine initializes itself from its Composer
bootstrap, including during the `opcache.preload` stage, so `require vendor/autoload.php`
Expand Down Expand Up @@ -184,7 +196,7 @@ hand-written message at the call site. Add a factory rather than an inline
## 9. Conventional commits

See [conventionalcommits.org](https://www.conventionalcommits.org/). Common scopes here: `runtime`,
`template`, `type`, `naming`, `phpstan`, `bench`, `ci`, `docs`.
`template`, `type`, `naming`, `native`, `phpstan`, `bench`, `ci`, `docs`.

```
feat(runtime): reify generic templates through class specialization
Expand All @@ -197,6 +209,7 @@ test(template): cover promoted properties producing two slots

```
src/Attribute/ the template and slot attributes users write
src/Native/ NativeVector - the one shipped template, and the second engine touchpoint
src/Template/ parsing a template class into a TemplateDefinition
src/Type/ type-argument grammar, validation and resolution
src/Naming/ specialized class-name mangling and parsing
Expand All @@ -211,8 +224,8 @@ extension.neon wires the extension up, auto-loaded by phpstan/extension-insta
benchmarks/ the monomorphization cost harness
examples/ runnable, and covered by a test that runs them
preload.php opcache.preload entry point - templates yes, specializations never
docs/ design.md, limitations.md, long-running.md, static-analysis.md
and the generated benchmarks.md
docs/ design.md, limitations.md, long-running.md, static-analysis.md,
native-vectors.md and the generated benchmarks.md
tests/phpstan/generated/ committed generator output - regenerate, never edit
```

Expand Down
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ from a hand-written class, on a class that did not exist a microsecond ago.**
- [Identity: sibling, not subclass](#identity-sibling-not-subclass)
- [Static analysis](#static-analysis) and the [extension guide](docs/static-analysis.md)
- [Long-running processes](#long-running-processes) and the [deployment guide](docs/long-running.md)
- [Known limitations](#known-limitations), in full in [docs/limitations.md](docs/limitations.md)
- [Native data vectors](#native-data-vectors) and the [memory model](docs/native-vectors.md)
- [A runnable example](#a-runnable-example)
- [Known limitations](#known-limitations), in full in [docs/limitations.md](docs/limitations.md)
- [What it costs](#what-it-costs) and the full [benchmark report](docs/benchmarks.md)
- [Design notes](#design-notes) and the full [design document](docs/design.md)
- [Contributing](#contributing)
Expand Down Expand Up @@ -435,16 +436,47 @@ registration lives until the request (or worker) ends. Nothing survives shutdown
[`docs/long-running.md`](docs/long-running.md) has the per-request budget, the worker and FPM
recipes and a deployment checklist.

## Native data vectors

`array<T>` element types are the one thing this package cannot enforce at run time (see
[Known limitations](#known-limitations)). For scalars there is now a data structure that can:

```php
use Lisachenko\Generics\Native\NativeVector;

$samples = new (NativeVector::of('int'))($blobFromTheWire);

echo $samples[0]; // a zend_long read straight out of the block
$samples[1] = -20; // written straight back into it
$samples->append(50); // the block grows by eight bytes
$samples->append(1.5); // TypeError, from the engine

$bytes = $samples->toBinary(); // back to a PHP string, byte for byte
```

**The block of memory is a PHP string.** Element `i` lives at byte `i * 8` of that string's
`zend_string.val`, and every accessor reaches it as a `zend_long *` or a `double *` — there is
no encoding step, so `pack()`/`unpack()` appear nowhere on the path. The element type is
enforced because `get()`, `set()` and `append()` are declared with the type parameter, which is
a slot the engine really does check; the array syntax delegates to them rather than replacing
them.

[`docs/native-vectors.md`](docs/native-vectors.md) has the memory model, the copy-on-write
discipline that makes `toBinary()` safe to hand out, the static-analysis setup, and the roadmap
towards sized scalar kinds and C structures.

## A runnable example

```bash
php -d ffi.enable=1 -d opcache.jit=off examples/collection.php
php -d ffi.enable=1 -d opcache.jit=off examples/native-vector.php
```

[`examples/collection.php`](examples/collection.php) specializes a collection template, prints
`get_class()`, shows the engine's own `TypeError` rejecting the wrong element type, and shows the
template left exactly as it was. It is covered by a test that runs it, so it cannot quietly stop
working.
template left exactly as it was. [`examples/native-vector.php`](examples/native-vector.php) casts
a binary blob to a `NativeVector<int>`, indexes it, grows it and hands it back as a string. Both
are covered by tests that run them, so they cannot quietly stop working.

## Known limitations

Expand All @@ -457,7 +489,8 @@ anything else:
array type, so a slot declared `array` is checked for being an array and nothing more. The doc
tag still carries the element type and PHPStan still enforces it — but statically only. This is
the one place where less is checked at run time than it looks, and therefore the most likely
source of false confidence in the package.
source of false confidence in the package. For scalar elements,
[native data vectors](#native-data-vectors) are the way out.
- **A specialization is a sibling, not a subclass.** `$box instanceof Box` is `false` and cannot
be made true. Type-hint an interface or an abstract base; both are preserved.
- **Everything is request-scoped.** Class entries are request memory. Specialize at worker boot —
Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"test:analysis": "phpunit --testsuite analysis",
"stubs:generate": "php bin/generics-stubs --out=tests/phpstan/generated 'Lisachenko\\Generics\\Fixture\\Box' 'Lisachenko\\Generics\\Fixture\\AttributeBox'",
"stubs:check": "php bin/generics-stubs --check --out=tests/phpstan/generated 'Lisachenko\\Generics\\Fixture\\Box' 'Lisachenko\\Generics\\Fixture\\AttributeBox'",
"stubs:generate": "php bin/generics-stubs --out=tests/phpstan/generated 'Lisachenko\\Generics\\Fixture\\Box' 'Lisachenko\\Generics\\Fixture\\AttributeBox' 'Lisachenko\\Generics\\Native\\NativeVector'",
"stubs:check": "php bin/generics-stubs --check --out=tests/phpstan/generated 'Lisachenko\\Generics\\Fixture\\Box' 'Lisachenko\\Generics\\Fixture\\AttributeBox' 'Lisachenko\\Generics\\Native\\NativeVector'",
"test:preload": "phpunit --filter PreloadTest"
},
"scripts-descriptions": {
Expand All @@ -67,7 +67,7 @@
"cs:check": "Check coding standards without fixing",
"cs:fix": "Fix coding standards",
"test:analysis": "Run the PHPStan extension tests, which need no engine",
"stubs:generate": "Regenerate the committed PHPStan stubs for the placeholder-form fixtures",
"stubs:generate": "Regenerate the committed PHPStan stubs for the placeholder-form templates",
"stubs:check": "Fail if the committed PHPStan stubs no longer match the generator",
"test:preload": "Boot preload.php in a child process; must run, never silently skip"
},
Expand Down
28 changes: 28 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ for you and will not tell you it did not.
This is the most likely source of false confidence in the package, which is why it is stated this
bluntly.

**For scalars, there is now a way out.** A
[native data vector](native-vectors.md) puts the elements in a block of memory instead of an
array, which moves the element type from a slot the engine cannot check (`array`) to method slots
it can: `NativeVector<int>` really does reject `1.5`, with the engine's own `TypeError`. It
covers `int` and `float` today. It is not a general answer — an `array<User>` is still an
`array` — and it is a different data structure rather than a fix to this entry.

---

## Loud: rejected at specialization time
Expand Down Expand Up @@ -263,6 +270,27 @@ case, since their type-argument names are long by construction. Tracked upstream
Everything else — method dispatch, class-typed *parameters*, builtin-typed properties — measured
at parity with a hand-written class.

### A stub-described template does not narrow `of()`

Affects static analysis only; nothing about the run time changes. A placeholder-form template
declares its type parameter as a native type, which an analyser resolves to a class that does not
exist, so the package ships a **generated stub** describing the class the way it behaves. Once
that stub is in your PHPStan configuration, `NativeVector::of('int')` stays
`class-string<NativeVector<T>>` instead of narrowing to `class-string<NativeVector<int>>`.

**Why.** A stub cannot name this library's own interfaces — stub files are reflected before the
analysed paths are indexed — so the stubbed class is not a `GenericObject`, and
`TemplateOfReturnTypeExtension` is registered against exactly that marker (it has to be: `of()`
is a *trait* method, and a trait is not in any class's ancestry). The two mechanisms are
therefore exclusive: the stub gives you element types, the marker gives you `of()` narrowing.

**What to do instead.** Spell the specialization where it enters your code and let inference do
the rest — `/** @var NativeVector<int> $vector */` once, and `$vector->get(0)` is `int`,
`iterator_to_array($vector)` is `array<int, int>` and `$vector[0]` is `int|null` from there on.
The setup and the exact `neon` snippet are in
[`native-vectors.md`](native-vectors.md#static-analysis). Attribute-form templates are unaffected:
they need no stub, so `of()` narrows for them as it always did.

---

## Environment
Expand Down
Loading