Class Shadow Linking
May 19, 2026 ยท View on GitHub
Goal
Class shadows are the mechanism VPHP uses to implement:
- PHP class constants backed by V-side constant carriers
- PHP class static properties backed by V-side shadow state
This lets V remain the semantic source while PHP receives familiar class-level APIs.
Why Shadows Exist
PHP wants class-level constructs such as:
Article::MAX_TITLE_LENArticle::$total_count
But in V, the natural backing data is usually:
- a module-level constant carrier
- a module-level shadow state struct/instance
So the compiler bridges them in two steps:
- parse class metadata that names the shadow source
- link the class to the actual shadow carrier later
Two Shadow Kinds
1. Shadow Constants
Purpose:
- expose PHP class constants from a V-side constant carrier
Class-side metadata:
shadow_const_name- later resolved into
shadow_const_type
Linked result:
- appended
PhpClassConstReprentries on the class repr
2. Shadow Statics
Purpose:
- expose PHP static properties from a V-side shadow state struct
Class-side metadata:
shadow_static_name- later resolved into
shadow_static_type
Linked result:
- appended
PhpClassPropReprentries withis_static = true
Current Linker Entry
Implemented in class.v.
Entry point:
link_class_shadows(mut elements, table)
This runs after all parse passes are complete.
Pipeline Position
flowchart TD
A["Parser creates class repr"] --> B["Parser creates const repr"]
B --> C["link_class_shadows(...)"]
C --> D["Class repr gains constants/static props"]
D --> E["builder/emitter export final PHP class shape"]
This ordering matters because shadow targets may be declared elsewhere in the source set.
Shadow Constant Linking
Implemented by:
link_class_shadow_constants(...)
Input Expectations
The class repr contains:
shadow_const_name != ''
And elements contains a matching repr.PhpConstRepr whose:
name == shadow_const_name
What Happens
- find the matching
PhpConstRepr - copy its
v_typeintocls.shadow_const_type - iterate over
el.fields - append one
PhpClassConstReprper field intocls.constants
Result
The class behaves as if those constants were declared directly on it.
Important Note
The source PhpConstRepr may be:
- a real exported PHP global constant
- or just a shadow carrier
The linker does not care. It only cares that the repr contains structured constant fields.
Shadow Static Linking
Implemented by:
link_class_shadow_statics(...)
Input Expectations
The class repr contains:
shadow_static_name != ''
And elements contains a matching repr.PhpConstRepr whose:
name == shadow_static_name
That constant repr points to a V type through:
v_type
What Happens
- find the matching
PhpConstRepr - copy
el.v_typeintocls.shadow_static_type - resolve that type in the V type table
- read its struct fields from
ast.Table - append one
PhpClassPropReprper field:visibility = 'public'is_static = true
Why ast.Table Is Needed
Shadow statics are not reconstructed from string metadata alone.
The linker needs the real V struct definition to determine:
- field names
- field types
That is why link_class_shadow_statics(...) receives table &ast.Table.
Why Linking Is Separate From Parsing
Keeping shadow resolution out of the parser helps in three ways.
1. Parse order stays simple
Parser can read declarations as they come without requiring every referenced shadow source to already be resolved.
2. Repr stays explicit
The class repr can say:
- "I depend on
article_statics"
without pretending that all derived PHP static properties are already known.
3. Linking logic stays centralized
All "resolve this class-level relationship after parse" logic can live in one place.
That becomes more valuable as new inferred relationships appear later.
Runtime Relationship
Linking is only the compile-time half.
For shadow statics, runtime behavior still requires bridge glue.
That happens later in v_glue.v, where class glue emits:
Class.statics()sync_statics_from_php(ctx)sync_statics_to_php(ctx)
So the full picture is:
- linker decides which PHP static properties exist
- builder/emitter declare them on the PHP class
- V glue synchronizes runtime values between PHP and V shadow state
Design Constraints
Current constraints are intentional.
Shadow constants
- expect a structured
PhpConstRepr.fields - map cleanly to
PhpClassConstRepr
Shadow statics
- expect a real V struct type
- derive properties from struct fields
- currently expose linked fields as public static properties
These defaults are conservative and easy to reason about.
Future Extension Points
This linker can grow in predictable ways.
Possible future additions
- inferred interface implementation linking
- trait flattening/link reconciliation
- property hook / interface property relationship checks
- stricter validation for missing or malformed shadow carriers
If those arrive, they should likely become additional linker files rather than enlarging parser or entry.v.
Failure Modes To Watch
The current linker is intentionally lightweight, so these are useful things to keep in mind.
-
missing shadow target
- class keeps unresolved shadow name/type
- no derived constants/properties are appended
-
unresolved V type
- static shadow type lookup may fail if the symbol cannot be found in the table
-
malformed carrier
- if the target does not have the expected shape, linking silently does less work
These are good candidates for future validation improvements.
Summary
Class shadows are a compile-time + runtime collaboration:
- compile time:
- linker resolves what the PHP class should expose
- runtime:
- V glue synchronizes actual values
This design keeps PHP-facing OOP APIs natural while allowing V to remain the real owner of the underlying class-level data.