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:

  1. docs/architecture.md
  2. docs/repr.md
  3. docs/class_shadows.md
  4. docs/builder.md
  5. docs/emission_pipeline.md

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

I want to understand the whole compiler

Read:

  1. docs/architecture.md
  2. docs/emission_pipeline.md

I want to add a new parsed feature

Read:

  1. docs/repr.md
  2. parser/
  3. docs/architecture.md

I want to change class static/class const shadow behavior

Read:

  1. docs/class_shadows.md
  2. v_glue.v
  3. c_emitter.v

I want to change export/code generation

Read:

  1. docs/builder.md
  2. docs/emission_pipeline.md

Important:

  • docs/emission_pipeline.md now documents return-shape classification for @[php_method], including container returns like map[string]string and []string

Ownership-facing Defaults

For new exported APIs, prefer ownership-aware wrapper types in signatures:

  • RequestBorrowedZBox
  • RequestOwnedZBox
  • PersistentOwnedZBox

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.PhpValue
  • vphp.PhpNull
  • vphp.PhpBool
  • vphp.PhpInt
  • vphp.PhpDouble
  • vphp.PhpString
  • vphp.PhpScalar
  • vphp.PhpArray
  • vphp.PhpObject
  • vphp.PhpCallable
  • vphp.PhpResource
  • vphp.PhpReference
  • vphp.PhpIterable
  • vphp.PhpThrowable
  • vphp.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.ZVal
  • vphp.RequestBorrowedZBox
  • vphp.RequestOwnedZBox
  • vphp.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 multiple name=type entries 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_optional only 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_default records PHP arginfo / reflection defaults and, for supported ordinary parameter forms, provides the value used by glue when omitted.
  • php_arg_type is most useful for interface/object/array/iterable contracts that V cannot express directly in PHP arginfo.
  • php_param_attr supports function and method parameters. Its attribute arguments support positional scalar literals, and named scalar literals are preserved for PHP attributes and PhpAttributeItem.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, and v_glue