VPHP Compiler Docs
May 13, 2026 ยท View on GitHub
This directory contains the VPHP compiler implementation and its design documents.
Project overview:
Start Here
If you are new to the compiler, read these in order:
Implementation Layout
vphp/compiler/
entry.v # compile pipeline entry
export.v # export assembly and file emission
c_emitter.v # C wrapper emission
v_glue.v # V bridge emission
php_types/ # shared PHP-facing type/spec mapping
repr/ # compiler representations
parser/ # AST -> repr
linker/ # post-parse linking
builder/ # repr -> export fragments
Quick Map
Core pipeline
Submodules
-
repr- internal compiler representations
-
parser- V AST parsing into reprs
-
linker- post-parse reconciliation such as class shadow linking
-
builder- reusable export/code fragment builders
Recommended Reading By Task
I want to understand the whole compiler
Read:
I want to add a new parsed feature
Read:
- docs/repr.md
parser/- docs/architecture.md
I want to change class static/class const shadow behavior
Read:
I want to change export/code generation
Read:
Important:
docs/emission_pipeline.mdnow documents return-shape classification for@[php_method], including container returns likemap[string]stringand[]string
Ownership-facing Defaults
For new exported APIs, prefer ownership-aware wrapper types in signatures:
RequestBorrowedZBoxRequestOwnedZBoxPersistentOwnedZBox
Treat these as the compiler-facing default vocabulary.
In practice:
- parameter signatures should prefer
RequestBorrowedZBox - temporary owned results inside glue should prefer
RequestOwnedZBox - long-lived stored values should prefer
PersistentOwnedZBox
Exported Function Parameter Forms
Exported V functions and methods should use one of five parameter forms. Pick the narrowest form that matches the API surface you want PHP users to see.
1. vphp.Context
Use Context when the implementation needs the full PHP call frame:
@[php_function]
fn handle(ctx vphp.Context) {
args := ctx.args_with_meta([
vphp.PhpArgMeta{
index: 0
name: 'payload'
},
])
value := args.at(0).value
ctx.return().string(value.type_name())
}
Context is hidden from PHP arginfo. It is the escape hatch for dynamic
argument handling, manual return writes, or direct access to request state.
Use ctx.args() when raw argument values are enough. Use
ctx.args_with_meta(...) when a manual Context function wants to attach
declaration metadata such as names or attributes.
Do not combine Context with PhpArgs; PhpArgs is derived from Context.
2. V Native Values
Use plain V types when the function wants business values:
@[php_function]
fn add(foo int, bar string, active bool, ratio f64) string {
return '${foo}:${bar}:${active}:${ratio}'
}
The generated glue reads PHP arguments, converts them to V values, and calls the function with those decoded values.
3. PHP Semantic Wrappers
Use semantic wrappers when the implementation wants PHP value semantics without
working at raw ZVal level:
@[php_function]
fn inspect(value vphp.PhpValue, obj vphp.PhpObject, arr vphp.PhpArray, cb vphp.PhpCallable) string {
return '${value.type_name()}:${obj.class_name()}:${arr.count()}:${cb.is_valid()}'
}
Supported wrapper parameters include:
vphp.PhpValuevphp.PhpNullvphp.PhpBoolvphp.PhpIntvphp.PhpDoublevphp.PhpStringvphp.PhpScalarvphp.PhpArrayvphp.PhpObjectvphp.PhpCallablevphp.PhpResourcevphp.PhpReferencevphp.PhpIterablevphp.PhpThrowablevphp.PhpEnumCase
Optional wrappers are supported:
@[php_function]
fn maybe(obj ?vphp.PhpObject, value ?vphp.PhpValue) string {
// ?PhpObject is a narrowing wrapper: null/non-object becomes none.
// ?PhpValue preserves PHP null as some(PhpValue(null)); none means missing.
return '${obj == none}:${value == none}'
}
4. Lifecycle / Raw Value Escape Hatches
Use these when ownership or raw Zend interop is the point of the API:
@[php_function]
fn low_level(raw vphp.ZVal, borrowed vphp.RequestBorrowedZBox) vphp.RequestOwnedZBox {
return vphp.RequestOwnedZBox.of(raw)
}
Supported low-level forms include:
vphp.ZValvphp.RequestBorrowedZBoxvphp.RequestOwnedZBoxvphp.PersistentOwnedZBox
These are useful for infrastructure and compatibility code. Prefer semantic wrappers for application-facing APIs.
5. PHP Argument Names
Exported V parameters use PHP-facing names in generated arginfo, stubs, and
named-argument lookup. By default, V snake_case parameter names are exported
as PHP camelCase names, matching @[params] struct fields:
@[php_function]
fn find_user(default_value string) string {
return default_value
}
PHP callers see $defaultValue and can call find_user(defaultValue: 'x').
Use @[php_arg_name: 'v_name=phpName'] only for explicit exceptions.
Argument metadata attributes accept both the legacy string form and V call-style attributes:
@[php_arg_name: 'default_value=defaultValue']
@[php_arg_default(default_value: '7')]
@[php_arg_optional(default_value: true)]
@[php_arg_type(default_value: 'int')]
@[php_param_attr(default_value: 'FromQuery("default"), MustBeInt')]
@[php_function]
fn read_value(default_value int) int {
return default_value
}
php_param_attr attaches PHP 8 attributes to function or method parameters.
The key is the V parameter name, and the value is a comma-separated list of PHP
attributes parsed with the same scalar DSL as class-level php_attr:
@[php_param_attr(query: 'FromQuery("q"), MustBeString', page: 'FromQuery("page"), MustBeInt')]
@[php_function]
fn search(query string, page int) string {
return query
}
This produces PHP parameter attributes visible through ReflectionParameter.
V does not currently support attributes directly on parameters, so
php_param_attr is the parameter-level escape hatch.
The generated glue also carries parameter attributes into PhpArgMeta, so
runtime helpers can inspect them from PhpArg:
@[php_param_attr(query: 'FromQuery("q"), MustBeString')]
@[php_function]
fn search(query string) string {
return query
}
Inside generated glue this becomes a PhpArgMeta entry similar to:
vphp.PhpArgMeta{
index: 0
name: 'query'
attributes: [
vphp.PhpAttribute.named('FromQuery').for_parameter().string('q'),
vphp.PhpAttribute.named('MustBeString').for_parameter(),
]
}
For pure Context functions, there is no V parameter list for the compiler to
read. Extension authors must provide the metadata explicitly when they need it:
@[php_function]
fn dynamic(ctx vphp.Context) {
args := ctx.args_with_meta([
vphp.PhpArgMeta{
index: 0
name: 'query'
attributes: [
vphp.PhpAttribute.named('FromQuery').string('q'),
vphp.PhpAttribute.named('Range').int(1).int(100),
]
},
])
query := args.at(0)
source := query.attr('FromQuery') or { vphp.PhpAttribute.named('') }
ctx.return().string_value(source.items[0].value)
}
ctx.args_with_meta(...) fills missing attribute targets with .parameter.
Generated glue writes .for_parameter() explicitly so the generated code is
self-describing.
6. @[params] Structs
Use a params struct when PHP arguments should map to a named/default parameter object:
@[params]
struct CreateResponseParams {
status int = 200
reason_phrase string = ''
}
@[php_function]
fn create_response(params CreateResponseParams) string {
return '${params.status}:${params.reason_phrase}'
}
The compiler expands the final @[params] struct into PHP-visible arguments,
builds PhpArgs in glue, applies V defaults when arguments are omitted, and
then calls the V function with the constructed params struct.
Generated params struct bindings read arguments by PHP parameter name first and fall back to position. This allows PHP callers to skip optional parameters with named arguments:
create_response(reasonPhrase: 'Accepted');
Params struct fields may use plain V scalar types or semantic PHP wrappers such
as vphp.PhpString, vphp.PhpBool, and vphp.PhpArray. Wrapper fields get
semantic empty defaults and still validate the PHP value when it is supplied.
Do not expose vphp.PhpArgs directly as a function parameter. PhpArgs is a
glue/runtime model and is available from Context via ctx.args() or
ctx.args_with_meta(...) when a function intentionally enters the low-level
Context mode.
Exported Function Return Forms
Exported V functions and methods should return through one of five forms. Match the return form to the same semantic layer used by the implementation.
1. vphp.Context Manual Return
Use ctx.return() when the function is already in explicit Context mode and
needs to write the PHP return slot manually:
@[php_function]
fn dynamic(ctx vphp.Context) {
args := ctx.args_with_meta([
vphp.PhpArgMeta{
index: 0
name: 'payload'
},
])
ctx.return().string_value(args.at(0).value.type_name())
}
ctx.return() returns vphp.PhpReturn. It supports direct methods such as
null(), bool_value(...), int_value(...), double_value(...),
string_value(...), value(...), dyn_value(...), request_owned(...), and
zval(...).
2. V Native Values
Return plain V values when the PHP API should receive decoded business data:
@[php_function]
fn add(foo int, bar int) int {
return foo + bar
}
Supported scalar returns include string, bool, int, i64, and f64.
Options and results keep their bridge semantics: ?T maps none to PHP null,
and !T maps errors to PHP exceptions.
3. V Containers And Structs
Return V arrays, maps, or structs when the function intentionally returns a PHP array built from V data:
struct UserPayload {
name string
age int
}
@[php_function]
fn user_payload() UserPayload {
return UserPayload{ name: 'codex', age: 7 }
}
This is a serialization path. It is appropriate for plain V data, not for PHP semantic wrappers.
4. PHP Semantic Wrappers
Return semantic wrappers when the implementation wants to preserve the original PHP value semantics:
@[php_function]
fn same_object(obj vphp.PhpObject) vphp.PhpObject {
return obj
}
@[php_function]
fn same_array(arr vphp.PhpArray) vphp.PhpArray {
return arr
}
Supported wrapper returns include the parameter wrappers listed above, plus
vphp.PhpClass, vphp.PhpFunction, vphp.PhpClosure, and persistent wrapper
forms such as vphp.PhpValue, vphp.PhpArray with persistent-owned storage,
vphp.PhpObject, and vphp.PhpClosure with persistent-owned storage.
Semantic wrapper returns preserve the wrapped PHP value. They are not serialized as V structs.
5. Lifecycle / Raw Value Returns
Return lifecycle boxes or raw values when ownership is part of the API:
@[php_function]
fn low_level(raw vphp.ZVal) vphp.RequestOwnedZBox {
return vphp.RequestOwnedZBox.of(raw)
}
Supported low-level returns include vphp.ZVal, vphp.RequestBorrowedZBox,
vphp.RequestOwnedZBox, and vphp.PersistentOwnedZBox.
Prefer semantic wrappers for application-facing APIs. Use lifecycle/raw returns for infrastructure code, compiler tests, and places where ownership is the actual behavior being exposed.
PHP Signature Attributes
The compiler supports a small set of explicit attributes for PHP-facing signatures. Prefer these over parameter-name heuristics.
@[php_arg_type: 'request=Psr\\Http\\Message\\RequestInterface']overrides the PHP arginfo type for a specific parameter.@[php_arg_type: 'a=array,b=Traversable']supports multiplename=typeentries in one attribute.@[php_arg_default: 'default_status=200,default_reason_phrase=""']records PHP reflection default values for optional parameters.@[php_arg_optional: 'default_status,default_reason_phrase']marks trailing PHP-optional parameters explicitly.@[php_param_attr(query: 'FromQuery("q"), MustBeString')]attaches one or more PHP 8 attributes to a parameter.
Notes:
php_arg_optionalonly changes PHP required-arg counts / arginfo shape. For ordinary parameters with a supported default literal, the generated glue also applies the default when PHP omits the argument.php_arg_defaultrecords PHP arginfo / reflection defaults and, for supported ordinary parameter forms, provides the value used by glue when omitted.php_arg_typeis most useful for interface/object/array/iterable contracts that V cannot express directly in PHP arginfo.php_param_attrsupports function and method parameters. Its attribute arguments support positional scalar literals, and named scalar literals are preserved for PHP attributes andPhpAttributeItem.name.- Be conservative with scalar narrowing (
string,int,bool) on exported interfaces. If compatibility matters, prefer runtime validation over over-constrained arginfo.
Guiding Idea
The compiler is organized around this progression:
AST -> repr -> linker -> builder fragments -> emitted C/V bridge code
When making changes, try to keep responsibilities in the right layer:
- parsing in
parser - semantic data in
repr - relationship reconciliation in
linker - reusable export assembly in
builder - concrete output generation in
export,c_emitter, andv_glue