Type System Strategy: Medium (Gradual Contracts)
RHP implements a medium-strategy gradual type system:
- Strict Signature Boundaries: Function, method, and constructor signatures enforce explicit parameter and return types to ensure API predictability, compile-time safety, and IDE auto-completion.
- Dynamic Internal Inference: Variable assignments inside function bodies are dynamically inferred and validated by the compiler, maintaining rapid developer ergonomics.
- No Implicit Type-Shifting: Once a variable is declared with an explicit type signature (such as
int $x = 0;), assigning an incompatible type throws a compile-time or runtimeVariableTypeMismatcherror.
Scalar Primitives
All primitive type keywords are strictly lowercase:
| Type Keyword | Description | Default Rust Storage |
|---|---|---|
int |
Signed integer — defaults to i32 |
i32 |
float |
Floating-point number — defaults to i32 (32-bit float) |
i32 |
string |
Binary-safe UTF-8 byte buffer with 23-byte SSO | RhpString (Vec<u8>) |
bool |
Boolean value (true or false) |
bool |
void |
Absence of a return value | Unit () |
mixed |
Permissive dynamic container | PhpValue |
Capitalized scalar variants (such as String or Integer) are strictly rejected by the parser.
int $count = 42;
float $pi = 3.14159;
string $appName = "RHP Engine";
bool $isProduction = true;
// Attempting incompatible reassignment fails:
// $count = "forty-two"; // Error: VariableTypeMismatch
Binary-Safe Strings & Multi-Byte Encodings
In RHP, strings are fundamentally raw byte buffers (RhpString storing $\le 23$ bytes inline on the stack via Small-String-Optimization and larger buffers via Rc<Vec<u8>>):
- Binary Safety: Strings can hold arbitrary binary data, image headers, network payloads, or null bytes (
\0) without triggering Rust UTF-8 panics. - Legacy Japanese Support: Legacy multi-byte character sets such as Shift-JIS (SJIS / CP932), EUC-JP, and ISO-2022-JP (JIS) are preserved as raw bytes. Developers use
Str::fromEncoding($raw, 'Shift-JIS')orStr::utf8($raw)for safe conversions. - Immutable Helpers: Chained helper methods like
$text->trim()->upper()are immutable by default, leaving the original variable untouched. Variable updates use explicit reassignment ($text = $text->trim()->upper();) with Copy-On-Write (COW) semantics.
String Quoting Strategies & Escaping
RHP provides versatile quoting strategies to simplify string handling, multi-line formatting, and template safety:
- Nested Opposing Quotes: Single quotes can enclose double quotes without escaping (
'Hello "world"'), and double quotes can enclose single quotes ("It's working!"). - Escaped Quotes: Backslash escapes
\'and\"are supported inside single- and double-quoted strings. - Raw Backtick Strings: Enclosing strings in backticks (
`...`) creates raw string literals where escape sequences (like\nor\t) and quotes are treated as literal characters without interpretation. - Line Ending Normalization: All newline variations (CRLF
\r\n, LF\n, CR\r) are tracked by the lexer and normalized cleanly across platforms.
string $single = 'He said, "Hello"';
string $double = "It's a great day";
string $escaped = "Line 1\nLine 2 with \"quotes\"";
string $rawPath = `C:\new_folder\test.txt`; // Literal backslashes preserved
Variable Declarations (Prefix Type Syntax)
RHP supports statically locked variable declarations using prefix type notation:
int $x = 10;
string $name = "Alice";
float $rate = 0.05;
bool $isActive = true;
int $uninitialized; // Uninitialized typed declaration
[!IMPORTANT] No Colon Type Syntax (
$x: int = 10;): RHP only supports prefix type declarations (int $x = 10;). TypeScript/Hack-style colon declarations ($x: int = 10;or$x: int;) are strictly forbidden and trigger a compile-time parse error:ParseError: Colon type syntax '$x: Type' is not supported. Use prefix type syntax 'Type $x' instead (e.g., 'int $x = 10;').This rule applies uniformly across local variables, class properties (
public int $id = 1;), and parameter lists (function calc(int $id)).
Variable Identifier Validation Rules
Variable identifiers must strictly adhere to lexical naming rules:
- Sigil (
$) Requirement: All variables must begin with a leading$sigil. - Identifier Character Rules: The character immediately following
$must be an ASCII letter (a-z,A-Z) or underscore (_), followed by any combination of letters, digits (0-9), or underscores. - Prohibited Patterns:
- Bare sigils (
$) without an identifier name are rejected. - Variable-variables (
$$var,$$$name) are strictly prohibited in RHP. - Variables starting with digits (
$0,$123) are forbidden.
- Bare sigils (
When an invalid variable name is detected, the lexer produces a clear diagnostic error:
ParseError: Variable names must start with a letter or underscore, followed by alphanumeric characters (e.g., $varName).
// ✅ Valid variable identifiers:
int $count = 10;
string $_temp = "cache";
float $data_v2 = 3.14;
// ❌ Invalid variable identifiers (trigger compile-time error):
// $$variable; // Error: Variable names must start with a letter or underscore...
// $0; // Error: Variable names must start with a letter or underscore...
// $; // Error: Variable names must start with a letter or underscore...
Type Conversion & Casting (Functional Syntax)
Type conversions in RHP use function-style conversion syntax rather than C-style or PHP-style parenthesized casts:
$raw = "42";
// Supported: functional type conversions
$count = int($raw); // 42 (int)
$text = string(123); // "123" (string)
$pi = float("3.14159"); // 3.14159 (float)
$flag = bool(1); // true (bool)
[!WARNING] C-Style / PHP-Style Casts (
(int)$x) Are Not Supported: Parenthesized type casting like(int)$x,(string)$x,(float)$x, or(bool)$xis rejected by the parser with a clear diagnostic:ParseError: Type cast syntax '(int)' is not supported in RHP. Use functional conversion 'int(...)' instead (e.g. 'int($x)').This guarantees consistent, unambiguous expression parsing and aligns scalar casting with functional conversions.
Default Storage: int and float are both i32
[!IMPORTANT] Both
intandfloatdefault toi32(32-bit) storage. Thefloatprimitive does not map to Rust’sf64by default. It stores a 32-bit integer under the hood just likeint. UseFloat::f64(...)orFloat::f32(...)via theFloatfacade when you explicitly need IEEE-754 precision.
int $count = 42; // i32 internally
float $score = 9; // also i32 internally — NOT f64
// Explicit 64-bit or IEEE float via the Float facade:
$precise = Float::f64(3.141592653589793); // f64 precision
$big = Int::i64(9223372036854775807); // i64 max value
Int and Float — Supporting Facade Classes (NOT Base Types)
Int and Float are static facade classes — they are not instances of int or float and can never be instantiated with new. They provide factory methods and fluent utilities for working with extended Rust numeric types.
| Item | Kind | Example |
|---|---|---|
int |
Primitive keyword (base datatype) | int $x = 5; |
float |
Primitive keyword (base datatype) | float $y = 3; |
Int |
Static facade class (identifier) | Int::i64(...) |
Float |
Static facade class (identifier) | Float::f64(...) |
// ✅ Correct — use as type keywords (primitive base types)
int $age = 30;
float $amount = 100;
// ✅ Correct — use as static facade (extended numeric utilities)
$bigId = Int::i64(9223372036854775807);
$precise = Float::f64(3.141592653589793)->toDecimal(6);
$random = Int::i64()->random()->add()->subtract();
// ❌ Fatal Error — Int and Float cannot be instantiated
// $obj = new Int();
// => Fatal Error: Cannot instantiate static facade class 'Int'. Use static methods directly.
[!CAUTION] Calling
new Int(),new Float(),new Arr(), ornew Str()always produces a fatal compile-time error. These are purely static facades.
Extended Rust Numeric Types via Facade Methods
For low-level operations (cryptography, network protocols, large data), use the Int and Float facades to access native Rust numeric variants with full fluent chaining:
// --- Int facade ---
$small = Int::i8(127); // i8 (-128 to 127)
$medium = Int::i32(2_147_483_647); // i32 (same as plain `int` default)
$large = Int::i64(9_223_372_036_854_775_807); // i64
$huge = Int::i128(170141183460469231731687303715884105727); // i128
$ptr = Int::usize(1024); // usize (pointer-width)
$chain = Int::i64()->random()->add(10)->subtract(3); // fluent
// --- Float facade ---
$single = Float::f32(3.14); // f32 (same as plain `float` default storage)
$double = Float::f64(3.141592653589793); // f64 full IEEE-754 64-bit
$fmt = Float::f64(1234567.89)->toDecimal(2); // "1234567.89"
$pct = Float::f64(0.75)->toPercent(); // "75.00%"
Explicit Subtyped Collections (Arr)
Instead of standard, ambiguous hash tables for every list or map, RHP provides four specialized collection subtypes backed directly by Rust heap structures:
1. Arr::list (Sequential Lists)
Optimized for strictly sequential numerical indices and maximum CPU cache locality. Backed by contiguous Rust Rc<RefCell<Vec<PhpValue>>> with Copy-On-Write (COW) semantics.
- CPU Cache Locality & Zero-Allocation Iteration: During
foreach ($list as $key => $val)iteration, the runtime uses a dedicatedActiveIterator::Liststream that points directly to the underlyingVec<PhpValue>contiguous memory buffer with an internal cursor index. It streams elements without allocating intermediate key-value pair vectors or cloning keys. - Strict Sequential Indexing: Supports numeric index reading (
$list[0]), updating ($list[1] = 999), and sequential appending ($list[] = $val). - String Key Rejection: Attempting to assign non-numeric string keys (such as
$list['name'] = 'val') is rejected with anUndefinedIndexerror, preventing sequential lists from degrading into hash tables. - Inline Type Annotations & Implicit Factory Lowering: Local variable declarations using
list $arr = [];are compiled directly into optimized Rust-backedArr::list([])allocations under the hood.
// Inline variable type annotation and implicit factory initialization
list $numbers = [10, 20, 30, 40]; // Lowers directly to Arr::list([10, 20, 30, 40])
// Fluent higher-order transformations
list $doubled = $numbers
->map(fn($n) => $n * 2)
->filter(fn($n) => $n > 30);
print($doubled[0]); // 40
// Sequential operations
$doubled[] = 100; // Appends 100
$doubled[1] = 999; // Updates index 1
print($doubled->count()); // 4
// Attempting to assign a non-numeric string key throws an error:
// $doubled['invalid_key'] = 42; // Error: Undefined index or key 'invalid_key'
Native Lowercase Collection Keywords & Inline Declarations
RHP aligns with PHP native conventions by using strictly lowercase keywords for collection types: list, dict, set, and hashmap (matching array, string, int, and bool).
This design provides two major advantages:
- PHP Native Alignment: Avoids confusion with userland classes or external object models. Collection hints feel like first-class language primitives.
- Inline Variable Declarations: Allows developers to declare and initialize statically typed collections in local scopes:
list $my_new_scoped_arr = []; dict $my_new_scoped_dict = [];
Compiler & Parser Lowering Strategy
When RHP processes collection types, the lexer, parser, and IR lowerer collaborate through a three-stage lowering pipeline:
- Token Recognition: The lexer recognizes
list,dict,set, andhashmapas dedicated built-in type keywords (Token::TypeList,Token::TypeDict, etc.), clearly separating them from user-defined class identifiers. - Implicit Lowering to Factory Calls: When the parser encounters local variable declarations or assignments like
list $my_arr = [];, it compiles them directly into the optimized Rust-backed factory initialization: $$\text{list } $my_arr = []; \quad \longrightarrow \quad $my_arr = \text{Arr::list}([]); \quad \text{(backed by contiguous Vec)}$$ $$\text{dict } $my_dict = []; \quad \longrightarrow \quad $my_dict = \text{Arr::dict}([]); \quad \text{(backed by IndexMap)}$$ $$\text{set } $my_set = []; \quad \longrightarrow \quad $my_set = \text{Arr::set}([]); \quad \text{(backed by HashSet)}$$ $$\text{hashmap } $my_map = []; \quad \longrightarrow \quad $my_map = \text{Arr::hashMap}([]); \quad \text{(backed by HashMap)}$$ - Parameter Type Safety: At function boundaries, type contracts enforce compile-time verification and runtime guards (
OpCode::AssertType), ensuring only the correct high-performance Rust variant enters the function.
2. Arr::dict (Insertion-Ordered Maps)
String-keyed map retaining strict deterministic insertion order and amortized $O(1)$ lookups, updates, and removals. Backed directly by Rust’s Rc<RefCell<IndexMap<String, PhpValue>>>.
- Strict Insertion Ordering: Unlike standard unordered hash tables,
Arr::dictpreserves exact key insertion order across all iterations, serializations, and mutations. Updating the value of an existing key preserves its original index slot. - Zero-Allocation Streaming Iteration:
foreach ($dict as $key => $val)drives anActiveIterator::Dictthat indexes directly into the underlyingIndexMapvia an internal cursor, eliminating intermediate vector allocations. - Laravel Collection Methods:
Arr::dictexposes the full suite of Laravel-style collection instance methods for fluent, functional data pipelines.
dict $product = Arr::dict([
'sku' => 'PROD-100',
'name' => 'Ergonomic Desk',
'price' => 250,
'discount' => 25,
'tax' => 15,
]);
// 1. Fluent Inspection & Access
$name = $product->get('name'); // 'Ergonomic Desk'
$stock = $product->get('stock', 0); // Fallback default: 0
$hasSku = $product->has('sku'); // true
$hasAny = $product->hasAny(['color', 'weight']); // false
list $keys = $product->keys(); // ['sku', 'name', 'price', 'discount', 'tax']
list $values = $product->values(); // ['PROD-100', 'Ergonomic Desk', 250, 25, 15]
// 2. In-Place Mutations & Detachment (Copy-On-Write)
$product->put('in_stock', true);
$discount = $product->pull('discount', 0); // 25 (removes 'discount' from $product)
$product->forget('tax'); // Removes 'tax'
// 3. Higher-Order Transforms (Immutable Pipelines)
dict $prices = Arr::dict(['item1' => 10, 'item2' => 20])
->map(fn($price, $key) => $price * 2); // ['item1' => 20, 'item2' => 40]
dict $filtered = Arr::dict(['a' => 10, 'b' => 5, 'c' => 25])
->filter(fn($val, $k) => $val >= 10); // ['a' => 10, 'c' => 25]
dict $rejected = Arr::dict(['a' => 10, 'b' => 5, 'c' => 25])
->reject(fn($val, $k) => $val >= 10); // ['b' => 5]
int $sum = Arr::dict(['x' => 10, 'y' => 20, 'z' => 30])->sum(); // 60
float $avg = Arr::dict(['x' => 10, 'y' => 20, 'z' => 30])->avg(); // 20.0
// 4. Filtering, Flipping, Merging & JSON Serialization
dict $subset = $product->only(['sku', 'name']);
dict $without = $product->except(['sku']);
dict $flipped = Arr::dict(['role' => 'admin'])->flip(); // ['admin' => 'role']
dict $merged = Arr::dict(['a' => 1])->merge(Arr::dict(['b' => 2, 'a' => 99])); // ['a' => 99, 'b' => 2]
string $json = $product->toJson(); // '{"sku":"PROD-100","name":"Ergonomic Desk",...}'
// 5. Zero-Allocation Streaming Iteration
foreach ($product as $prop => $val) {
Sys::print("{}: {}", $prop, (string)$val);
}
3. Arr::set (Unique Sets - Step 10)
Unique collection mapped directly to native Rust HashSet<PhpValue>. Automatically deduplicates values, provides guaranteed $O(1)$ set membership, and implements mathematical set algebra:
set $tags = Arr::set(['fast', 'rust', 'php', 'rust', 'backend', 'php']);
// 1. Automatic Deduplication
print($tags->count()); // 4 (unique: 'fast', 'rust', 'php', 'backend')
// 2. Element Insertion & Bracket Push Syntax
$tags->add('frontend'); // Appends 'frontend'; returns $tags
$tags[] = 'systems'; // Bracket push syntax (deduplicated automatically)
// 3. O(1) Membership Checking
bool $hasRust = $tags->contains('rust'); // true
bool $hasJava = $tags->has('java'); // false
// Bracket Indexing for Membership
bool $isMember = $tags['rust']; // true (bracket indexing checks set membership)
$tags['go'] = true; // Inserts 'go' into set
$tags['go'] = false; // Removes 'go' from set
// 4. Element Removal
bool $removed = $tags->remove('php'); // true (was present)
$tags->delete('nonexistent'); // false (alias for remove())
// 5. Mathematical Set Algebra
$setA = Arr::set([1, 2, 3, 4]);
$setB = Arr::set([3, 4, 5, 6]);
set $diff = $setA->diff($setB); // Relative complement: {1, 2}
set $intersect = $setA->intersect($setB); // Intersection: {3, 4}
set $union = $setA->union($setB); // Set union: {1, 2, 3, 4, 5, 6}
set $diffArr = $setA->diff([2, 4]); // Cross-collection algebra with plain array
// 6. Higher-Order Transforms & Reductions
set $evens = Arr::set([1, 2, 3, 4, 5, 6])
->filter(fn($n) => $n % 2 === 0); // {2, 4, 6}
set $doubled = $evens->map(fn($n) => $n * 10); // {20, 40, 60}
int $sum = $doubled->reduce(fn($acc, $n) => $acc + $n, 0); // 120
// 7. Streaming Foreach & Export
list $asList = $tags->all(); // Converts to sequential list
string $json = $tags->toJson(); // '["fast","rust",...]'
foreach ($tags as $tag) {
// Visits each unique element exactly once
}
4. Arr::hashMap / Arr::map (Universal Complex Maps - Step 11)
Universal general key-value dictionary mapped directly to native Rust HashMap<PhpValue, PhpValue>. Unlike traditional PHP associative arrays which strictly coerce keys to integers or strings, Arr::hashMap supports arbitrary complex keys (arrays/lists, objects/class instances, sets, and scalars):
class Coordinate {
public float $lat;
public float $lng;
public function __construct(float $lat, float $lng) {
$this->lat = $lat;
$this->lng = $lng;
}
}
// 1. Instantiation (using Arr::hashMap or Arr::map alias)
$map = Arr::hashMap();
// 2. Arrays / Lists as Complex Keys
$map[[10, 20]] = 'Alpha Base';
$map[[30, 40]] = 'Beta Outpost';
$loc = $map[[10, 20]]; // 'Alpha Base' (lookups use deep structural equality)
// 3. Objects / Class Instances as Keys
$c1 = new Coordinate(37.7749, -122.4194);
$c2 = new Coordinate(40.7128, -74.0060);
$map[$c1] = 'San Francisco';
$map[$c2] = 'New York';
$city = $map[$c1]; // 'San Francisco'
// 4. Sets & Scalars as Keys
$map[Arr::set(['read', 'write'])] = 'AdminPerms';
$map[42] = 'integer key';
$map[3.14] = 'float key';
$map['tenant'] = 'string key';
$map[true] = 'boolean key';
// 5. Fluent Key-Value Operations
$map->put('timeout', 5000);
$timeout = $map->get('timeout', 1000); // 5000
$fallback = $map->get('missing', 'default_val'); // 'default_val'
bool $hasTimeout = $map->has('timeout'); // true (also: contains(), hasKey())
$pulled = $map->pull('timeout', 0); // Retrieves 5000 and removes key
$map->remove('tenant'); // Removes key (also: forget())
// 6. Projections & Higher-Order Transforms
list $allKeys = $map->keys(); // List of keys (including complex keys)
list $allVals = $map->values(); // List of values
list $entries = $map->entries(); // List of [[$key, $value], ...] pairs
$doubled = $map
->filter(fn($v, $k) => is_numeric($v))
->map(fn($v, $k) => $v * 2);
// Re-index entries via mapWithKeys
$reindexed = $map->mapWithKeys(fn($v, $k) => ["transformed_" . (string)$k => $v]);
// Diff, Intersect & Merge
$other = Arr::hashMap(['a' => 1, 'b' => 2]);
$diffMap = $map->diff($other); // Keys in $map not in $other
$merged = $map->merge($other); // Merged map
// 7. Streaming Foreach with Complex Keys
foreach ($map as $key => $val) {
// $key yields the complex key directly (array, object, set, or scalar)!
}
The Permissive array Parent Type vs. Explicit Subtypes
RHP achieves the best of both worlds: full backward compatibility for dynamic PHP codebases alongside Rust-grade performance and memory layout optimization.
1. Unified Parent Wrapper (RhpType::Array)
Under the hood, RhpType::Array serves as the permissive parent datatype for all collection variants (list, dict, set, and hashmap):
| Syntax | Compiled Type | Underlying Rust Structure | Type Flexibility |
|---|---|---|---|
$arr = []; |
RhpType::Array |
Dynamic PhpValue::Array |
Dynamic: Freely reassignable across list, dict, set, hashmap |
array $arr = []; |
RhpType::Array |
Unified collection parent | Permissive: Accepts any collection variant interchangeably |
list $arr = []; |
CollectionSubtype::List |
Contiguous Rc<RefCell<Vec<PhpValue>>> |
Strict: Sequential integer-indexed lists only |
dict $arr = []; |
CollectionSubtype::Dict |
Rc<RefCell<IndexMap<String, PhpValue>>> |
Strict: Insertion-ordered string-keyed dictionaries only |
set $arr = []; |
CollectionSubtype::Set |
Rc<RefCell<HashSet<PhpValue>>> |
Strict: Unique value sets only |
hashmap $arr = []; |
CollectionSubtype::HashMap |
Rc<RefCell<HashMap<PhpValue, PhpValue>>> |
Strict: Universal complex key-value maps only |
2. Dynamic Typing via PhpValue & Flexible Reassignment
Local variables initialized without a strict type constraint (or declared with the permissive array type hint) store values dynamically in a unified PhpValue enum handle.
Because they are not rigidly locked to a single data structure type for their entire lifetime, you can easily reassign $arr from a sequential list structure to a dictionary or map structure without throwing a type error:
// Dynamic flexibility on untyped variables:
$arr = [1, 2, 3]; // Sequential list
$arr = ['name' => 'yogesh']; // Seamlessly reassigns to dictionary
// Permissive parent type hint for backward compatibility:
array $data = [1, 2, 3];
$data = ['name' => 'yogesh']; // Allowed
$data = Arr::list([10, 20]); // Allowed
$data = Arr::dict(['env' => 'prod']); // Allowed
3. Strict Type Bounds vs. Dynamic Variables
This dynamic flexibility applies to untyped variables and generic array-hinted variables. However, once a variable or parameter is explicitly type-hinted with a strict collection subtype (like list $arr or dict $arr) or a primitive scalar (like int $x), RHP enforces strict type boundaries and throws a Rust-style compile-time or runtime error on incompatible assignments:
// 1. Strict List rejects string-keyed dictionaries:
list $numbers = [1, 2, 3];
// $numbers = ['name' => 'yogesh'];
// Error: VariableTypeMismatch: Cannot assign value of type 'Dict' to typed variable '$numbers' of type 'List'
// 2. Strict Dict rejects sequential integer lists:
dict $user = ['name' => 'yogesh'];
// $user = [1, 2, 3];
// Error: VariableTypeMismatch: Cannot assign value of type 'List' to typed variable '$user' of type 'Dict'
// 3. Strict scalar primitives reject incompatible types:
int $x = 0;
// $x = 'hello';
// Error: VariableTypeMismatch: Cannot assign value of type 'string' to typed variable '$x' of type 'int'
Immutable Helper Methods by Default
- Immutability by Default: Calling array and collection helper methods (such as
$my_array->sort()orArr::list($my_array)->sort()) never mutates the original array in place. - Zero Hidden Side Effects: Every transformation returns a brand-new collection instance, keeping data flows clean, predictable, and thread-safe.
$original = [3, 1, 2];
// $sorted is a new sorted instance; $original remains [3, 1, 2]
$sorted = $original->sort();
print($original[0]); // 3 (untouched!)
print($sorted[0]); // 1
Explicit Variable Reassignment
If a developer wants to update a variable with the transformed or sorted array, they use explicit reassignment:
$my_array = $my_array->sort();
// or
$my_array = Arr::list($my_array)->sort();
Under the hood, this leverages RHP’s internal Copy-On-Write (COW) and pointer mechanics, making variable replacement lightning-fast without unnecessary memory duplication or garbage collection overhead.
Copy-On-Write (COW) Semantics
To prevent unexpected side effects and eliminate unnecessary heap clones, collections and strings in RHP use Copy-On-Write (COW) semantics:
list $original = Arr::list([1, 2, 3]);
// Shares the underlying buffer with zero memory copy (O(1) pointer reference)
list $copy = $original;
// Modifying $copy detaches it into a new buffer transparently
$copy[] = 4;
print($original->count()); // 3 (untouched)
print($copy->count()); // 4