feat(native): vectors of native data as a template over a PHP string - #15
Merged
Conversation
`NativeVector<int>` and `NativeVector<float>` are fixed-layout blocks of native scalars addressed as `$vector[$i]`, whose storage is an ordinary PHP string: element `i` lives at byte `i * 8` of that string's `zend_string.val`, in the machine's own layout. There is no encoding step, so pack()/unpack() appear nowhere on the path - a read initializes a PHP value from bytes that are already there, a write copies one into them, through a cached `zend_long *`/`double *`. The element type is enforced because `get()`, `set()` and `append()` are declared with the type parameter itself, which is a slot the engine really does check. `ArrayAccess::offsetSet(mixed, mixed)` cannot narrow its parameters, so the array syntax delegates to those three rather than replacing them, and `$vector[$i] = $x` gets exactly the TypeError `$vector->set($i, $x)` gets. Correctness rests on a small copy-on-write discipline: the class holds no engine reference on its own buffer, reads the refcount off the cached `zend_string` before every write, and separates the block - by assigning a string offset onto itself, the userland spelling of the engine's own separation - whenever somebody else is holding those bytes. That is what makes `toBinary()` safe to hand the buffer out rather than copy it. Growth stays literal `.=` concatenation, which the engine does in place precisely because nothing else holds a reference. This answers the scalar half of docs/limitations.md's most important entry: `array<T>` element types cannot be enforced, and a block of memory does not need them to be. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013qW1hct9mDKsFaHTvkLggx
NativeVector is the first template with private helpers, named constructors,
class constants, a defaulted constructor parameter and interfaces, and the
generator described none of those: every method came out `public`, statics were
dropped entirely, defaults were lost, and the class implemented nothing. Analysed
code therefore could not call `fromString()`, could not write
`new (NativeVector::of('int'))()` without being told it passed too few arguments,
could not index or count or iterate a vector - and could call a private helper
and be told nothing.
The generator now reproduces visibility, class constants, parameter defaults and
own static methods (`of()` stays the one it writes out itself), and declares the
interfaces PHP itself owns. A generic interface has to say what it was
parameterized with, and only the template knows, so the `@implements` tags are
copied off the class doc comment - the static-analysis source of truth, which is
what decision 1 reserves doc comments for. The library's own interfaces still
cannot be named: a stub is reflected before the analysed paths are indexed.
The stub only wins if PHPStan is not also reading the real declaration, so
NativeVector.php is excluded from scanning rather than just from analysis. Box
and AttributeBox regenerate byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qW1hct9mDKsFaHTvkLggx
…release NativeVectorTest is the sole owner of `NativeVector<int>`, `NativeVector<float>` and `NativeVector<string>` across the suite (AGENTS.md section 6), which is why the shipped example runs in a subprocess. pack() is used throughout as the ground truth the block is checked against: the production path never encodes anything, so an independent encoder is exactly what makes a byte-level assertion worth having. The two that justify the design are the engine's own TypeError arriving on `append(1.5)` and on `$vector[0] = 'x'` - the second proving the array syntax really does delegate - and the copy-on-write case, where a binary handed out by `toBinary()` is shown to survive a later `set()` and a later `append()` unchanged. A literal cast into a vector is shown untouched by a write for the same reason, since every literal is interned. examples/native-vector.php casts a blob of native int64s, indexes it, grows it, round-trips it and destroys it; its test asserts the shape of the output and the TypeErrors, never wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013qW1hct9mDKsFaHTvkLggx
…t decision docs/native-vectors.md covers the model in full: the block of memory is a PHP string, both native scalar kinds are eight bytes and `zend_string.val` is eight-aligned so `i * 8` is naturally aligned, the byte order is the machine's by definition, the cast and round trip, growth by concatenation, the copy-on-write discipline behind `toBinary()`, `destroy()`, and the roadmap towards sized scalar kinds and C structures. Two honest entries go with it. limitations.md's `array<T>` paragraph now points at the way out for scalar elements, and a new entry records what the stub costs: a stub cannot name this library's own interfaces, so a stub-described template is not a `GenericObject` and `of()` stops narrowing for it. The two mechanisms are exclusive; spell the specialization once at the boundary and everything downstream follows. static-analysis.md carries the neon snippet. AGENTS.md decision 13 is amended as the maintainer directed: `Native\` is the second sanctioned z-engine touchpoint, may use StringEntry (getRawValue included) and Core::cast, and confines both to NativeVector's private low-level methods, so the property that made decision 13 worth having still holds - every line that touches the engine can be named. `native` joins the commit scopes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013qW1hct9mDKsFaHTvkLggx
Owner
Author
|
Session:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Lisachenko\Generics\Native\NativeVector— a placeholder-form generic template whose storage is a PHP string acting as a raw block of memory:NativeVector<int>maps elements tozend_long,NativeVector<float>todouble(8 bytes each, naturally aligned inzend_string.val). Anything else is rejected at construction.pack()/unpack()anywhere on the hot path. Reads dereference a cached typed pointer (Core::cast('zend_long *'|'double *', $zstr->val)) so FFI initializes the PHP value straight from memory; writes store the value in place. Tests usepack()only as ground truth.get(int): T,set(int, T)andappend(T)are declared with the type parameter itself, so$intVector->append(1.5)is a realTypeErrorthrown by the Zend Engine — theArrayAccesssugar delegates to those methods rather than re-checking anything.zend_stringis exclusively owned (refcount 1, not interned); otherwise the buffer is separated first, so a string returned bytoBinary()is never retroactively mutated. Growth releases the pointer cache so.=keeps its in-place realloc fast path.StringEntry,Core::cast) — no z-engine changes. AGENTS.md decision 13 is amended to sanction theNative\namespace as the second engine touchpoint, confined to this class's private helpers.This is a direct answer to the
array<T>entry indocs/limitations.md: a vector's elements are enforced by construction.Also in this PR
NativeVectorException/NativeVectorBoundsExceptionwith named constructors (misaligned binary, unsupported element type, bounds, destroyed, fixed layout).analyseAndScan), so$v->get(0)isintforNativeVector<int>.docs/native-vectors.md(memory model, COW/destroy semantics, roadmap to sized scalar kinds and C-struct layouts), cross-links fromdocs/limitations.md,docs/static-analysis.mdand README.examples/native-vector.php+ subprocess example test.of()(stub vsGenericObject-extension exclusivity) — pre-existing forBox, now written down.Verification
All run locally on PHP 8.5.9 (z-engine
8.5.x-dev):ffi.enable=1,opcache.jit=off): 179 tests, 510 assertions, 1 pre-existing skipopcache.enable_cli=1 opcache.save_comments=0: pass (1 new deliberate skip: the stub generator's@implementstest, which reads doc comments by design — tooling, not runtime)--group internal --process-isolation: passcomposer test:analysis,composer phpstan(level max, no new baseline entries),composer cs:check,composer stubs:check,composer test:preload,composer bench:smoke: all passRoadmap (out of scope here)
Sized scalar kinds (
int32,uint16, …) and C-struct layouts as follow-up phases; a zero-copy engine-side buffer was considered and deliberately not built (this class already writes in place through the existing API).🤖 Generated with Claude Code
https://claude.ai/code/session_013qW1hct9mDKsFaHTvkLggx
Generated by Claude Code