diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..2a7db6dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[Makefile] +indent_style = tab + +[*.{zig,c,cpp,h,php,json,yml,yaml,md}] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e1ffdcde --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +* text=auto + +*.zig text eol=lf +*.c text eol=lf +*.cpp text eol=lf +*.h text eol=lf +*.php text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.zon text eol=lf +.gitignore text eol=lf +.gitattributes text eol=lf +.editorconfig text eol=lf +Makefile text eol=lf +audit text eol=lf +php text eol=lf + +*.bin binary +*.gz binary +*.phar binary \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac6b5d72..41a5d7f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,26 @@ zig-out/ zig-cache/ .zig-cache/ -test_compress -/*.md -!README.md -.handoff/ docs/book/ + +# Local tooling +.handoff/ .claude/ +.idea/ +.vscode/ +*.code-workspace + +# Local state +.env +.env.* +!.env.example +*.log +*.tmp +*.swp +.DS_Store +Thumbs.db + +# Generated dependencies and test artifacts +**/vendor/ +**/.cache/ +test_compress \ No newline at end of file diff --git a/Makefile b/Makefile index 31f2ad50..43b52ee5 100644 --- a/Makefile +++ b/Makefile @@ -1,60 +1,49 @@ PKG_CONFIG_PATH := /opt/homebrew/opt/mysql-client/lib/pkgconfig:/opt/homebrew/opt/libpq/lib/pkgconfig:/opt/homebrew/opt/openssl@3/lib/pkgconfig:/opt/homebrew/opt/curl/lib/pkgconfig:/opt/homebrew/opt/icu4c@77/lib/pkgconfig:/opt/homebrew/opt/icu4c/lib/pkgconfig:/opt/homebrew/opt/gmp/lib/pkgconfig:/opt/homebrew/opt/gd/lib/pkgconfig:/opt/homebrew/opt/libsodium/lib/pkgconfig:/opt/homebrew/opt/openldap/lib/pkgconfig:$(PKG_CONFIG_PATH) export PKG_CONFIG_PATH -.PHONY: build -build: ## Build zphp (Debug; ~30x slower than release due to Zig's debug allocator stack-trace capture). use `make release` for benchmarking +.PHONY: build release test compat pdo examples bench bench-macro laravel symfony all-tests check docs clean help +build: ## Build the debug binary zig build -.PHONY: release -release: ## Build zphp in ReleaseFast (no debug allocator overhead). prefer for any perf-sensitive run; for testing zphp's actual PHP-execution speed +release: ## Build the optimized binary zig build -Doptimize=ReleaseFast -.PHONY: test -test: ## Run zig unit tests +test: ## Run Zig unit tests zig build test -.PHONY: compat compat: build ## Run PHP compatibility tests (requires PHP 8.4) ./tests/run -.PHONY: pdo pdo: build ## Run PDO driver tests ./tests/pdo_test -.PHONY: examples examples: build ## Run example project tests (requires PHP 8.4) ./tests/examples_test -.PHONY: bench bench: ## Run runtime benchmarks (ReleaseFast) zig build -Doptimize=ReleaseFast ./benchmarks/runtime/run -.PHONY: bench-macro bench-macro: ## Track real-app perf vs php (WordPress + Laravel harnesses, ReleaseFast) zig build -Doptimize=ReleaseFast ./benchmarks/macro/run -.PHONY: laravel laravel: build ## Run Laravel compatibility tests (requires PHP 8.4 + composer) ./tests/laravel/run -.PHONY: symfony symfony: build ## Run Symfony component and serve compatibility tests (requires PHP 8.4 + composer) ./tests/symfony/run ./tests/symfony/serve_run -.PHONY: all-tests all-tests: test compat examples laravel ## Run all tests -.PHONY: docs +check: test compat examples ## Run the standard local verification suite + docs: ## Serve docs locally with live reload mdbook serve docs -.PHONY: clean clean: ## Clean build artifacts rm -rf zig-out .zig-cache -.PHONY: help help: ## Show help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index 9f8295ab..a56b30a5 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,59 @@ # zphp -zphp is a PHP runtime written in Zig with PHP 8.x compatibility. It includes a built-in HTTP server with WebSocket support, TLS, and HTTP/2, database drivers for SQLite, MySQL, and PostgreSQL, and cURL bindings for HTTP client operations. +**A high-performance PHP runtime built in Zig.** + +zphp is an experimental PHP 8.x-compatible runtime focused on **speed, low memory usage, and modern deployment**. It combines a custom runtime with a built-in HTTP server, WebSocket support, TLS, HTTP/2, database drivers, cURL bindings, package management, testing, formatting, and standalone compilation. + +Built with **Zig**, zphp is designed to give PHP workloads lower-level control, reduced overhead, and a more performance-oriented runtime architecture. ```sh -zphp run app.php # run a script -zphp serve app.php --port 8080 # start an HTTP server -zphp build --compile app.php # compile to a standalone executable -zphp test # run tests -zphp fmt src/*.php # format code -zphp install # install packages from composer.json +zphp run app.php +zphp serve app.php --port 8080 +zphp build --compile app.php +zphp test +zphp fmt src/*.php +zphp install ``` -## Quick comparison - -| | PHP | zphp | -|---|---|---| -| Run a script | `php script.php` | `zphp run script.php` | -| HTTP server | php-fpm + nginx | `zphp serve app.php` | -| Install deps | `composer install` | `zphp install` | -| Add a package | `composer require pkg` | `zphp add pkg` | -| Run tests | `phpunit` | `zphp test` | -| Format code | `php-cs-fixer fix` | `zphp fmt` | -| Standalone binary | - | `zphp build --compile app.php` | +## Features + +* PHP 8.x compatibility +* Runtime written in Zig +* Built-in HTTP server +* HTTP/2, TLS, and WebSockets +* SQLite, MySQL, and PostgreSQL +* cURL bindings +* Composer package support +* Standalone executable compilation +* Performance and low-memory focused architecture + +## Comparison + +| Task | PHP | zphp | +| ----------------- | ------------------ | ---------------------- | +| Run script | `php app.php` | `zphp run app.php` | +| HTTP server | PHP-FPM + nginx | `zphp serve app.php` | +| Dependencies | `composer install` | `zphp install` | +| Tests | PHPUnit | `zphp test` | +| Formatting | PHP-CS-Fixer | `zphp fmt` | +| Standalone binary | External tooling | `zphp build --compile` | ## Installation -Download prebuilt binaries from [GitHub Releases](https://github.com/nvms/zphp/releases). See the [documentation](https://nvms.github.io/zphp/) for building from source and detailed guides. +Download builds from [GitHub Releases](https://github.com/nvms/zphp/releases). + +See the [documentation](https://nvms.github.io/zphp/) for build instructions and usage guides. + +## Project Status + +zphp was originally developed and maintained heavily through AI-assisted development. + +The project is now being actively reviewed and hardened with a stronger focus on **security, correctness, memory safety, testing, performance validation, and production readiness**. + +AI may still be used as a development tool, but critical runtime code is expected to be reviewed, tested, and independently verified. + +zphp is still experimental and should be thoroughly tested before production use. --- -This project is an experiment in AI-maintained open source - autonomously built, tested, and refined by AI with human oversight. +**PHP on the surface. Zig at the core. Built for speed.** diff --git a/benchmarks/runtime/array_ops.php b/benchmarks/runtime/array_ops.php index 2e68089d..63c628a4 100644 --- a/benchmarks/runtime/array_ops.php +++ b/benchmarks/runtime/array_ops.php @@ -2,19 +2,16 @@ // array operations - tests array creation, access, manipulation $n = 50000; -// build array $arr = []; for ($i = 0; $i < $n; $i++) { $arr[] = $n - $i; } -// sum $sum = 0; for ($i = 0; $i < $n; $i++) { $sum += $arr[$i]; } -// filter to new array $filtered = []; for ($i = 0; $i < $n; $i++) { if ($arr[$i] % 3 === 0) { @@ -22,7 +19,6 @@ } } -// map $mapped = []; for ($i = 0; $i < count($filtered); $i++) { $mapped[] = $filtered[$i] * 2; diff --git a/benchmarks/runtime/closures.php b/benchmarks/runtime/closures.php index d185bb84..09782567 100644 --- a/benchmarks/runtime/closures.php +++ b/benchmarks/runtime/closures.php @@ -2,7 +2,6 @@ // closure operations - tests closure creation, invocation, captures $n = 50000; -// create and call closures $adders = []; for ($i = 0; $i < 100; $i++) { $adders[] = function ($x) use ($i) { return $x + $i; }; @@ -13,12 +12,10 @@ $sum += $adders[$i % 100]($i); } -// higher-order: array_map with closure $data = range(1, 10000); $squared = array_map(function ($x) { return $x * $x; }, $data); $total = array_sum($squared); -// nested closures function compose(callable $f, callable $g): callable { return function ($x) use ($f, $g) { return $f($g($x)); }; } diff --git a/benchmarks/runtime/loops.php b/benchmarks/runtime/loops.php index ff85e500..7f47075c 100644 --- a/benchmarks/runtime/loops.php +++ b/benchmarks/runtime/loops.php @@ -2,14 +2,12 @@ // tight loop arithmetic - tests raw bytecode execution speed $n = 5000000; -// integer arithmetic loop $sum = 0; for ($i = 0; $i < $n; $i++) { $sum += $i; } echo "$sum\n"; -// nested loops with conditionals $count = 0; for ($i = 0; $i < 2000; $i++) { for ($j = 0; $j < 2000; $j++) { @@ -20,7 +18,6 @@ } echo "$count\n"; -// while loop with mixed ops $x = 1.0; $i = 0; while ($i < 1000000) { diff --git a/benchmarks/runtime/objects.php b/benchmarks/runtime/objects.php index 8d157379..bfc4744b 100644 --- a/benchmarks/runtime/objects.php +++ b/benchmarks/runtime/objects.php @@ -23,18 +23,15 @@ public function add(Point $other): Point { $n = 50000; $points = []; -// create objects for ($i = 0; $i < $n; $i++) { $points[] = new Point($i * 0.1, $i * 0.2); } -// method calls $totalDist = 0.0; for ($i = 1; $i < $n; $i++) { $totalDist += $points[$i]->distanceTo($points[$i - 1]); } -// chained operations $sum = new Point(0, 0); for ($i = 0; $i < 1000; $i++) { $sum = $sum->add($points[$i]); diff --git a/benchmarks/runtime/string_ops.php b/benchmarks/runtime/string_ops.php index af4d1042..a2b6fdf1 100644 --- a/benchmarks/runtime/string_ops.php +++ b/benchmarks/runtime/string_ops.php @@ -8,13 +8,10 @@ $s .= "item" . $i . ","; } -// count occurrences $count = substr_count($s, "item1"); -// replace $replaced = str_replace("item", "elem", $s); -// split and rejoin $parts = explode(",", $s); $joined = implode(";", $parts); diff --git a/build.zig b/build.zig index 159be8ac..03ebaa0a 100644 --- a/build.zig +++ b/build.zig @@ -174,10 +174,10 @@ fn addXxhashShim(b: *std.Build, mod: *std.Build.Module) void { fn addIcuShim(b: *std.Build, mod: *std.Build.Module) void { var flags = std.ArrayList([]const u8){}; defer flags.deinit(b.allocator); - flags.append(b.allocator, "-std=c11") catch {}; + flags.append(b.allocator, "-std=c11") catch return; if (pkgConfigCflagsIncludes(b, "icu-i18n")) |inc| { const flag = std.fmt.allocPrint(b.allocator, "-I{s}", .{inc}) catch return; - flags.append(b.allocator, flag) catch {}; + flags.append(b.allocator, flag) catch return; } mod.addCSourceFile(.{ .file = b.path("src/stdlib/icu_shim.c"), @@ -188,10 +188,10 @@ fn addIcuShim(b: *std.Build, mod: *std.Build.Module) void { // exposed in the C++ API. link libc++ once for the whole module var cpp_flags = std.ArrayList([]const u8){}; defer cpp_flags.deinit(b.allocator); - cpp_flags.append(b.allocator, "-std=c++17") catch {}; + cpp_flags.append(b.allocator, "-std=c++17") catch return; if (pkgConfigCflagsIncludes(b, "icu-i18n")) |inc| { const flag = std.fmt.allocPrint(b.allocator, "-I{s}", .{inc}) catch return; - cpp_flags.append(b.allocator, flag) catch {}; + cpp_flags.append(b.allocator, flag) catch return; } mod.addCSourceFile(.{ .file = b.path("src/stdlib/icu_msg_shim.cpp"), diff --git a/build.zig.zon b/build.zig.zon index 63cb6062..3c104161 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,6 +6,8 @@ .paths = .{ "build.zig", "build.zig.zon", + "LICENSE", + "README.md", "src", }, } diff --git a/examples/api-client/main.php b/examples/api-client/main.php index f181529e..4f272ff0 100644 --- a/examples/api-client/main.php +++ b/examples/api-client/main.php @@ -1,10 +1,4 @@ $resp) { yield $name => $resp->toArray(); @@ -220,7 +213,6 @@ function responseStats(array $responses): Generator { // simulate responses (connection will fail but that exercises error handling) $responses = []; -// test error handling path $r1 = $client->get("http://127.0.0.1:1/api/users"); $responses['users'] = $r1; echo "users ok: " . ($r1->ok() ? 'true' : 'false') . "\n"; @@ -236,38 +228,30 @@ function responseStats(array $responses): Generator { $r3 = $client->get("http://127.0.0.1:1/api/users"); $responses['users_cached'] = $r3; -// render summary echo renderSummary($responses); -// generator stats foreach (responseStats($responses) as $name => $stats) { echo "$name: status={$stats['status']} ok=" . ($stats['ok'] ? 'true' : 'false') . "\n"; } -// logger output $log = $logger->format(); $lines = explode("\n", trim($log)); echo "log entries: " . count($lines) . "\n"; -// verify timezone in log entries $first = $lines[0]; echo "log has timezone: " . (strpos($first, 'EST') !== false || strpos($first, 'EDT') !== false ? 'true' : 'false') . "\n"; -// cache state echo "cache keys: " . count($cache->keys()) . "\n"; -// datetime with timezone $dt = new DateTime("now", new DateTimeZone("America/New_York")); $tz = $dt->getTimezone(); echo "tz: " . $tz->getName() . "\n"; -// serializable interface echo "r1 serializable: " . ($r1 instanceof Exportable ? 'true' : 'false') . "\n"; $arr = $r1->toArray(); echo "toArray has status: " . (isset($arr['status']) ? 'true' : 'false') . "\n"; echo "toArray has ok: " . (isset($arr['ok']) ? 'true' : 'false') . "\n"; -// enum match $levels = [LogLevel::DEBUG, LogLevel::INFO, LogLevel::WARN, LogLevel::ERROR]; foreach ($levels as $l) { $icon = match($l) { @@ -280,7 +264,6 @@ function responseStats(array $responses): Generator { } echo "\n"; -// $argv / $argc echo "argv type: " . gettype($argv) . "\n"; echo "argc type: " . gettype($argc) . "\n"; diff --git a/examples/array-deep-ops/main.php b/examples/array-deep-ops/main.php index 817b6264..72a24ce5 100644 --- a/examples/array-deep-ops/main.php +++ b/examples/array-deep-ops/main.php @@ -1,7 +1,4 @@ ['name' => 'Alice', 'tags' => ['admin']]]; diff --git a/examples/array-pointers/main.php b/examples/array-pointers/main.php index 9b1b388d..cd2b820d 100644 --- a/examples/array-pointers/main.php +++ b/examples/array-pointers/main.php @@ -1,7 +1,5 @@ 'apple', 'b' => 'banana', 'c' => 'cherry', 'd' => 'date']; @@ -50,7 +47,6 @@ echo "after prev - current: " . current($assoc) . "\n"; echo "after prev - key: " . key($assoc) . "\n"; -// --- pointer past end --- echo "\n=== Pointer past end ===\n"; $small = [100, 200]; @@ -66,7 +62,6 @@ reset($small); echo "after reset from past-end: " . current($small) . "\n"; -// --- prev before start --- echo "\n=== Pointer before start ===\n"; $small2 = [100, 200]; @@ -78,7 +73,6 @@ echo "key before start: "; var_dump(key($small2)); -// --- empty array --- echo "\n=== Empty array ===\n"; $empty = []; @@ -95,7 +89,6 @@ echo "end on empty: "; var_dump(end($empty)); -// --- single element --- echo "\n=== Single element ===\n"; $single = ['only' => 42]; @@ -131,7 +124,6 @@ echo "arr1 after reset: " . current($arr1) . "\n"; echo "arr2 unchanged: " . current($arr2) . "\n"; -// --- walking with next --- echo "\n=== Walking with next ===\n"; $walk = ['first', 'second', 'third']; @@ -141,7 +133,6 @@ $val = next($walk); } -// --- pointer after unset --- echo "\n=== Pointer after unset ===\n"; $mod = [10, 20, 30, 40, 50]; @@ -165,7 +156,6 @@ echo "after add - current: " . current($grow) . "\n"; echo "after add - key: " . key($grow) . "\n"; -// --- reset return value --- echo "\n=== Reset and end return values ===\n"; $ret = [100, 200, 300]; diff --git a/examples/attribute-enforcement/main.php b/examples/attribute-enforcement/main.php index 1547a183..c87550a0 100644 --- a/examples/attribute-enforcement/main.php +++ b/examples/attribute-enforcement/main.php @@ -1,8 +1,4 @@ newInstance(); echo "valid newInstance: " . $inst->val . "\n"; -// non-attribute class class NotAnAttr {} #[NotAnAttr] @@ -32,7 +27,6 @@ class MyClass2 {} echo "non-attr: caught\n"; } -// target enforcement #[Attribute(Attribute::TARGET_METHOD)] class MethodOnly { public function __construct(public string $val = '') {} @@ -50,7 +44,6 @@ class MyClass3 {} echo "target: caught\n"; } -// repeatability enforcement #[Attribute] class SingleAttr { public function __construct(public string $val = '') {} @@ -85,7 +78,6 @@ class MyClass5 {} $inst5b = $attrs5[1]->newInstance(); echo "repeatable: " . $inst5a->val . ", " . $inst5b->val . "\n"; -// #[Override] valid usage class Base { public function doStuff(): string { return 'base'; } } diff --git a/examples/attributes-enum-interface-trait/main.php b/examples/attributes-enum-interface-trait/main.php index 5fbba1f6..5d4e50c3 100644 --- a/examples/attributes-enum-interface-trait/main.php +++ b/examples/attributes-enum-interface-trait/main.php @@ -1,10 +1,4 @@ getName() . "\n"; -// --- Interface attributes --- echo "\n=== Interface Attributes ===\n"; @@ -133,7 +122,6 @@ public function touch(): void { // note: getMethod on interfaces requires interface methods in ClassDef.methods // which is tracked separately - test class-level attrs here -// --- Trait attributes --- echo "\n=== Trait Attributes ===\n"; diff --git a/examples/attributes-reflection/main.php b/examples/attributes-reflection/main.php index 88b8887f..e23b5ac8 100644 --- a/examples/attributes-reflection/main.php +++ b/examples/attributes-reflection/main.php @@ -1,13 +1,4 @@ hasMethod('greet') ? 'true' : 'false') . "\n"; echo "hasMethod nonexistent: " . ($ref->hasMethod('nonexistent') ? 'true' : 'false') . "\n"; -// --- methods --- echo "\n=== Methods ===\n"; @@ -98,7 +87,6 @@ public static function fromArray(array $data): self { echo "all methods: " . implode(', ', $methodNames) . "\n"; echo "method count: " . count($methodNames) . "\n"; -// --- method visibility --- echo "\n=== Method Visibility ===\n"; @@ -118,7 +106,6 @@ public static function fromArray(array $data): self { $createM = $ref->getMethod('fromArray'); echo "fromArray isStatic: " . ($createM->isStatic() ? 'true' : 'false') . "\n"; -// --- parameters --- echo "\n=== Parameters ===\n"; @@ -140,7 +127,6 @@ public static function fromArray(array $data): self { echo $line . "\n"; } -// --- properties --- echo "\n=== Properties ===\n"; @@ -163,7 +149,6 @@ public static function fromArray(array $data): self { $pwdProp = $ref->getProperty('password'); echo "password isPrivate: " . ($pwdProp->isPrivate() ? 'true' : 'false') . "\n"; -// --- practical: object inspector --- echo "\n=== Object Inspector ===\n"; @@ -203,7 +188,6 @@ function inspect(object $obj): void { $user = new User(1, 'Alice', 'alice@example.com'); inspect($user); -// --- Attributes --- echo "\n=== Attributes ===\n"; @@ -263,7 +247,6 @@ public function create() { return 'created'; } echo "title attr 0: " . $titleAttrs[0]->getName() . "\n"; echo "title attr 0 arg: " . $titleAttrs[0]->getArguments()[0] . "\n"; -// newInstance $routeAttr = $classAttrs[0]; $routeInstance = $routeAttr->newInstance(); echo "route instance class: " . get_class($routeInstance) . "\n"; diff --git a/examples/autoloader/main.php b/examples/autoloader/main.php index 3524fcc3..75c62dce 100644 --- a/examples/autoloader/main.php +++ b/examples/autoloader/main.php @@ -1,6 +1,4 @@ ), foreach references (&$entry), first-class callable syntax, -// compact(), constructor property promotion, enums (eviction policy), -// array destructuring with keys, match expressions, spread operator, -// pass-by-reference (&$stats), heredoc, static methods enum EvictionPolicy: string { case LRU = 'lru'; @@ -136,7 +130,6 @@ private function evict(): void { } } - // ArrayAccess public function offsetExists(mixed $offset): bool { return $this->has($offset); } @@ -153,7 +146,6 @@ public function offsetUnset(mixed $offset): void { $this->delete($offset); } - // Countable public function count(): int { return count($this->entries); } @@ -213,7 +205,6 @@ public function collectStats(): array { } } -// --- basic set/get --- $cache = new Cache(maxSize: 5, policy: EvictionPolicy::LRU, defaultTtl: 3600); @@ -227,7 +218,6 @@ public function collectStats(): array { echo $cache->get('missing', 'default') . "\n"; echo "count: " . count($cache) . "\n"; -// --- ArrayAccess --- $cache['color'] = 'blue'; echo $cache['color'] . "\n"; @@ -236,7 +226,6 @@ public function collectStats(): array { echo "after unset: " . (isset($cache['color']) ? 'yes' : 'no') . "\n"; echo "count: " . count($cache) . "\n"; -// --- serialize complex values --- $data = ['nested' => ['a' => 1, 'b' => [2, 3]], 'flag' => true, 'nothing' => null]; $cache->set('complex', $data); @@ -246,7 +235,6 @@ public function collectStats(): array { echo "flag: " . ($retrieved['flag'] ? 'true' : 'false') . "\n"; echo "nothing: " . ($retrieved['nothing'] === null ? 'null' : 'other') . "\n"; -// --- eviction (LRU) --- $lru = new Cache(maxSize: 3, policy: EvictionPolicy::LRU); $lru->set('a', 1); @@ -259,7 +247,6 @@ public function collectStats(): array { echo "b evicted: " . ($lru->has('b') ? 'no' : 'yes') . "\n"; echo "d exists: " . ($lru->has('d') ? 'yes' : 'no') . "\n"; -// --- eviction (FIFO) --- $fifo = new Cache(maxSize: 3, policy: EvictionPolicy::FIFO); $fifo->set('x', 10); @@ -271,7 +258,6 @@ public function collectStats(): array { echo "x evicted fifo: " . ($fifo->has('x') ? 'no' : 'yes') . "\n"; echo "z survived fifo: " . ($fifo->has('z') ? 'yes' : 'no') . "\n"; -// --- generator iteration --- $cache->set('g1', 'alpha'); $cache->set('g2', 'beta'); @@ -284,14 +270,12 @@ public function collectStats(): array { sort($items); echo "entries: " . implode(', ', $items) . "\n"; -// --- pass-by-reference stats --- $h = 0; $m = 0; $cache->stats($h, $m); echo "hits: $h, misses: $m\n"; -// --- nullsafe operator --- $group = new CacheGroup(); $group->add('main', $cache); @@ -301,7 +285,6 @@ public function collectStats(): array { $nope = $group->find('nonexistent')?->getPolicy(); echo "null policy: " . ($nope === null ? 'null' : 'other') . "\n"; -// --- enum features --- echo EvictionPolicy::LRU->value . "\n"; echo EvictionPolicy::FIFO->label() . "\n"; @@ -323,7 +306,6 @@ public function collectStats(): array { echo "group $name: $cnt items, $pol\n"; } -// --- heredoc --- $cacheName = 'main'; $cacheCount = count($cache); @@ -334,7 +316,6 @@ public function collectStats(): array { REPORT; echo $report . "\n"; -// --- DateTime/strtotime --- $base = 1750000000; $expiry = strtotime("+1 hour", $base); @@ -345,7 +326,6 @@ public function collectStats(): array { $dt->modify('+30 minutes'); echo "modified: " . $dt->format('H:i') . "\n"; -// --- compact --- $status = 'active'; $size = 5; diff --git a/examples/calculator/main.php b/examples/calculator/main.php index df5e0e41..91efb5d1 100644 --- a/examples/calculator/main.php +++ b/examples/calculator/main.php @@ -1,13 +1,5 @@ prop}", -// "{$arr['key']}"), multi-level inheritance (grandparent->parent->child), -// type juggling (string to number, loose comparison), switch with fallthrough, -// nested ternary, str_split, substr, ctype_digit, ctype_alpha, ctype_space, -// is_numeric, intval, floatval, number_format, abs, pow, sqrt, max, min, -// array_reverse, array_key_exists, sprintf, compact, recursive array processing -// --- token types --- class Token { @@ -26,7 +18,6 @@ public function __toString(): string } } -// --- lexer --- class Lexer { @@ -260,7 +251,6 @@ public function display(): string } } -// --- recursive descent parser --- class Parser { @@ -293,7 +283,6 @@ private function eat(string $type): Token return $tok; } - // expression: term ((+|-) term)* private function parseExpression(): AstNode { $node = $this->parseTerm(); @@ -305,7 +294,6 @@ private function parseExpression(): AstNode return $node; } - // term: power ((*|/|%) power)* private function parseTerm(): AstNode { $node = $this->parsePower(); @@ -317,7 +305,6 @@ private function parseTerm(): AstNode return $node; } - // power: unary (^ unary)* private function parsePower(): AstNode { $node = $this->parseUnary(); @@ -329,7 +316,6 @@ private function parsePower(): AstNode return $node; } - // unary: -unary | primary private function parseUnary(): AstNode { if ($this->current()->type === "op" && $this->current()->value === "-") { @@ -383,7 +369,6 @@ private function parsePrimary(): AstNode } } -// --- evaluator helper --- function calc(string $expr, array $env = []): string { @@ -398,7 +383,6 @@ function calc(string $expr, array $env = []): string return number_format($result, 6, ".", ""); } -// --- recursive helpers --- function factorial(int $n): int { @@ -419,7 +403,6 @@ function flattenArray(array $arr): array return $result; } -// === test: basic arithmetic === echo "1+2: " . calc("1 + 2") . "\n"; echo "10-3*2: " . calc("10 - 3 * 2") . "\n"; @@ -428,7 +411,6 @@ function flattenArray(array $arr): array echo "10%3: " . calc("10 % 3") . "\n"; echo "-5+3: " . calc("-5 + 3") . "\n"; -// === test: function calls === echo "sqrt(144): " . calc("sqrt(144)") . "\n"; echo "abs(-42): " . calc("abs(-42)") . "\n"; @@ -446,7 +428,6 @@ function flattenArray(array $arr): array echo "x+y: " . calc("x + y", ["x" => 10, "y" => 20]) . "\n"; echo "x^2+1: " . calc("x ^ 2 + 1", ["x" => 5]) . "\n"; -// === test: complex expressions === echo "nested: " . calc("(1 + 2) * (3 + 4) / (5 - 3)") . "\n"; echo "deep: " . calc("sqrt(pow(3, 2) + pow(4, 2))") . "\n"; @@ -464,7 +445,6 @@ function flattenArray(array $arr): array $strs = array_map(function ($t) { return (string) $t; }, $tokens); echo "tokens: " . implode(" ", $strs) . "\n"; -// === test: error handling === try { calc("1 + + 2"); @@ -478,7 +458,6 @@ function flattenArray(array $arr): array echo "error: " . $e->getMessage() . "\n"; } -// === test: recursive functions === echo "5!: " . factorial(5) . "\n"; echo "10!: " . factorial(10) . "\n"; @@ -489,7 +468,6 @@ function flattenArray(array $arr): array $flat = flattenArray($nested); echo "flat: " . implode(", ", $flat) . "\n"; -// === test: nested closures === function makeAdder(int $n): Closure { @@ -503,7 +481,6 @@ function makeAdder(int $n): Closure echo "add5(3): " . $add5(3) . "\n"; echo "add10(3): " . $add10(3) . "\n"; -// closure returning closure function multiplierFactory(): Closure { return function (int $factor) { @@ -518,7 +495,6 @@ function multiplierFactory(): Closure echo "double(7): " . $double(7) . "\n"; echo "triple(7): " . $triple(7) . "\n"; -// === test: type juggling === echo "str+num: " . ("5" + 3) . "\n"; echo "str*num: " . ("4" * "3") . "\n"; diff --git a/examples/closure-binding/main.php b/examples/closure-binding/main.php index 660b209d..85d98cc0 100644 --- a/examples/closure-binding/main.php +++ b/examples/closure-binding/main.php @@ -1,7 +1,4 @@ dump(); -// macro system using Closure::bind class Collection { public array $items; public static array $macros = []; @@ -105,7 +101,6 @@ public function __call(string $name, array $args): mixed { echo "has 20: " . ($nums->contains(20) ? "yes" : "no") . "\n"; echo "has 99: " . ($nums->contains(99) ? "yes" : "no") . "\n"; -// bindTo for middleware-style pattern class Request { public string $method; public string $path; @@ -170,13 +165,11 @@ public function __construct(array $data) { echo $reader->call($config, "port") . "\n"; echo $reader->call($config, "missing") . "\n"; -// Closure::fromCallable function double(int $n): int { return $n * 2; } $fn = Closure::fromCallable('double'); echo $fn(21) . "\n"; -// captured vars preserved across rebinding $multiplier = 3; $multiply = function(int $n) use ($multiplier) { return $n * $multiplier + count($this->items); diff --git a/examples/closure-introspection/main.php b/examples/closure-introspection/main.php index 3d96b9dc..4905aaf6 100644 --- a/examples/closure-introspection/main.php +++ b/examples/closure-introspection/main.php @@ -1,7 +1,4 @@ getClosure(), -// static closures, $this-less arrow funcs, closure use vs auto-capture class Wallet { private int $cents = 0; diff --git a/examples/closures-advanced/main.php b/examples/closures-advanced/main.php index 2549a69b..645011d1 100644 --- a/examples/closures-advanced/main.php +++ b/examples/closures-advanced/main.php @@ -1,12 +1,5 @@ $x * 2; @@ -99,7 +89,6 @@ public function cube(int $n): int { $addOneThenTriple = $compose($timesThree, $addOne); echo "compose(triple, addOne)(4): " . $addOneThenTriple(4) . "\n"; -// --- first-class callable syntax --- echo "\n=== First-Class Callable Syntax ===\n"; $strlen = strlen(...); @@ -127,7 +116,6 @@ public static function lower(string $s): string { $lowerFn = Formatter::lower(...); echo "lower('WORLD'): " . $lowerFn('WORLD') . "\n"; -// --- recursive closures --- echo "\n=== Recursive Closures ===\n"; $factorial = null; @@ -202,7 +190,6 @@ function pipeline(array $fns): Closure { ]); echo "pipeline(5): " . $transform(5) . "\n"; -// --- static closures --- echo "\n=== Static Closures ===\n"; $static = static function(int $a, int $b): int { @@ -213,7 +200,6 @@ function pipeline(array $fns): Closure { $staticArrow = static fn(int $x) => $x * $x; echo "static square(9): " . $staticArrow(9) . "\n"; -// --- array_reduce and array_walk --- echo "\n=== array_reduce and array_walk ===\n"; $nums = [1, 2, 3, 4, 5]; @@ -238,7 +224,6 @@ function pipeline(array $fns): Closure { } echo implode(", ", $parts) . "\n"; -// --- variable function calls --- echo "\n=== Variable Function Calls ===\n"; $fn = 'strlen'; @@ -250,7 +235,6 @@ function pipeline(array $fns): Closure { $fn = 'array_sum'; echo "sum([1,2,3]): " . $fn([1, 2, 3]) . "\n"; -// --- is_callable checks --- echo "\n=== is_callable Checks ===\n"; echo "closure: " . (is_callable(function() {}) ? "yes" : "no") . "\n"; diff --git a/examples/collection-pipeline/main.php b/examples/collection-pipeline/main.php index d484539c..8788e668 100644 --- a/examples/collection-pipeline/main.php +++ b/examples/collection-pipeline/main.php @@ -1,12 +1,5 @@ 1.50, 'banana' => 0.75, 'cherry' => 2.00]; array_walk($prices, function(&$price, $key) { @@ -36,7 +28,6 @@ echo "$item: $price\n"; } -// test 3: sorting functions echo "\n=== Test 3: Sorting ===\n"; $data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]; usort($data, fn($a, $b) => $a - $b); @@ -56,7 +47,6 @@ foreach ($assoc as $k => $v) echo "$k=$v "; echo "\n"; -// test 4: array_combine and array_column echo "\n=== Test 4: Combine and Column ===\n"; $keys = ['name', 'age', 'city']; $values = ['Alice', 30, 'NYC']; @@ -76,7 +66,6 @@ $byName = array_column($people, null, 'name'); echo "Bob's age: " . $byName['Bob']['age'] . "\n"; -// test 5: compact and extract echo "\n=== Test 5: Compact/Extract ===\n"; $name = "Alice"; $age = 30; @@ -90,7 +79,6 @@ extract($record); echo "Extract: color=$color, size=$size, count=$count\n"; -// test 6: set operations echo "\n=== Test 6: Set Operations ===\n"; $a = [1, 2, 3, 4, 5]; $b = [3, 4, 5, 6, 7]; @@ -99,7 +87,6 @@ echo "Diff (b-a): " . implode(', ', array_diff($b, $a)) . "\n"; echo "Union: " . implode(', ', array_unique(array_merge($a, $b))) . "\n"; -// test 7: array manipulation echo "\n=== Test 7: Array Manipulation ===\n"; $arr = [1, 2, 3, 4, 5]; echo "Reverse: " . implode(', ', array_reverse($arr)) . "\n"; @@ -114,7 +101,6 @@ echo "Pad to 8: " . implode(', ', array_pad($arr, 8, 0)) . "\n"; echo "Fill(0,5,x): " . implode(', ', array_fill(0, 5, 'x')) . "\n"; -// test 8: search and count echo "\n=== Test 8: Search and Count ===\n"; $fruits = ['apple', 'banana', 'cherry', 'banana', 'date', 'banana']; echo "Has banana: " . (in_array('banana', $fruits) ? 'yes' : 'no') . "\n"; @@ -126,7 +112,6 @@ $counts = array_count_values($fruits); echo "Banana count: " . $counts['banana'] . "\n"; -// test 9: flip and unique echo "\n=== Test 9: Flip and Unique ===\n"; $map = ['a' => 1, 'b' => 2, 'c' => 3]; $flipped = array_flip($map); @@ -137,7 +122,6 @@ $dupes = [1, 2, 2, 3, 3, 3, 4]; echo "Unique: " . implode(', ', array_unique($dupes)) . "\n"; -// test 10: keys and values echo "\n=== Test 10: Keys/Values ===\n"; $data = ['x' => 10, 'y' => 20, 'z' => 30]; echo "Keys: " . implode(', ', array_keys($data)) . "\n"; @@ -162,7 +146,6 @@ ); echo implode(', ', $labels) . "\n"; -// test 13: complex pipeline echo "\n=== Test 13: Complex Pipeline ===\n"; $students = [ ['name' => 'Alice', 'grade' => 92], @@ -180,7 +163,6 @@ $avg = array_sum(array_column($students, 'grade')) / count($students); echo "Class average: " . round($avg, 1) . "\n"; -// test 14: splice echo "\n=== Test 14: Splice ===\n"; $arr = ['a', 'b', 'c', 'd', 'e']; $removed = array_splice($arr, 1, 2, ['x', 'y', 'z']); diff --git a/examples/color-converter/main.php b/examples/color-converter/main.php index 51930469..804222e0 100644 --- a/examples/color-converter/main.php +++ b/examples/color-converter/main.php @@ -1,5 +1,4 @@ = 2) { $first = $value[0]; $last = $value[strlen($value) - 1]; @@ -37,7 +35,6 @@ function parseIni(string $content): array { } } - // type coercion if (strtolower($value) === 'true' || strtolower($value) === 'on' || strtolower($value) === 'yes') { $value = true; } elseif (strtolower($value) === 'false' || strtolower($value) === 'off' || strtolower($value) === 'no') { @@ -139,7 +136,6 @@ function deepMerge(array $base, array $override): array { echo " cache.ttl: " . $merged['cache']['ttl'] . "\n"; echo " cache.driver: " . $merged['cache']['driver'] . "\n"; -// --- config path accessor --- function configGet(array $config, string $path, $default = null) { $keys = explode('.', $path); @@ -178,7 +174,6 @@ function configSet(array $config, string $path, $value): array { echo " database.pool_size: " . configGet($merged, 'database.pool_size') . "\n"; echo " new.nested.value: " . configGet($merged, 'new.nested.value') . "\n"; -// --- config validation --- function validateConfig(array $config, array $schema): array { $errors = []; @@ -250,7 +245,6 @@ function validateConfig(array $config, array $schema): array { echo " $err\n"; } -// --- compact/extract --- $host = 'localhost'; $port = 5432; diff --git a/examples/config-loader/main.php b/examples/config-loader/main.php index dce35c16..dc6fe6cb 100644 --- a/examples/config-loader/main.php +++ b/examples/config-loader/main.php @@ -1,11 +1,4 @@ "myapp", "debug" => false]; $override = ["debug" => true, "version" => "1.0"]; @@ -213,7 +203,6 @@ function joinAll(string $sep, string ...$parts): string $items = ["a", "b", "c"]; echo "spread call: " . joinAll("-", ...$items) . "\n"; -// === test: named arguments --- function formatEntry(string $key, string $value, string $separator = ": "): string { @@ -239,7 +228,6 @@ function formatEntry(string $key, string $value, string $separator = ": "): stri $settings["theme"] ??= "light"; echo "nullish: " . $settings["theme"] . "\n"; -// === test: clone === $original = new Config(); $original->set("key1", "val1"); @@ -251,7 +239,6 @@ function formatEntry(string $key, string $value, string $separator = ": "): stri echo "original: " . $original->get("key1") . "\n"; echo "cloned: " . $cloned->get("key1") . "\n"; -// === test: type casting === $cv = new ConfigValue("port", "8080"); echo "readonly: " . $cv->key . " type=" . $cv->type . "\n"; @@ -261,7 +248,6 @@ function formatEntry(string $key, string $value, string $separator = ": "): stri $cv2 = new ConfigValue("flag", "1"); echo "cast bool: " . ($cv2->cast("bool") ? "true" : "false") . "\n"; -// === test: serialize/unserialize === $data = ["host" => "localhost", "port" => 5432, "debug" => true]; $serialized = serialize($data); @@ -275,13 +261,11 @@ function formatEntry(string $key, string $value, string $separator = ": "): stri $vars = ["name" => "World", "place" => "zphp"]; echo interpolate($template, $vars) . "\n"; -// === test: static variables === echo generateId() . "\n"; echo generateId() . "\n"; echo generateId() . "\n"; -// === test: class constants === echo "separator: " . Config::SEPARATOR . "\n"; echo "max depth: " . Config::MAX_DEPTH . "\n"; @@ -297,13 +281,11 @@ function formatEntry(string $key, string $value, string $separator = ": "): stri $resolved = $config2->resolve(); echo "resolved: host=" . $resolved["host"] . " port=" . $resolved["port"] . " timeout=" . $resolved["timeout"] . " debug=" . ($resolved["debug"] ? "yes" : "no") . "\n"; -// === test: date/time === $ts = mktime(14, 30, 0, 6, 15, 2025); echo "date: " . date("Y-m-d", $ts) . "\n"; echo "time: " . date("H:i:s", $ts) . "\n"; -// === test: sprintf === echo sprintf("config has %d entries, version %s\n", 7, "1.0"); diff --git a/examples/config-merge/main.php b/examples/config-merge/main.php index 8536efb6..4dccba1b 100644 --- a/examples/config-merge/main.php +++ b/examples/config-merge/main.php @@ -1,8 +1,4 @@ [ @@ -54,7 +50,6 @@ 'debug' => true, ]; -// array_replace_recursive: deep merge configs $config = array_replace_recursive($defaults, $environment, $overrides); echo "=== merged config ===\n"; @@ -81,12 +76,10 @@ function printConfig(array $config, string $prefix = ''): void echo "\n=== database config only ===\n"; printConfig($dbOnly); -// array_diff_key: everything except database $rest = array_diff_key($config, $dbKeys); echo "\n=== non-database config ===\n"; printConfig($rest); -// compact/extract $host = $config['database']['host']; $port = $config['database']['port']; $driver = $config['cache']['driver']; @@ -98,13 +91,11 @@ function printConfig(array $config, string $prefix = ''): void echo " $k = $v\n"; } -// extract into scope $data = ['app_name' => 'MyApp', 'version' => '2.1.0', 'env' => 'production']; extract($data); echo "\n=== extract ===\n"; echo " $app_name v$version ($env)\n"; -// array_walk_recursive: collect all leaf values $leaves = []; array_walk_recursive($config, function ($value) use (&$leaves) { $leaves[] = $value; @@ -120,7 +111,6 @@ function printConfig(array $config, string $prefix = ''): void echo " author: " . implode(', ', $merged['meta']['author']) . "\n"; echo " year: " . $merged['meta']['year'] . "\n"; -// verify key preservation through operations $original_keys = array_keys($defaults); $merged_keys = array_keys($config); sort($original_keys); diff --git a/examples/control-flow-nesting/main.php b/examples/control-flow-nesting/main.php index f0fd0d33..b9af6c8f 100644 --- a/examples/control-flow-nesting/main.php +++ b/examples/control-flow-nesting/main.php @@ -1,7 +1,4 @@ " . strlen($binary) . " bytes -> " . bin2hex($binary) . "\n"; -// hmac signing echo "\n=== HMAC signing ===\n"; $secret = "my-secret-key-2024"; $payloads = [ @@ -59,7 +52,6 @@ echo "key1 vs key2: " . ($sigs[0] === $sigs[1] ? 'same' : 'different') . "\n"; echo "key1 vs key1: " . ($sigs[0] === $sigs[2] ? 'same' : 'different') . "\n"; -// hash_equals: timing-safe comparison echo "\n=== hash_equals (timing-safe) ===\n"; $known_hash = hash('sha256', 'correct-password'); @@ -80,10 +72,8 @@ $label, $expected ? 'true' : 'false', $result ? 'true' : 'false', $status); } -// different lengths always return false echo " length mismatch: " . (hash_equals("abc", "ab") ? 'true' : 'false') . "\n"; -// webhook signature verification pattern echo "\n=== webhook verification ===\n"; function verifyWebhook(string $payload, string $signature, string $secret): bool { $expected = hash_hmac('sha256', $payload, $secret); @@ -128,12 +118,10 @@ function validateToken(string $token, string $secret): array { echo "valid: " . ($check['valid'] ? 'yes' : 'no') . "\n"; echo "user: " . $check['user'] . "\n"; -// tamper with token $tampered = str_replace("user_42", "user_99", $token); $check2 = validateToken($tampered, $secret); echo "tampered valid: " . ($check2['valid'] ? 'yes' : 'no') . "\n"; -// checksum verification echo "\n=== data integrity ===\n"; $files = [ 'config.json' => '{"db":"localhost","port":5432}', diff --git a/examples/csv-edge/main.php b/examples/csv-edge/main.php index ff7d560c..ad871670 100644 --- a/examples/csv-edge/main.php +++ b/examples/csv-edge/main.php @@ -1,7 +1,4 @@ 0, '$75K-$100K' => 0, 'Over $100K' => 0]; foreach ($rows as $row) { @@ -98,7 +93,6 @@ echo " " . str_pad($band, 12) . " $bar ($count)\n"; } -// formatted table echo "\nFull Roster:\n"; echo sprintf(" %-10s %-14s %10s %6s\n", 'Name', 'Department', 'Salary', 'Years'); echo " " . str_repeat('-', 42) . "\n"; diff --git a/examples/csv-streaming/main.php b/examples/csv-streaming/main.php index dbacc92a..b16b2f73 100644 --- a/examples/csv-streaming/main.php +++ b/examples/csv-streaming/main.php @@ -1,7 +1,4 @@ 0 ? "string" : "missing"); diff --git a/examples/data-export/main.php b/examples/data-export/main.php index 513fb955..ca066a3f 100644 --- a/examples/data-export/main.php +++ b/examples/data-export/main.php @@ -1,14 +1,8 @@ 1, 'name' => 'Alice Chen', 'department' => 'Engineering', 'salary' => 125000, 'start_date' => '2020-03-15'], ['id' => 2, 'name' => 'Bob Kumar', 'department' => 'Marketing', 'salary' => 95000, 'start_date' => '2021-07-01'], @@ -20,7 +14,6 @@ ['id' => 8, 'name' => 'Hank Brown', 'department' => 'Engineering', 'salary' => 118000, 'start_date' => '2022-08-01'], ]; -// write CSV echo "=== CSV export ===\n"; $csv_file = "$tmp/employees.csv"; $fp = fopen($csv_file, 'w'); @@ -35,7 +28,6 @@ echo "size: " . $info['size'] . " bytes\n"; echo "rows: " . count($employees) . " data + 1 header\n"; -// read CSV back and verify echo "\n=== CSV import ===\n"; $fp = fopen($csv_file, 'r'); $headers = fgetcsv($fp); @@ -55,7 +47,6 @@ echo "imported " . count($imported) . " rows\n"; echo "roundtrip match: " . (count($imported) === count($employees) ? 'yes' : 'no') . "\n"; -// department summary report echo "\n=== department summary ===\n"; $dept_stats = []; foreach ($employees as $emp) { @@ -84,7 +75,6 @@ number_format($stats['max_salary'], 0, '.', ',')); } -// total $total_salary = array_sum(array_column($employees, 'salary')); $avg_salary = round($total_salary / count($employees)); echo " " . str_repeat("-", 72) . "\n"; @@ -111,7 +101,6 @@ } fclose($fp); -// multi-file export report echo "\n=== export summary ===\n"; $files = [$csv_file, $rank_file]; foreach ($files as $f) { @@ -119,7 +108,6 @@ echo sprintf(" %-30s %6d bytes\n", basename($f), $s['size']); } -// generate a fixed-width report echo "\n=== fixed-width report ===\n"; $report_file = "$tmp/report.txt"; $fp = fopen($report_file, 'w'); @@ -142,7 +130,6 @@ echo file_get_contents($report_file); -// cleanup foreach ($files as $f) unlink($f); unlink($report_file); rmdir($tmp); diff --git a/examples/data-pipeline/main.php b/examples/data-pipeline/main.php index c21191b9..8410ce74 100644 --- a/examples/data-pipeline/main.php +++ b/examples/data-pipeline/main.php @@ -1,16 +1,4 @@ floatval($r[$field] ?? 0), $rows); $count = count($values); @@ -195,7 +182,6 @@ function summarize(array $rows, string $field): Generator { yield 'avg' => round(array_sum($values) / $count, 2); } -// format table using output buffering function formatTable(array $rows, array $columns): string { if (count($rows) === 0) return "(empty)\n"; @@ -211,7 +197,6 @@ function formatTable(array $rows, array $columns): string { } ob_start(); - // header $parts = []; foreach ($columns as $col) { $parts[] = str_pad($col, $widths[$col]); @@ -222,7 +207,6 @@ function formatTable(array $rows, array $columns): string { $parts[] = str_repeat("-", $widths[$col]); } echo implode("-+-", $parts) . "\n"; - // rows foreach ($rows as $row) { $parts = []; foreach ($columns as $col) { @@ -233,7 +217,6 @@ function formatTable(array $rows, array $columns): string { return ob_get_clean(); } -// --- test data --- $csv = "name, age, score, department Alice, 28, 92.5, engineering Bob, 34, 87.3, marketing @@ -246,7 +229,6 @@ function formatTable(array $rows, array $columns): string { Ivy, 33, 84.6, marketing Jack, 38, 77.8, sales"; -// parse and apply schema $schema = (new Schema()) ->add(new Column('name', DataType::STRING)) ->add(new Column('age', DataType::INT)) @@ -281,13 +263,11 @@ function formatTable(array $rows, array $columns): string { echo "--- engineering (sorted by score, graded) ---\n"; echo formatTable($result, ['name', 'age', 'score', 'department']); -// stats on all rows echo "--- score stats (all departments) ---\n"; foreach (summarize($rows, 'score') as $stat => $value) { echo " $stat: $value\n"; } -// group by department using array_reduce $grouped = array_reduce($rows, function($acc, $row) { $dept = $row['department']; if (!isset($acc[$dept])) $acc[$dept] = []; @@ -301,7 +281,6 @@ function formatTable(array $rows, array $columns): string { echo " $dept: " . implode(", ", $names) . "\n"; } -// array operations $names = array_column($rows, 'name'); echo "\nnames: " . implode(", ", $names) . "\n"; @@ -315,7 +294,6 @@ function formatTable(array $rows, array $columns): string { sort($unique_depts); echo "departments: " . implode(", ", $unique_depts) . "\n"; -// test compact/extract $total = count($rows); $avg_age = round(array_sum($ages) / $total); $summary = compact('total', 'avg_age'); @@ -324,23 +302,19 @@ function formatTable(array $rows, array $columns): string { extract($summary); echo "extract: total=$total avg_age=$avg_age\n"; -// list destructuring [$first, $second] = $rows; echo "first: {$first['name']}, second: {$second['name']}\n"; -// array_walk $scores_formatted = array_column($rows, 'score'); array_walk($scores_formatted, function(&$v) { $v = number_format($v, 1); }); echo "scores: " . implode(", ", $scores_formatted) . "\n"; -// array_chunk $chunks = array_chunk($names, 3); echo "chunks: " . count($chunks) . "\n"; echo "chunk[0]: " . implode(", ", $chunks[0]) . "\n"; -// string operations on names $upper_names = array_map('strtoupper', array_slice($names, 0, 3)); echo "upper: " . implode(", ", $upper_names) . "\n"; diff --git a/examples/data-structures/main.php b/examples/data-structures/main.php index 77050225..4f9c6665 100644 --- a/examples/data-structures/main.php +++ b/examples/data-structures/main.php @@ -1,7 +1,5 @@ size() . "\n"; echo " remaining: " . implode(', ', $stack->toArray()) . "\n"; -// --- queue (FIFO) --- class Queue { private array $items = []; @@ -93,7 +90,6 @@ public function toArray(): array { echo " dequeue: " . $queue->dequeue() . "\n"; echo " size: " . $queue->size() . "\n"; -// --- priority queue --- class PriorityQueue { private array $items = []; @@ -128,7 +124,6 @@ public function size(): int { echo " " . $pq->extract() . "\n"; } -// --- linked list --- class ListNode { public $value; @@ -229,7 +224,6 @@ public function reverse(): void { echo " removeFirst: " . $list->removeFirst() . "\n"; echo " items: " . implode(' -> ', $list->toArray()) . "\n"; -// --- ring buffer --- class RingBuffer { private array $buffer; @@ -288,7 +282,6 @@ public function toArray(): array { echo " read: " . $ring->read() . "\n"; echo " remaining: " . implode(', ', $ring->toArray()) . "\n"; -// --- trie --- class TrieNode { public array $children = []; @@ -370,7 +363,6 @@ private function collectWords(TrieNode $node, string $prefix, array &$words): vo echo " words with 'app': " . implode(', ', $trie->wordsWithPrefix('app')) . "\n"; echo " words with 'b': " . implode(', ', $trie->wordsWithPrefix('b')) . "\n"; -// --- array utility functions --- echo "\nArray utilities:\n"; diff --git a/examples/data-uri/main.php b/examples/data-uri/main.php index 0ecec1ec..eb17a818 100644 --- a/examples/data-uri/main.php +++ b/examples/data-uri/main.php @@ -1,7 +1,4 @@ y/$diff->m/$diff->d/$diff->h/$diff->i/$diff->s\n"; echo "invert (start>end): " . $diff->invert . "\n"; -// reverse should invert $diff2 = $end->diff($start); echo "reverse invert: " . $diff2->invert . "\n"; diff --git a/examples/date-time/main.php b/examples/date-time/main.php index 1a9fbd2a..df05bb76 100644 --- a/examples/date-time/main.php +++ b/examples/date-time/main.php @@ -1,9 +1,5 @@ get('Logger'); $log->log('INFO', 'Container initialized'); @@ -153,20 +140,16 @@ public function has(string $name): bool { $c2 = $container->get('Cache'); echo " cache singleton check: " . ($c2->get('greeting') === 'hello from DI' ? 'ok' : 'fail') . "\n"; -// verify singleton identity echo " same instance: " . (spl_object_id($c1) === spl_object_id($c2) ? 'yes' : 'no') . "\n"; -// dispatch an event $dispatcher = $container->get('EventDispatcher'); $dispatcher->dispatch('user.login', ['user' => 'admin']); -// container has checks echo "\n=== container has ===\n"; echo "has Logger: " . ($container->has('Logger') ? 'yes' : 'no') . "\n"; echo "has Cache: " . ($container->has('Cache') ? 'yes' : 'no') . "\n"; echo "has Database: " . ($container->has('Database') ? 'yes' : 'no') . "\n"; -// get_class on resolved instances echo "\n=== resolved types ===\n"; $services = ['Logger', 'Cache', 'EventDispatcher']; foreach ($services as $name) { @@ -174,7 +157,6 @@ public function has(string $name): bool { echo sprintf(" %-20s -> %s\n", $name, get_class($instance)); } -// method_exists on resolved instances echo "\n=== method checks ===\n"; echo "Logger->log: " . (method_exists($log, 'log') ? 'yes' : 'no') . "\n"; echo "Logger->get: " . (method_exists($log, 'get') ? 'yes' : 'no') . "\n"; diff --git a/examples/di-container/main.php b/examples/di-container/main.php index ff969dee..38fd02ad 100644 --- a/examples/di-container/main.php +++ b/examples/di-container/main.php @@ -1,10 +1,4 @@ bind('Logger', 'ConsoleLogger'); $container->singleton('Database', 'Database'); -// resolve a complex dependency tree $repo = $container->make('UserRepository'); echo $repo->find(42) . "\n"; -// singleton check - same instance $db1 = $container->make('Database'); $db2 = $container->make('Database'); echo ($db1 === $db2 ? "same" : "different") . " instance\n"; -// reflection introspection $rc = new ReflectionClass('UserRepository'); echo "class: " . $rc->getName() . "\n"; echo "methods: " . count($rc->getMethods()) . "\n"; @@ -134,7 +124,6 @@ private function build(string $concrete): object { echo "constructor declaring class: " . $ctor->getDeclaringClass()->getName() . "\n"; echo "constructor required params: " . $ctor->getNumberOfRequiredParameters() . "\n"; -// ReflectionFunction function greet(string $name, string $greeting = "Hello"): string { return "$greeting, $name!"; } @@ -156,7 +145,6 @@ function greet(string $name, string $greeting = "Hello"): string { echo "\n"; } -// parent class and interface checks $drc = new ReflectionClass('Database'); echo "Database parent: " . ($drc->getParentClass() === false ? "none" : $drc->getParentClass()->getName()) . "\n"; diff --git a/examples/diff-tool/main.php b/examples/diff-tool/main.php index f59c978b..ad477a8c 100644 --- a/examples/diff-tool/main.php +++ b/examples/diff-tool/main.php @@ -1,8 +1,4 @@ $context * 2) { - // close hunk, trim trailing context $trim = $idle - $context; $cur['ops'] = array_slice($cur['ops'], 0, count($cur['ops']) - $trim); $hunks[] = $cur; diff --git a/examples/dom-mutation/main.php b/examples/dom-mutation/main.php index c79fe8ce..6742c0a3 100644 --- a/examples/dom-mutation/main.php +++ b/examples/dom-mutation/main.php @@ -1,7 +1,4 @@ formatOutput = false; diff --git a/examples/edge-cases-runtime/main.php b/examples/edge-cases-runtime/main.php index 206734e8..b8520745 100644 --- a/examples/edge-cases-runtime/main.php +++ b/examples/edge-cases-runtime/main.php @@ -1,9 +1,5 @@ id; } - // late static binding for factory public static function create(array $attrs = []): static { return new static($attrs); } } -// --- concrete entities --- class User extends Entity { @@ -271,7 +261,6 @@ public function pluck(string $key): Collection } } -// --- bitwise flags --- const PERM_READ = 1; const PERM_WRITE = 2; @@ -294,7 +283,6 @@ function describePermissions(int $perms): string return implode(", ", $names); } -// --- custom exceptions --- class ValidationException extends RuntimeException { @@ -325,9 +313,7 @@ function saveEntity(Entity $entity, int $permissions): void } } -// ============================================================ // tests -// ============================================================ // === test: entity creation with late static binding === @@ -373,7 +359,6 @@ function saveEntity(Entity $entity, int $permissions): void $evens = $numbers(function ($n) { return $n % 2 === 0; }); echo "evens: " . implode(", ", $evens->toArray()) . "\n"; -// === test: collection methods === $users = new Collection([ ["name" => "Alice", "age" => 30], @@ -383,7 +368,6 @@ function saveEntity(Entity $entity, int $permissions): void $names = $users->pluck("name"); echo "names: " . implode(", ", $names->toArray()) . "\n"; -// === test: bitwise operations === $perms = PERM_READ | PERM_WRITE; echo "perms: " . describePermissions($perms) . "\n"; @@ -431,7 +415,6 @@ function riskyOperation(): void } riskyOperation(); -// === test: do-while === $i = 1; $sum = 0; @@ -463,7 +446,6 @@ function riskyOperation(): void } echo "continue 2: " . implode(" ", $result) . "\n"; -// === test: __clone magic === $original = User::create(["name" => "CloneMe", "email" => "clone@test.com"]); $original->setMeta("source", "original"); diff --git a/examples/enum-patterns/main.php b/examples/enum-patterns/main.php index f7c96298..e9b9556d 100644 --- a/examples/enum-patterns/main.php +++ b/examples/enum-patterns/main.php @@ -1,7 +1,5 @@ name . " = " . $case->value . "\n"; } -// --- enum methods --- enum Direction { case North; @@ -101,7 +95,6 @@ public function label(): string { echo "North label: " . $dir->label() . "\n"; echo "East opposite: " . Direction::East->opposite()->name . "\n"; -// --- enum implementing interfaces --- interface HasDescription { public function description(): string; @@ -128,7 +121,6 @@ public function description(): string { echo "Season value: " . $season->value . "\n"; echo "Season description: " . $season->description() . "\n"; -// --- enum constants --- enum Size { case Small; @@ -156,7 +148,6 @@ function describeStatus(HttpStatus $s): string { echo "404 status: " . describeStatus(HttpStatus::NotFound) . "\n"; echo "500 status: " . describeStatus(HttpStatus::ServerError) . "\n"; -// --- enum in arrays --- $statusMessages = []; $statusMessages[HttpStatus::OK->value] = "All good"; @@ -171,7 +162,6 @@ function describeStatus(HttpStatus $s): string { echo "First favorite: " . $favorites[0]->name . "\n"; echo "Second favorite: " . $favorites[1]->name . "\n"; -// --- enum static methods --- enum Currency: string { case USD = 'USD'; @@ -209,7 +199,6 @@ public function symbol(): string { } echo "Unknown symbol throws: " . ($caughtSymbol ? "true" : "false") . "\n"; -// --- enum comparison --- $a = Suit::Hearts; $b = Suit::Hearts; diff --git a/examples/enum-state-machine/main.php b/examples/enum-state-machine/main.php index cfa40724..b62bf5cf 100644 --- a/examples/enum-state-machine/main.php +++ b/examples/enum-state-machine/main.php @@ -1,7 +1,4 @@ throw new TypeError("t"), diff --git a/examples/error-recovery/main.php b/examples/error-recovery/main.php index d3fd140f..c97d24e9 100644 --- a/examples/error-recovery/main.php +++ b/examples/error-recovery/main.php @@ -1,9 +1,5 @@ getMessage() : 'none') . "\n"; } -// --- finally with return --- echo "\n=== finally ===\n"; function withFinally($fail) { $result = "start"; diff --git a/examples/event-dispatcher/main.php b/examples/event-dispatcher/main.php index 1615d888..1302d81d 100644 --- a/examples/event-dispatcher/main.php +++ b/examples/event-dispatcher/main.php @@ -1,7 +1,4 @@ listen("user.created", $userHandler, 10); $dispatcher->listen("user.created", $auditLogger, 5); -// register closure listener $createdNames = []; $dispatcher->listen("user.created", function (EventInterface $event) use (&$createdNames) { $createdNames[] = $event->get("name"); }, 1); -// register listener for different event $deletedLog = []; $dispatcher->listen("user.deleted", function (EventInterface $event) use (&$deletedLog) { $deletedLog[] = $event->get("name") . " was deleted"; }); -// dispatch events $event1 = new Event("user.created", ["name" => "Alice", "email" => "alice@test.com"]); $dispatcher->dispatch($event1); @@ -181,7 +173,6 @@ public function getEntries(): array $event3 = new Event("user.deleted", ["name" => "Charlie"]); $dispatcher->dispatch($event3); -// check results echo "notifications:\n"; foreach ($userHandler->getNotifications() as $n) echo " $n\n"; @@ -191,7 +182,6 @@ public function getEntries(): array echo "created: " . implode(", ", $createdNames) . "\n"; echo "deleted: " . implode(", ", $deletedLog) . "\n"; -// test priority ordering $order = []; $d2 = new EventDispatcher(); $d2->listen("test", function ($e) use (&$order) { $order[] = "low"; }, 1); @@ -200,7 +190,6 @@ public function getEntries(): array $d2->dispatch(new Event("test")); echo "priority order: " . implode(", ", $order) . "\n"; -// test propagation stopping $stopped = []; $d3 = new EventDispatcher(); $d3->listen("stop.test", function (EventInterface $e) use (&$stopped) { @@ -213,11 +202,9 @@ public function getEntries(): array $d3->dispatch(new Event("stop.test")); echo "stopped after: " . implode(", ", $stopped) . "\n"; -// test hasListeners echo "has user.created: " . var_export($dispatcher->hasListeners("user.created"), true) . "\n"; echo "has unknown: " . var_export($dispatcher->hasListeners("unknown"), true) . "\n"; -// test event data mutation $mutator = new EventDispatcher(); $mutator->listen("transform", function (EventInterface $e) { $val = $e->get("value"); @@ -232,12 +219,10 @@ public function getEntries(): array $mutator->dispatch($te); echo "transformed: " . $te->get("value") . "\n"; -// test dispatch log $log = $dispatcher->getLog(); echo "dispatched events: " . count($log) . "\n"; foreach ($log as $entry) echo " $entry\n"; -// test try/catch in listener $errorDispatcher = new EventDispatcher(); $errorResults = []; $errorDispatcher->listen("risky", function ($e) use (&$errorResults) { diff --git a/examples/event-system/main.php b/examples/event-system/main.php index e4217eda..f73f7210 100644 --- a/examples/event-system/main.php +++ b/examples/event-system/main.php @@ -1,5 +1,4 @@ once('notify', function(string $msg) use (&$onceLog) { $onceLog[] = $msg; @@ -128,7 +125,6 @@ public function format(): array { echo "Once listener fired: " . count($onceLog) . " time(s)\n"; echo "Once value: " . $onceLog[0] . "\n"; -// listener count $emitter->on('data', function() {}); $emitter->on('data', function() {}); echo "Data listeners: " . $emitter->listenerCount('data') . "\n"; @@ -155,7 +151,6 @@ public function format(): array { echo " Count: " . count($errors) . "\n"; echo " Message: " . $errors[0]['message'] . "\n"; -// --- middleware pipeline --- function pipeline(array $middlewares, $input) { $next = function($value) { return $value; }; @@ -231,7 +226,6 @@ public function update(Subject $subject): void { echo "\nObserver A received: " . implode(', ', $obs1->received) . "\n"; echo "Observer B received: " . implode(', ', $obs2->received) . "\n"; -// --- closure composition --- function compose(callable ...$fns): callable { return function($x) use ($fns) { @@ -250,7 +244,6 @@ function compose(callable ...$fns): callable { $transform = compose($square, $addOne, $double); echo "\nCompose (5): " . $transform(5) . "\n"; -// --- currying --- function curry(callable $fn, ...$initial): callable { return function() use ($fn, $initial) { diff --git a/examples/facade-pattern/main.php b/examples/facade-pattern/main.php index 8037269f..1b549e9e 100644 --- a/examples/facade-pattern/main.php +++ b/examples/facade-pattern/main.php @@ -1,7 +1,4 @@ ), -// recursive closures, closure as return value, closure composition -// --- higher-order functions --- echo "=== Higher-Order Functions ===\n"; @@ -25,7 +19,6 @@ function($s) { return preg_replace('/[^a-z0-9-]/', '', $s); } ); echo "piped: $result\n"; -// --- closure factories --- echo "\n=== Closure Factories ===\n"; @@ -64,7 +57,6 @@ function makeCounter() { $counter['reset'](); echo "after reset: " . $counter['get']() . "\n"; -// --- map/filter/reduce pipeline --- echo "\n=== Pipeline ===\n"; @@ -97,7 +89,6 @@ function makeCounter() { }, 0); echo "grand total: \$" . number_format($grand, 2) . "\n"; -// --- compact/extract --- echo "\n=== Compact/Extract ===\n"; @@ -114,7 +105,6 @@ function makeCounter() { extract($other); echo "extracted: color=$color, food=$food\n"; -// --- call_user_func --- echo "\n=== Call User Func ===\n"; @@ -134,7 +124,6 @@ public function lower($s) { return strtolower($s); } $fmt = new Formatter(); echo call_user_func([$fmt, 'lower'], 'WORLD') . "\n"; -// --- recursive closure --- echo "\n=== Recursive Closure ===\n"; @@ -149,7 +138,6 @@ public function lower($s) { return strtolower($s); } } echo "fibonacci: " . implode(', ', $fibs) . "\n"; -// --- array_walk with ref --- echo "\n=== Array Walk ===\n"; @@ -175,7 +163,6 @@ public function lower($s) { return strtolower($s); } $strings = array_map(function($v) { return (string)((int)$v * 2); }, $values); echo "doubled strings: " . implode(', ', $strings) . "\n"; -// --- named arguments --- echo "\n=== Named Arguments ===\n"; @@ -190,7 +177,6 @@ function createTag($tag, $content, $class = '', $id = '') { echo createTag('p', 'World', class: 'highlight') . "\n"; echo createTag('span', 'Test', id: 'main', class: 'bold') . "\n"; -// --- memoize pattern --- echo "\n=== Memoize ===\n"; diff --git a/examples/generator-edges/main.php b/examples/generator-edges/main.php index 460ed70b..cb988c66 100644 --- a/examples/generator-edges/main.php +++ b/examples/generator-edges/main.php @@ -1,7 +1,4 @@ withHeader("Accept", "application/json") @@ -137,20 +132,17 @@ public static function report($ch): void { $ch3 = $delete->toCurl(); RequestInspector::report($ch3); -// test curl_reset echo "\n--- reset ---\n"; curl_reset($ch); $info = curl_getinfo($ch); echo "after reset url: '" . ($info['url'] ?? '') . "'\n"; echo "after reset error: '" . curl_error($ch) . "'\n"; -// test version info echo "\n--- version ---\n"; $ver = curl_version(); echo "has version: " . (isset($ver['version']) ? 'yes' : 'no') . "\n"; echo "has ssl: " . (isset($ver['ssl_version']) ? 'yes' : 'no') . "\n"; -// test method routing with match echo "\n--- method routing ---\n"; $methods = [HttpMethod::GET, HttpMethod::POST, HttpMethod::PUT, HttpMethod::DELETE, HttpMethod::PATCH]; foreach ($methods as $method) { diff --git a/examples/image-thumbnail/main.php b/examples/image-thumbnail/main.php index 13930f27..a75c146d 100644 --- a/examples/image-thumbnail/main.php +++ b/examples/image-thumbnail/main.php @@ -1,7 +1,4 @@ value yield), -// generator methods (current, key, valid, next, getReturn), Fiber (start, -// resume, suspend, getReturn, isTerminated), iterator chaining, generator -// delegation, generator exception handling, closures as generator factories, -// foreach on generators, generator state lifecycle -// --- fiber basics --- echo "=== Fiber Basics ===\n"; @@ -23,7 +17,6 @@ echo "return: " . $fiber->getReturn() . "\n"; echo "terminated: " . ($fiber->isTerminated() ? 'yes' : 'no') . "\n"; -// --- fiber as coroutine --- echo "\n=== Fiber Coroutine ===\n"; @@ -51,7 +44,6 @@ function make_counter(int $start): Fiber { $counter->resume('stop'); echo "terminated: " . ($counter->isTerminated() ? 'yes' : 'no') . "\n"; -// --- multiple fibers interleaved --- echo "\n=== Interleaved Fibers ===\n"; @@ -99,7 +91,6 @@ function make_worker(string $name, int $steps): Fiber { echo "$entry\n"; } -// --- basic generator pipeline --- echo "\n=== Generator Pipeline ===\n"; @@ -143,7 +134,6 @@ function take_gen(Generator $source, int $n): Generator { } echo "pipeline: " . implode(', ', $results) . "\n"; -// --- key-value yield --- echo "\n=== Key-Value Yield ===\n"; @@ -178,7 +168,6 @@ function sum_gen(array $nums): Generator { echo "running sums: " . implode(', ', $running) . "\n"; echo "final return: " . $gen->getReturn() . "\n"; -// --- generator send --- echo "\n=== Generator Send ===\n"; @@ -200,7 +189,6 @@ function accumulator(): Generator { $result = $acc->send(null); echo "accumulated: " . $acc->getReturn() . "\n"; -// --- generator state methods --- echo "\n=== Generator State ===\n"; @@ -221,7 +209,6 @@ function three_items(): Generator { $gen->next(); echo "exhausted valid: " . ($gen->valid() ? 'yes' : 'no') . "\n"; -// --- generator factory closures --- echo "\n=== Generator Factory ===\n"; @@ -241,7 +228,6 @@ function make_repeater(string $value, int $times): Closure { } echo "repeated: " . implode(', ', $items) . "\n"; -// --- chained generator transforms --- echo "\n=== Chained Transforms ===\n"; @@ -278,7 +264,6 @@ function flatten_gen(Generator $source): Generator { } echo "chunk-flatten-double: " . implode(', ', $results) . "\n"; -// --- generator with exception --- echo "\n=== Generator Exception ===\n"; @@ -302,7 +287,6 @@ function safe_divide_gen(array $pairs): Generator { } echo implode(', ', $results) . "\n"; -// --- yield from delegation --- echo "\n=== Yield From ===\n"; @@ -325,7 +309,6 @@ function outer_gen(): Generator { } echo "all items: " . implode(', ', $items) . "\n"; -// --- nested yield from --- echo "\n=== Nested Yield From ===\n"; @@ -394,7 +377,6 @@ function select(Generator $source, string $field): Generator { } echo "engineering: " . implode(', ', $eng) . "\n"; -// --- fiber with generator --- echo "\n=== Fiber With Generator ===\n"; diff --git a/examples/job-queue/main.php b/examples/job-queue/main.php index 0daa6382..f80a5882 100644 --- a/examples/job-queue/main.php +++ b/examples/job-queue/main.php @@ -1,8 +1,6 @@ ), preg_match_all PREG_SET_ORDER, generators with send(), priority queue insertion, exception handling in generators error_reporting(E_ALL & ~E_DEPRECATED); -// DSL parser with named regex function parseJobDef(string $line): ?array { $pattern = '/^(?P[a-z_]+)\s*\[(?P\d+)\]\s*:\s*(?P.+)$/'; if (preg_match($pattern, $line, $m)) { @@ -122,7 +120,6 @@ public function getStats(): array { return $this->stats; } echo "recent: " . implode(",", $q->recentFirst()) . "\n"; echo "tracked: " . count($q->trackedJobs()) . "\n"; -// generator on queue2 $q2 = new Queue(); $order2 = []; $q2->enqueue(new Job('task_a', 3, function($j) use (&$order2) { $order2[] = $j->name; echo "A"; })); @@ -138,7 +135,6 @@ public function getStats(): array { return $this->stats; } echo "\nscheduler order: " . implode(",", $order2) . "\n"; echo "scheduler completed: " . $q2->completedCount() . "\n"; -// nested output buffering ob_start(); echo "outer"; ob_start(); diff --git a/examples/json-edge-cases/main.php b/examples/json-edge-cases/main.php index ae878ac2..6b7891dd 100644 --- a/examples/json-edge-cases/main.php +++ b/examples/json-edge-cases/main.php @@ -1,7 +1,4 @@ 0) { @@ -56,7 +53,6 @@ function rangeGen(int $start, int $end, int $step = 1): Generator { echo "Range(1,10,2): " . implode(', ', iterator_to_array(rangeGen(1, 10, 2), false)) . "\n"; echo "Range(10,1,-3): " . implode(', ', iterator_to_array(rangeGen(10, 1, -3), false)) . "\n"; -// --- generator pipeline: map/filter/reduce --- function genMap(Generator $gen, callable $fn): Generator { foreach ($gen as $value) { @@ -88,7 +84,6 @@ function genReduce(Generator $gen, callable $fn, $initial) { $sum = genReduce(rangeGen(1, 100), function($acc, $n) { return $acc + $n; }, 0); echo "Sum 1..100: $sum\n"; -// --- yield from (delegation) --- function inner(): Generator { yield 'a'; @@ -104,7 +99,6 @@ function outer(): Generator { echo "Delegated: " . implode(', ', iterator_to_array(outer(), false)) . "\n"; -// --- chunked reading --- function chunks(array $data, int $size): Generator { $len = count($data); @@ -119,7 +113,6 @@ function chunks(array $data, int $size): Generator { echo " batch: [" . implode(', ', $chunk) . "] sum=" . array_sum($chunk) . "\n"; } -// --- memoize with closures --- function memoize(callable $fn): callable { $cache = []; @@ -142,7 +135,6 @@ function memoize(callable $fn): callable { echo " 5! = " . $factorial(5) . "\n"; echo " 10! = " . $factorial(10) . "\n"; -// --- pipe function --- function pipe(string $value): string { $value = trim($value); diff --git a/examples/ldap-directory/main.php b/examples/ldap-directory/main.php index cf095178..efb3fb84 100644 --- a/examples/ldap-directory/main.php +++ b/examples/ldap-directory/main.php @@ -1,6 +1,4 @@ 0, 'INFO' => 1, @@ -19,7 +14,6 @@ function formatLogLine(string $level, string $message, array $context = []): str $padded = str_pad(strtoupper($level), 8); if (count($context) > 0) { - // interpolate {key} placeholders foreach ($context as $key => $value) { $placeholder = '{' . $key . '}'; if (is_array($value)) { @@ -38,7 +32,6 @@ function formatLogLine(string $level, string $message, array $context = []): str echo formatLogLine('error', 'Failed to connect to {host}:{port}', ['host' => 'db.example.com', 'port' => 5432]) . "\n"; echo formatLogLine('debug', 'Query result: {data}', ['data' => ['rows' => 42, 'cached' => true]]) . "\n"; -// vsprintf for structured formatting echo "\n=== vsprintf formatting ===\n"; $formats = [ "%-20s %5d requests %6.2f%% success", @@ -64,7 +57,6 @@ function formatLogLine(string $level, string $message, array $context = []): str echo sprintf(" %-15s -> pattern: %-25s match: %s\n", $input, $pattern, $matches ? 'yes' : 'no'); } -// log parsing with regex echo "\n=== log parsing ===\n"; $logLines = [ "[2024-01-15 10:30:45] ERROR Database connection failed: timeout after 30s", @@ -94,7 +86,6 @@ function formatLogLine(string $level, string $message, array $context = []): str echo " " . $err['time'] . " - " . $err['message'] . "\n"; } -// number formatting for metrics echo "\n=== number formatting ===\n"; $metrics = [ ['name' => 'requests', 'value' => 1234567], @@ -107,7 +98,6 @@ function formatLogLine(string $level, string $message, array $context = []): str echo sprintf(" %-20s %20s\n", $metric['name'], $formatted); } -// splitting log messages echo "\n=== preg_split ===\n"; $kvLog = "host=db.prod port=5432 user=admin db=myapp pool_size=10"; $parts = preg_split('/\s+/', $kvLog); @@ -128,7 +118,6 @@ function formatLogLine(string $level, string $message, array $context = []): str echo " utc: " . gmdate('Y-m-d H:i:s', $ts) . "\n"; echo " rfc: " . gmdate('D, d M Y H:i:s', $ts) . " GMT\n"; -// round with precision echo "\n=== round precision ===\n"; $values = [3.14159, 2.71828, 1.41421, 0.57721]; foreach ($values as $v) { diff --git a/examples/magic-methods/main.php b/examples/magic-methods/main.php index 577424cc..4df84b9d 100644 --- a/examples/magic-methods/main.php +++ b/examples/magic-methods/main.php @@ -1,8 +1,4 @@ '; @@ -28,19 +26,16 @@ function parseMarkdown(string $markdown): string { continue; } - // close list if needed if ($inList && !preg_match('/^\s*[-*]\s/', $line) && !preg_match('/^\s*\d+\.\s/', $line) && trim($line) !== '') { $html[] = $listType === 'ul' ? '' : ''; $inList = false; } - // close blockquote if needed if ($inBlockquote && !str_starts_with($line, '>')) { $html[] = ''; $inBlockquote = false; } - // empty line if (trim($line) === '') { if ($inList) { $html[] = $listType === 'ul' ? '' : ''; @@ -49,7 +44,6 @@ function parseMarkdown(string $markdown): string { continue; } - // headings if (preg_match('/^(#{1,6})\s+(.+)$/', $line, $matches)) { $level = strlen($matches[1]); $text = parseInline($matches[2]); @@ -57,13 +51,11 @@ function parseMarkdown(string $markdown): string { continue; } - // horizontal rule if (preg_match('/^[-*_]{3,}$/', trim($line))) { $html[] = '
'; continue; } - // blockquote if (str_starts_with($line, '>')) { if (!$inBlockquote) { $html[] = '
'; @@ -74,7 +66,6 @@ function parseMarkdown(string $markdown): string { continue; } - // unordered list if (preg_match('/^\s*[-*]\s+(.+)$/', $line, $matches)) { if (!$inList || $listType !== 'ul') { if ($inList) $html[] = ''; @@ -86,7 +77,6 @@ function parseMarkdown(string $markdown): string { continue; } - // ordered list if (preg_match('/^\s*\d+\.\s+(.+)$/', $line, $matches)) { if (!$inList || $listType !== 'ol') { if ($inList) $html[] = ''; @@ -98,7 +88,6 @@ function parseMarkdown(string $markdown): string { continue; } - // paragraph $html[] = '

' . parseInline($line) . '

'; } @@ -110,28 +99,21 @@ function parseMarkdown(string $markdown): string { } function parseInline(string $text): string { - // bold + italic $text = preg_replace('/\*\*\*(.+?)\*\*\*/', '$1', $text); - // bold $text = preg_replace('/\*\*(.+?)\*\*/', '$1', $text); - // italic $text = preg_replace('/\*(.+?)\*/', '$1', $text); - // inline code $text = preg_replace('/`([^`]+)`/', '$1', $text); - // links $text = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $text); - // images $text = preg_replace('/!\[([^\]]*)\]\(([^)]+)\)/', '$1', $text); return $text; } -// --- test --- $markdown = <<<'MD' # Markdown Parser @@ -176,7 +158,6 @@ function hello() { $html = parseMarkdown($markdown); echo $html . "\n"; -// --- verify specific patterns --- echo "\nInline parsing:\n"; $tests = [ diff --git a/examples/math-crypto/main.php b/examples/math-crypto/main.php index 7eb0bb8e..0d4b9e62 100644 --- a/examples/math-crypto/main.php +++ b/examples/math-crypto/main.php @@ -1,11 +1,5 @@ 1e308: " . (PHP_FLOAT_MAX > 1e308 ? 'yes' : 'no') . "\n"; -// --- hash functions --- echo "\n=== Hash Functions ===\n"; @@ -91,7 +79,6 @@ echo "hash('md5', 'hello'): " . hash('md5', 'hello') . "\n"; echo "crc32('hello'): " . crc32('hello') . "\n"; -// --- hmac --- echo "\n=== HMAC ===\n"; @@ -99,7 +86,6 @@ echo "hash_hmac('md5', 'data', 'key'): " . hash_hmac('md5', 'data', 'key') . "\n"; echo "hash_hmac('sha1', 'data', 'key'): " . hash_hmac('sha1', 'data', 'key') . "\n"; -// --- password hashing --- echo "\n=== Password Hashing ===\n"; @@ -109,7 +95,6 @@ echo "verify correct: " . (password_verify('secret123', $hash) ? 'yes' : 'no') . "\n"; echo "verify wrong: " . (password_verify('wrong', $hash) ? 'yes' : 'no') . "\n"; -// --- random --- echo "\n=== Random ===\n"; @@ -119,7 +104,6 @@ echo "random_bytes length: " . strlen($bytes) . "\n"; echo "random_bytes hex length: " . strlen(bin2hex($bytes)) . "\n"; -// --- numeric checks --- echo "\n=== Numeric Checks ===\n"; @@ -134,7 +118,6 @@ echo "is_numeric(true): " . (is_numeric(true) ? 'yes' : 'no') . "\n"; echo "is_numeric(null): " . (is_numeric(null) ? 'yes' : 'no') . "\n"; -// --- type conversion --- echo "\n=== Type Conversion ===\n"; @@ -145,7 +128,6 @@ echo "floatval('3.14'): " . floatval('3.14') . "\n"; echo "floatval('1.2e3'): " . floatval('1.2e3') . "\n"; -// --- sprintf with numbers --- echo "\n=== Sprintf ===\n"; @@ -159,7 +141,6 @@ echo sprintf("percent: %%") . "\n"; echo sprintf("multiple: %d + %d = %d", 2, 3, 5) . "\n"; -// --- array math --- echo "\n=== Array Math ===\n"; diff --git a/examples/math-edge-cases/main.php b/examples/math-edge-cases/main.php index a77484d0..e6d00816 100644 --- a/examples/math-edge-cases/main.php +++ b/examples/math-edge-cases/main.php @@ -1,10 +1,5 @@ 0 ? "true" : "false") . "\n"; -// --- float precision --- echo "--- float precision ---\n"; echo "0.1 + 0.2 == 0.3: " . ((0.1 + 0.2) == 0.3 ? "true" : "false") . "\n"; @@ -24,7 +18,6 @@ echo "1/3 + 2/3: " . (1/3 + 2/3) . "\n"; echo "1/3 * 3: " . (1/3 * 3) . "\n"; -// --- division --- echo "--- division ---\n"; echo "10 / 3: " . (10 / 3) . "\n"; @@ -33,7 +26,6 @@ echo "-7 / 2: " . (-7 / 2) . "\n"; echo "intdiv(-7, 2): " . intdiv(-7, 2) . "\n"; -// --- modulo --- echo "--- modulo ---\n"; echo "10 % 3: " . (10 % 3) . "\n"; @@ -43,7 +35,6 @@ echo "fmod(10.5, 3.2): " . fmod(10.5, 3.2) . "\n"; echo "fmod(-10.5, 3.2): " . fmod(-10.5, 3.2) . "\n"; -// --- special values --- echo "--- special values ---\n"; echo "INF: " . INF . "\n"; @@ -60,7 +51,6 @@ echo "INF * 0: " . (INF * 0) . "\n"; echo "1 / INF: " . (1 / INF) . "\n"; -// --- math functions --- echo "--- math functions ---\n"; echo "abs(-42): " . abs(-42) . "\n"; @@ -87,7 +77,6 @@ echo "log10(1000): " . log10(1000) . "\n"; echo "log2(8): " . log(8, 2) . "\n"; -// --- type conversion --- echo "--- type conversion ---\n"; echo "intval('42'): " . intval('42') . "\n"; @@ -98,7 +87,6 @@ echo "intval(3.9): " . intval(3.9) . "\n"; echo "intval(-3.9): " . intval(-3.9) . "\n"; -// --- bitwise --- echo "--- bitwise ---\n"; echo "0xFF & 0x0F: " . (0xFF & 0x0F) . "\n"; @@ -108,7 +96,6 @@ echo "1 << 8: " . (1 << 8) . "\n"; echo "256 >> 4: " . (256 >> 4) . "\n"; -// --- comparison edge cases --- echo "--- comparisons ---\n"; echo "0 == false: " . (0 == false ? "true" : "false") . "\n"; @@ -119,7 +106,6 @@ echo "0 == null: " . (0 == null ? "true" : "false") . "\n"; echo "'' == null: " . ('' == null ? "true" : "false") . "\n"; -// spaceship operator echo "1 <=> 2: " . (1 <=> 2) . "\n"; echo "2 <=> 1: " . (2 <=> 1) . "\n"; echo "1 <=> 1: " . (1 <=> 1) . "\n"; diff --git a/examples/math-precision/main.php b/examples/math-precision/main.php index 9d3e4bea..70112515 100644 --- a/examples/math-precision/main.php +++ b/examples/math-precision/main.php @@ -1,9 +1,5 @@ 10 = " . base_convert('73', 36, 10) . "\n"; -// color math with hex echo "\n=== color math ===\n"; function hexToRgb(string $hex): array { $hex = ltrim($hex, '#'); @@ -76,7 +69,6 @@ function blendColors(string $c1, string $c2, float $ratio): string { echo sprintf(" %.0f%%: %s\n", $r * 100, blendColors($red, $blue, $r)); } -// logarithms and scientific calculations echo "\n=== logarithms ===\n"; $values = [1, 2, 10, 100, 1024, 65536]; echo sprintf(" %-8s %8s %8s %8s\n", "value", "log2", "log10", "ln"); @@ -84,7 +76,6 @@ function blendColors(string $c1, string $c2, float $ratio): string { echo sprintf(" %-8d %8.4f %8.4f %8.4f\n", $v, log($v) / log(2), log10($v), log($v)); } -// pythagorean calculations with hypot echo "\n=== hypot (pythagorean) ===\n"; $triangles = [[3, 4], [5, 12], [8, 15], [7, 24]]; foreach ($triangles as $t) { @@ -92,7 +83,6 @@ function blendColors(string $c1, string $c2, float $ratio): string { echo sprintf(" sides %2d, %2d -> hypotenuse = %.4f\n", $t[0], $t[1], $h); } -// distance between 2D points function distance(float $x1, float $y1, float $x2, float $y2): float { return hypot($x2 - $x1, $y2 - $y1); } @@ -104,7 +94,6 @@ function distance(float $x1, float $y1, float $x2, float $y2): float { echo sprintf(" (%.0f,%.0f) to (%.0f,%.0f) = %.4f\n", $p[0], $p[1], $p[2], $p[3], $d); } -// rounding modes echo "\n=== rounding ===\n"; $nums = [2.5, 3.5, 4.5, -2.5, 2.55, 2.449]; echo sprintf(" %-8s %8s %8s %8s %8s\n", "value", "round", "floor", "ceil", "round2"); @@ -113,7 +102,6 @@ function distance(float $x1, float $y1, float $x2, float $y2): float { $n, round($n), floor($n), ceil($n), round($n, 1)); } -// financial calculations with number_format echo "\n=== financial formatting ===\n"; $prices = [1234.5, 1000000, 0.99, 42195.876, 0.001]; foreach ($prices as $price) { @@ -122,7 +110,6 @@ function distance(float $x1, float $y1, float $x2, float $y2): float { number_format($price, 2, '.', ',')); } -// fmod for precise remainder echo "\n=== fmod ===\n"; $fmod_cases = [[10.5, 3.2], [2.5, 0.5], [7.0, 2.5], [-5.5, 3.0]]; foreach ($fmod_cases as $case) { diff --git a/examples/matrix-math/main.php b/examples/matrix-math/main.php index 74e9356f..dfb6c66c 100644 --- a/examples/matrix-math/main.php +++ b/examples/matrix-math/main.php @@ -1,5 +1,4 @@ setHeader("X-Request-ID", "req-12345"); @@ -88,7 +84,6 @@ function addRequestId(Request $req, callable $next): Response return $response; } -// middleware: log $log = []; function logMiddleware(Request $req, callable $next): Response { @@ -99,7 +94,6 @@ function logMiddleware(Request $req, callable $next): Response return $response; } -// middleware: auth check function authMiddleware(Request $req, callable $next): Response { $token = $req->getHeader("Authorization"); @@ -110,14 +104,12 @@ function authMiddleware(Request $req, callable $next): Response return $next($req); } -// final handler function handler(Request $req): Response { $resp = new Response(); return $resp->setStatus(200)->setBody("Hello from " . $req->path); } -// test 1: request with auth $pipeline = new Pipeline(); $pipeline->pipe("addRequestId") ->pipe("logMiddleware") @@ -138,7 +130,6 @@ function handler(Request $req): Response echo "status: " . $resp2->status . "\n"; echo "body: " . $resp2->body . "\n"; -// log output foreach ($log as $entry) { echo "log: " . $entry . "\n"; } diff --git a/examples/mime-encoder/main.php b/examples/mime-encoder/main.php index 130fedb0..07c2e866 100644 --- a/examples/mime-encoder/main.php +++ b/examples/mime-encoder/main.php @@ -1,7 +1,5 @@ ", false) . "\n"; -// --- str_word_count --- echo "\nWord count:\n"; $sentence = "Hello beautiful world"; @@ -64,13 +55,11 @@ $words = str_word_count("The quick brown fox", 1); echo "Word list: " . implode(", ", $words) . "\n"; -// mode 2: return positions $positions = str_word_count("Hello world test", 2); foreach ($positions as $pos => $word) { echo " Position $pos: $word\n"; } -// --- array_combine --- echo "\nArray combine:\n"; $keys = ['name', 'age', 'city']; @@ -80,7 +69,6 @@ echo " $k: $v\n"; } -// --- array_flip --- echo "\nArray flip:\n"; $colors = ['red' => 1, 'green' => 2, 'blue' => 3]; @@ -89,12 +77,10 @@ echo " $k => $v\n"; } -// flip indexed array $fruits = ['apple', 'banana', 'cherry']; $fruit_index = array_flip($fruits); echo "Index of banana: " . $fruit_index['banana'] . "\n"; -// --- array_pad --- echo "\nArray pad:\n"; $arr = [1, 2, 3]; @@ -107,7 +93,6 @@ $no_pad = array_pad($arr, 2, 0); echo "Pad to 2 (no change): " . implode(",", $no_pad) . "\n"; -// --- MIME header encoding --- echo "\nMIME header encoding:\n"; @@ -193,7 +178,6 @@ function buildMimeMessage(array $parts): string { echo "Has boundary: " . (str_contains($message, "----boundary_12345") ? "yes" : "no") . "\n"; echo "Has base64: " . (str_contains($message, "Content-Transfer-Encoding: base64") ? "yes" : "no") . "\n"; -// decode the attachment back preg_match('/Content-Transfer-Encoding: base64\r\n.+?\r\n\r\n(.+?)(?:\r\n\r\n--|\z)/s', $message, $matches); if (isset($matches[1])) { $decoded_attachment = base64_decode(trim($matches[1])); @@ -268,7 +252,6 @@ function formatTable(array $rows, int $cols): void { ['Size'], ], 2); -// --- word frequency analysis --- echo "\nWord frequency:\n"; $text = "the cat sat on the mat the cat"; diff --git a/examples/mini-shell/main.php b/examples/mini-shell/main.php index 39a77e8f..06a421eb 100644 --- a/examples/mini-shell/main.php +++ b/examples/mini-shell/main.php @@ -1,12 +1,5 @@ $h) { $headerCells[] = str_pad($h, $widths[$i]); } $lines[] = implode(' | ', $headerCells); - // separator $sepCells = []; foreach ($widths as $w) { $sepCells[] = str_repeat('-', $w); } $lines[] = implode('-+-', $sepCells); - // rows foreach ($rows as $row) { $cells = []; foreach ($row as $i => $cell) { @@ -237,7 +223,6 @@ function formatTable($headers, $rows) { ] ) . "\n"; -// --- glob pattern matching --- function globMatch($pattern, $string) { $regex = '/^'; @@ -269,7 +254,6 @@ function globMatch($pattern, $string) { echo "test?.log matches test12.log: " . (globMatch('test?.log', 'test12.log') ? 'yes' : 'no') . "\n"; echo "*.* matches any.file: " . (globMatch('*.*', 'any.file') ? 'yes' : 'no') . "\n"; -// --- number formatting --- echo "--- numbers ---\n"; echo number_format(1234567.89, 2) . "\n"; @@ -277,7 +261,6 @@ function globMatch($pattern, $string) { echo number_format(1000000, 0, '.', ',') . "\n"; echo number_format(0.5, 4) . "\n"; -// --- array operations --- echo "--- array ops ---\n"; diff --git a/examples/multi-file/main.php b/examples/multi-file/main.php index 470e9955..866b942c 100644 --- a/examples/multi-file/main.php +++ b/examples/multi-file/main.php @@ -1,11 +1,4 @@ [ @@ -57,17 +49,14 @@ echo "first user: {$decoded['users'][0]['name']}\n"; echo "meta total: {$decoded['meta']['total']}\n"; -// nested json with special chars $special = ['message' => 'hello "world" & ', 'emoji' => "tab\there"]; $json2 = json_encode($special); $back = json_decode($json2, true); echo "special roundtrip: " . ($back['message'] === $special['message'] ? 'yes' : 'no') . "\n"; -// json encode with numeric keys $indexed = [10, 20, 30]; echo "indexed: " . json_encode($indexed) . "\n"; -// --- version_compare --- echo "\n=== version_compare ===\n"; echo "1.0 < 1.1: " . (version_compare('1.0', '1.1', '<') ? 'yes' : 'no') . "\n"; echo "2.0 > 1.9.9: " . (version_compare('2.0', '1.9.9', '>') ? 'yes' : 'no') . "\n"; @@ -79,7 +68,6 @@ $raw = version_compare('1.2.3', '1.2.4'); echo "raw compare: $raw\n"; -// --- class_alias --- echo "\n=== class_alias ===\n"; class Logger { private $name; @@ -92,7 +80,6 @@ class_alias('Logger', 'Log'); echo $log->info("started") . "\n"; echo ($log instanceof Logger) ? "instanceof: yes\n" : "instanceof: no\n"; -// --- deep array manipulation --- echo "\n=== deep arrays ===\n"; // build a tree via vivification then traverse @@ -110,7 +97,6 @@ function sumTree($node) { } echo "tree sum: " . sumTree($tree['root']) . "\n"; -// compact and extract $host = 'localhost'; $port = 5432; $driver = 'pgsql'; @@ -121,18 +107,15 @@ function sumTree($node) { extract($settings); echo "extracted: timeout=$timeout retries=$retries verbose=" . ($verbose ? 'true' : 'false') . "\n"; -// array_combine $keys = ['name', 'age', 'city']; $values = ['Alice', 30, 'NYC']; $combined = array_combine($keys, $values); echo "combined: {$combined['name']} age {$combined['age']} in {$combined['city']}\n"; -// array_fill_keys $defaults = array_fill_keys(['read', 'write', 'admin'], false); echo "defaults: read=" . ($defaults['read'] ? 'true' : 'false'); echo " admin=" . ($defaults['admin'] ? 'true' : 'false') . "\n"; -// nested array modification $users = [ ['name' => 'Alice', 'scores' => [90, 85, 92]], ['name' => 'Bob', 'scores' => [78, 82, 88]], diff --git a/examples/nested-traits/main.php b/examples/nested-traits/main.php index 9312c92f..327da844 100644 --- a/examples/nested-traits/main.php +++ b/examples/nested-traits/main.php @@ -1,6 +1,4 @@ format("Y-m-d") . "\n"; // method from Units trait (nested: Date uses Units) @@ -35,7 +32,6 @@ public function __construct($value) { echo ($c->isEqual($c2) ? "equal" : "not-equal") . "\n"; echo ($c->isEqual($c3) ? "equal" : "not-equal") . "\n"; -// 4-level deep autoloaded trait chain class DeepUser { use App\Traits\Deep\Top; } diff --git a/examples/oop-advanced/main.php b/examples/oop-advanced/main.php index c8590845..3c21fb0d 100644 --- a/examples/oop-advanced/main.php +++ b/examples/oop-advanced/main.php @@ -1,13 +1,5 @@ setCreatedAt("2024-06-15"); $p1 = new Paragraph("This is the first paragraph."); @@ -142,7 +138,6 @@ public function getComponentCount(): int echo $page->render() . "\n"; echo "components: " . $page->getComponentCount() . "\n"; -// test instanceof echo ($h1 instanceof Renderable) ? "renderable" : "not"; echo "\n"; echo ($h1 instanceof HasTitle) ? "has title" : "no title"; @@ -150,15 +145,12 @@ public function getComponentCount(): int echo ($p1 instanceof HasTitle) ? "has title" : "no title"; echo "\n"; -// test trait echo $h1->getCreatedAt() . "\n"; echo $p1->getCreatedAt() . "\n"; -// test abstract method dispatch echo $h1->describe() . "\n"; echo $p1->describe() . "\n"; -// test is_a echo is_a($h1, "Component") ? "is component" : "not"; echo "\n"; echo is_a($page, "Component") ? "is component" : "not"; @@ -166,11 +158,9 @@ public function getComponentCount(): int echo is_a($page, "Renderable") ? "is renderable" : "not"; echo "\n"; -// get_class echo get_class($h1) . "\n"; echo get_class($p1) . "\n"; -// get_parent_class echo get_parent_class($h1) . "\n"; echo "done\n"; diff --git a/examples/orm-lite/main.php b/examples/orm-lite/main.php index b6489359..f9ed7dab 100644 --- a/examples/orm-lite/main.php +++ b/examples/orm-lite/main.php @@ -1,11 +1,5 @@ getDirty(); echo "changes: " . json_encode($changes) . "\n"; -// --- test: __toString --- echo "--- toString ---\n"; echo "$user\n"; @@ -192,14 +182,12 @@ public function groupBy($field) { $user3 = new User([]); echo "$user3\n"; -// --- test: json --- echo "--- json ---\n"; echo $user2->toJson() . "\n"; $arr = $user2->toArray(); echo "keys: " . implode(', ', array_keys($arr)) . "\n"; -// --- test: collection basics --- echo "--- collection ---\n"; @@ -215,13 +203,11 @@ public function groupBy($field) { echo "first: " . $users->first() . "\n"; echo "last: " . $users->last() . "\n"; -// --- test: pluck --- echo "--- pluck ---\n"; $names = $users->pluck('name'); echo "names: " . implode(', ', $names) . "\n"; -// --- test: where --- echo "--- where ---\n"; $admins = $users->where('role', 'admin'); @@ -229,7 +215,6 @@ public function groupBy($field) { $adminNames = $admins->pluck('name'); echo "admin names: " . implode(', ', $adminNames) . "\n"; -// --- test: sortBy --- echo "--- sort ---\n"; $byAge = $users->sortBy('age'); @@ -244,13 +229,11 @@ public function groupBy($field) { $sortedNames = $byName->pluck('name'); echo "by name: " . implode(', ', $sortedNames) . "\n"; -// --- test: sum --- echo "--- sum ---\n"; echo "total score: " . $users->sum('score') . "\n"; echo "total age: " . $users->sum('age') . "\n"; -// --- test: groupBy --- echo "--- groupBy ---\n"; $byRole = $users->groupBy('role'); @@ -259,7 +242,6 @@ public function groupBy($field) { echo "$role: " . implode(', ', $groupNames) . "\n"; } -// --- test: chaining --- echo "--- chaining ---\n"; $result = $users diff --git a/examples/pack-unpack/main.php b/examples/pack-unpack/main.php index 6cab8244..45b11ca7 100644 --- a/examples/pack-unpack/main.php +++ b/examples/pack-unpack/main.php @@ -1,5 +1,4 @@ 2.718 && $ud['val'] < 2.719) ? "ok" : "fail"; echo "\n"; -// big-endian float $gf = pack("G", 1.5); $ugf = unpack("Gval", $gf); echo "big-endian float 1.5: " . $ugf['val'] . "\n"; -// big-endian double $gd = pack("E", 2.5); $ugd = unpack("Eval", $gd); echo "big-endian double 2.5: " . $ugd['val'] . "\n"; @@ -122,16 +106,13 @@ echo "a3x2a3 hex: " . bin2hex($x) . "\n"; echo "a3x2a3 length: " . strlen($x) . "\n"; -// back up $xb = pack("a5X2a2", "ABCDE", "XY"); echo "a5X2a2: " . $xb . "\n"; -// absolute position $at = pack("a3@10a3", "ABC", "DEF"); echo "@10 length: " . strlen($at) . "\n"; echo "@10 hex: " . bin2hex($at) . "\n"; -// --- unpack with offset --- echo "\nUnpack with offset:\n"; $data = pack("NNN", 100, 200, 300); diff --git a/examples/pcntl-worker-pool/main.php b/examples/pcntl-worker-pool/main.php index 38f17c40..ff842766 100644 --- a/examples/pcntl-worker-pool/main.php +++ b/examples/pcntl-worker-pool/main.php @@ -1,7 +1,4 @@ \d{4})-(?\d{2})-(?\d{2})$/'; diff --git a/examples/pdo-fetch-modes/main.php b/examples/pdo-fetch-modes/main.php index 5bf3190a..2ad22df7 100644 --- a/examples/pdo-fetch-modes/main.php +++ b/examples/pdo-fetch-modes/main.php @@ -1,6 +1,4 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); diff --git a/examples/pdo-models/main.php b/examples/pdo-models/main.php index 64aafec1..27018d53 100644 --- a/examples/pdo-models/main.php +++ b/examples/pdo-models/main.php @@ -1,7 +1,4 @@ createTable(); -// insert users $repo->insert("Alice", "alice@example.com", 30); $repo->insert("Bob", "bob@example.com", 25); $repo->insert("Charlie", "charlie@example.com", 35); echo "inserted 3 users\n"; echo "count: " . $repo->count() . "\n"; -// find by id $user = $repo->findById(1); echo "by id: " . $user["name"] . " " . $user["email"] . "\n"; -// find by email $user = $repo->findByEmail("bob@example.com"); echo "by email: " . $user["name"] . " age " . $user["age"] . "\n"; -// list all $all = $repo->all(); foreach ($all as $u) { echo " " . $u["name"] . " (" . $u["email"] . ")\n"; } -// update $repo->updateAge(2, 26); $updated = $repo->findById(2); echo "updated age: " . $updated["age"] . "\n"; -// delete $repo->delete(3); echo "after delete: " . $repo->count() . "\n"; -// transaction test $db->getPdo()->beginTransaction(); $repo->insert("Dave", "dave@example.com", 40); $db->getPdo()->rollBack(); diff --git a/examples/pdo-sqlite-bookings/main.php b/examples/pdo-sqlite-bookings/main.php index 6dce9904..1c4fe584 100644 --- a/examples/pdo-sqlite-bookings/main.php +++ b/examples/pdo-sqlite-bookings/main.php @@ -1,7 +1,4 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); diff --git a/examples/phar-stream/main.php b/examples/phar-stream/main.php index c479e4e3..de18ef3a 100644 --- a/examples/phar-stream/main.php +++ b/examples/phar-stream/main.php @@ -1,8 +1,4 @@ ), first-class callable syntax with pipe, -// pipe chaining, pipe with closures, pipe with variables echo ("hello" |> strtoupper(...)), "\n"; // HELLO echo (" hello " |> trim(...) |> strlen(...)), "\n"; // 5 @@ -11,16 +9,13 @@ echo ("hello" |> strlen(...)), "\n"; // 5 -// pipe into static method class Str { public static function upper(string $s): string { return strtoupper($s); } } echo ("hello" |> Str::upper(...)), "\n"; // HELLO -// pipe result in expression $result = ("hello" |> strlen(...)) * 2; echo $result, "\n"; // 10 -// pipe with null coalesce $val = null; echo (($val ?? "default") |> strtoupper(...)), "\n"; // DEFAULT diff --git a/examples/process-control/main.php b/examples/process-control/main.php index 31107568..fc2c4225 100644 --- a/examples/process-control/main.php +++ b/examples/process-control/main.php @@ -1,14 +1,10 @@ $pid, 'expected' => $i * 2]; diff --git a/examples/query-builder/main.php b/examples/query-builder/main.php index 0855efac..bdfaf241 100644 --- a/examples/query-builder/main.php +++ b/examples/query-builder/main.php @@ -1,8 +1,4 @@ ), array_map/array_filter/array_column/array_slice, -// method chaining with self return, usort with callable comparator, -// fluent collection API, variadic params class Collection { @@ -230,7 +226,6 @@ public function toSql(): string } } -// seed test data QueryBuilder::seed("users", [ ["id" => 1, "name" => "Alice", "email" => "alice@test.com", "age" => 30, "role" => "admin"], ["id" => 2, "name" => "Bob", "email" => "bob@test.com", "age" => 25, "role" => "user"], @@ -239,44 +234,35 @@ public function toSql(): string ["id" => 5, "name" => "Eve", "email" => "eve@test.com", "age" => 22, "role" => "user"], ]); -// basic query $users = QueryBuilder::table("users")->get(); echo "total: " . $users->count() . "\n"; -// where clause $admins = QueryBuilder::table("users")->where("role", "=", "admin")->get(); echo "admins: " . $admins->count() . "\n"; echo "names: " . $admins->pluck("name")->implode(", ") . "\n"; -// chained wheres $youngUsers = QueryBuilder::table("users") ->where("role", "=", "user") ->where("age", "<", 30) ->get(); echo "young users: " . $youngUsers->pluck("name")->implode(", ") . "\n"; -// order by $byAge = QueryBuilder::table("users")->orderBy("age", "DESC")->get(); echo "oldest first: " . $byAge->pluck("name")->implode(", ") . "\n"; -// limit/offset $page = QueryBuilder::table("users")->orderBy("id")->limit(2)->offset(2)->get(); echo "page: " . $page->pluck("name")->implode(", ") . "\n"; -// select specific columns $emails = QueryBuilder::table("users")->select("name", "email")->get(); $first = $emails->first(); echo "first: {$first['name']} <{$first['email']}>\n"; -// first() $alice = QueryBuilder::table("users")->where("name", "=", "Alice")->first(); echo "found: {$alice['name']} age {$alice['age']}\n"; -// count $userCount = QueryBuilder::table("users")->where("role", "=", "user")->count(); echo "user count: $userCount\n"; -// toSql $sql = QueryBuilder::table("users") ->select("name", "email") ->where("role", "=", "admin") @@ -286,27 +272,22 @@ public function toSql(): string ->toSql(); echo "sql: $sql\n"; -// collection operations $names = $users->pluck("name"); echo "all: " . $names->implode(", ") . "\n"; echo "contains Alice: " . var_export($names->contains("Alice"), true) . "\n"; echo "contains Zara: " . var_export($names->contains("Zara"), true) . "\n"; -// map + filter $ages = $users->map(function ($u) { return $u["age"]; }); $over25 = $ages->filter(function ($a) { return $a > 25; }); echo "ages over 25: " . $over25->implode(", ") . "\n"; -// reduce $totalAge = $ages->reduce(function ($carry, $age) { return $carry + $age; }, 0); echo "total age: $totalAge\n"; -// groupBy $grouped = $users->groupBy("role"); echo "admin group: " . count($grouped["admin"]) . "\n"; echo "user group: " . count($grouped["user"]) . "\n"; -// sortBy $sorted = $users->sortBy(function ($a, $b) { return $a["age"] <=> $b["age"]; }); echo "youngest: " . $sorted->first()["name"] . "\n"; diff --git a/examples/readonly-and-firstclass/main.php b/examples/readonly-and-firstclass/main.php index be9b207b..aef04eaa 100644 --- a/examples/readonly-and-firstclass/main.php +++ b/examples/readonly-and-firstclass/main.php @@ -1,7 +1,4 @@ getAddress()?->getFormatted() ?? "none") . "\n"; echo "nullsafe3: " . ($bob?->address?->city ?? "none") . "\n"; -// --- type hints --- function add_ints(int $a, int $b): int { return $a + $b; @@ -111,14 +101,12 @@ function nullable_str(?string $s): string { echo "nullable1: " . nullable_str("hello") . "\n"; echo "nullable2: " . nullable_str(null) . "\n"; -// type error catching try { add_ints("not", "ints"); } catch (TypeError $e) { echo "type_error: caught\n"; } -// --- spread in calls --- function sum3(int $a, int $b, int $c): int { return $a + $b + $c; @@ -132,7 +120,6 @@ function sum3(int $a, int $b, int $c): int { $merged = [...$first, ...$second]; echo "array_spread: " . implode(",", $merged) . "\n"; -// --- static:: in inheritance --- class Base { protected static string $type = "base"; diff --git a/examples/regex-processor/main.php b/examples/regex-processor/main.php index 481a8173..85f45091 100644 --- a/examples/regex-processor/main.php +++ b/examples/regex-processor/main.php @@ -1,15 +1,10 @@ \d{4})-(?P\d{2})-(?P\d{2})/', '2024-03-15', $matches); echo "Year: " . $matches['year'] . "\n"; echo "Month: " . $matches['month'] . "\n"; echo "Day: " . $matches['day'] . "\n"; -// test 4: preg_match_all echo "\n=== Test 4: Match All ===\n"; $count = preg_match_all('/\b[A-Z]\w+/', 'Alice met Bob at the Park on Sunday', $matches); echo "Count: $count\n"; echo "Words: " . implode(', ', $matches[0]) . "\n"; -// test 5: preg_replace echo "\n=== Test 5: Replace ===\n"; $result = preg_replace('/\d+/', '#', 'abc123def456ghi'); echo "Digits replaced: $result\n"; @@ -38,7 +30,6 @@ $result = preg_replace('/(\w+)@(\w+)\.(\w+)/', '$1 at $2 dot $3', 'user@example.com'); echo "Email: $result\n"; -// test 6: preg_replace_callback echo "\n=== Test 6: Replace Callback ===\n"; $result = preg_replace_callback('/\b\w+\b/', function($m) { return ucfirst(strtolower($m[0])); @@ -50,7 +41,6 @@ }, 'a1 b2 c3 d10'); echo "Doubled: $result\n"; -// test 7: preg_split echo "\n=== Test 7: Split ===\n"; $parts = preg_split('/[\s,;]+/', 'one, two; three four,,five'); echo "Parts: " . implode('|', $parts) . "\n"; @@ -72,26 +62,22 @@ echo $test[2] . ": " . $match . "\n"; } -// test 9: anchors and boundaries echo "\n=== Test 9: Anchors ===\n"; echo "Start: " . (preg_match('/^hello/', 'hello world') ? 'yes' : 'no') . "\n"; echo "End: " . (preg_match('/world$/', 'hello world') ? 'yes' : 'no') . "\n"; echo "Word boundary: " . (preg_match('/\bcat\b/', 'the cat sat') ? 'yes' : 'no') . "\n"; echo "No boundary: " . (preg_match('/\bcat\b/', 'concatenate') ? 'yes' : 'no') . "\n"; -// test 10: alternation echo "\n=== Test 10: Alternation ===\n"; preg_match('/^(cat|dog|bird)$/', 'dog', $m); echo "Animal: " . $m[1] . "\n"; echo "No match: " . (preg_match('/^(cat|dog|bird)$/', 'fish') ? 'yes' : 'no') . "\n"; -// test 11: modifiers echo "\n=== Test 11: Modifiers ===\n"; echo "Case insensitive: " . (preg_match('/hello/i', 'HELLO') ? 'yes' : 'no') . "\n"; echo "Multiline: " . (preg_match('/^world/m', "hello\nworld") ? 'yes' : 'no') . "\n"; echo "Dotall: " . (preg_match('/hello.world/s', "hello\nworld") ? 'yes' : 'no') . "\n"; -// test 12: practical patterns echo "\n=== Test 12: Practical Patterns ===\n"; $ipPattern = '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/'; echo "Valid IP: " . (preg_match($ipPattern, '192.168.1.1') ? 'yes' : 'no') . "\n"; @@ -100,7 +86,6 @@ $urlPattern = '/^https?:\/\/[\w.-]+(?:\/[\w.-]*)*\/?$/'; echo "Valid URL: " . (preg_match($urlPattern, 'https://example.com/path') ? 'yes' : 'no') . "\n"; -// test 13: replace with limit echo "\n=== Test 13: Replace with Limit ===\n"; $result = preg_replace('/\d/', 'X', 'a1b2c3d4', 2); echo "Limited: $result\n"; diff --git a/examples/regex-toolkit/main.php b/examples/regex-toolkit/main.php index 276deb25..99534f78 100644 --- a/examples/regex-toolkit/main.php +++ b/examples/regex-toolkit/main.php @@ -1,10 +1,5 @@ get("/", function ($params) { return "home"; }); -// string callable $router->get("/about", function ($params) { return "about page"; }); -// static method via array callable $router->get("/users", ["UserController", "index"]); $router->get("/users/:id", ["UserController", "show"]); -// instance method via array callable $ctrl = new UserController(); $router->get("/profile/:id", [$ctrl, "profile"]); -// route with multiple params $router->get("/posts/:year/:slug", function ($params) { return "post:" . $params["year"] . "/" . $params["slug"]; }); diff --git a/examples/scheduling-intervals/main.php b/examples/scheduling-intervals/main.php index 4261cdb6..670cb933 100644 --- a/examples/scheduling-intervals/main.php +++ b/examples/scheduling-intervals/main.php @@ -1,6 +1,4 @@ 'string'], "string type"); @@ -196,7 +185,6 @@ function assertInvalid($data, $schema, $expectedCount, $label) { assertInvalid(42, ['type' => 'string'], 1, "int is not string"); assertInvalid("42", ['type' => 'integer'], 1, "string is not integer"); -// --- test: string constraints --- echo "--- string constraints ---\n"; assertValid("hello", ['type' => 'string', 'minLength' => 3, 'maxLength' => 10], "string in range"); @@ -207,7 +195,6 @@ function assertInvalid($data, $schema, $expectedCount, $label) { assertValid("red", ['type' => 'string', 'enum' => ['red', 'green', 'blue']], "enum valid"); assertInvalid("purple", ['type' => 'string', 'enum' => ['red', 'green', 'blue']], 1, "enum invalid"); -// --- test: format validation --- echo "--- format validation ---\n"; assertValid("user@example.com", ['type' => 'string', 'format' => 'email'], "valid email"); @@ -219,14 +206,12 @@ function assertInvalid($data, $schema, $expectedCount, $label) { assertValid("192.168.1.1", ['type' => 'string', 'format' => 'ipv4'], "valid ipv4"); assertInvalid("999.1.1.1", ['type' => 'string', 'format' => 'ipv4'], 1, "invalid ipv4"); -// --- test: numeric constraints --- echo "--- numeric constraints ---\n"; assertValid(5, ['type' => 'integer', 'minimum' => 0, 'maximum' => 10], "int in range"); assertInvalid(-1, ['type' => 'integer', 'minimum' => 0], 1, "int below minimum"); assertInvalid(100, ['type' => 'integer', 'maximum' => 50], 1, "int above maximum"); -// --- test: array validation --- echo "--- array validation ---\n"; assertValid([1, 2, 3], ['type' => 'array', 'items' => ['type' => 'integer']], "int array"); @@ -235,7 +220,6 @@ function assertInvalid($data, $schema, $expectedCount, $label) { assertValid([1, 2, 3], ['type' => 'array', 'minItems' => 2, 'maxItems' => 5], "array size ok"); assertInvalid([1], ['type' => 'array', 'minItems' => 2], 1, "array too small"); -// --- test: object validation --- echo "--- object validation ---\n"; @@ -270,7 +254,6 @@ function assertInvalid($data, $schema, $expectedCount, $label) { "multiple field errors" ); -// --- test: nested objects --- echo "--- nested objects ---\n"; diff --git a/examples/search-index/main.php b/examples/search-index/main.php index 07f0071e..ac829d46 100644 --- a/examples/search-index/main.php +++ b/examples/search-index/main.php @@ -1,5 +1,4 @@ > term => doc_id => term_freq */ @@ -144,7 +143,6 @@ function parseQuery(InvertedIndex $idx, string $q): array { $q = parseQuery($idx, '+zig compiler !php'); echo "parsed: must=" . implode(',', $q['must']) . " should=" . implode(',', $q['should']) . " not=" . implode(',', $q['must_not']) . "\n"; -// boolean query execution function executeBoolean(InvertedIndex $idx, array $q): array { $r = new ReflectionClass($idx); $postings = $r->getProperty('postings')->getValue($idx); @@ -193,7 +191,6 @@ function highlight(string $text, array $terms): string { $body = $data['docs'][3]['body']; echo highlight($body, ['zig', 'compiler', 'lexing']) . "\n"; -// mb_* on multibyte strings $mb = 'Café résumé naïve'; echo mb_strlen($mb) . " chars / " . strlen($mb) . " bytes\n"; echo mb_strtoupper($mb) . "\n"; diff --git a/examples/serialization-roundtrip/main.php b/examples/serialization-roundtrip/main.php index c68a4742..c47c6fa5 100644 --- a/examples/serialization-roundtrip/main.php +++ b/examples/serialization-roundtrip/main.php @@ -1,10 +1,5 @@ area(), 2) . "\n"; echo "instanceof Shape: " . ($back instanceof Shape ? 'yes' : 'no') . "\n"; -// --- array of objects --- echo "\n=== Array of Objects ===\n"; @@ -197,7 +185,6 @@ public function area(): float { echo "count: " . count($back) . "\n"; echo "B distance to C: " . round($back[1]->distanceTo($back[2]), 2) . "\n"; -// --- nested objects --- echo "\n=== Nested Objects ===\n"; @@ -226,7 +213,6 @@ public function itemCount(): int { echo "inner-1 contents: " . $back->contents[0]->contents[0]->label . "\n"; echo "inner-2 contents: " . $back->contents[1]->contents[0]->label . "\n"; -// --- json roundtrips --- echo "\n=== JSON Roundtrips ===\n"; @@ -246,7 +232,6 @@ public function itemCount(): int { echo "nested.b is null: " . (is_null($back['nested']['b']) ? 'yes' : 'no') . "\n"; echo "nested.c: " . $back['nested']['c'] . "\n"; -// --- large nested structure --- echo "\n=== Large Nested Structure ===\n"; diff --git a/examples/serialize-formats/main.php b/examples/serialize-formats/main.php index 49d1ff44..8f82c4ba 100644 --- a/examples/serialize-formats/main.php +++ b/examples/serialize-formats/main.php @@ -1,7 +1,4 @@ singleton("logger", function ($c) { @@ -126,7 +122,6 @@ public function send(string $to, string $message): void return new NotificationService($c->get("logger")); }); -// use services $userService = $container->get("users"); $userService->create("Alice", "alice@example.com"); $userService->create("Bob", "bob@example.com"); @@ -141,23 +136,19 @@ public function send(string $to, string $message): void echo $msg . "\n"; } -// verify singleton identity $userService2 = $container->get("users"); echo ($userService === $userService2) ? "singleton works" : "not singleton"; echo "\n"; -// binding creates new instances $notifier2 = $container->get("notifications"); echo ($notifier === $notifier2) ? "same" : "different instances"; echo "\n"; -// container has echo $container->has("logger") ? "has logger" : "no logger"; echo "\n"; echo $container->has("missing") ? "has missing" : "no missing"; echo "\n"; -// find user $user = $userService->find(1); echo $user["name"] . " " . $user["email"] . "\n"; diff --git a/examples/signed-cookies/main.php b/examples/signed-cookies/main.php index d477995e..5cbb8fb7 100644 --- a/examples/signed-cookies/main.php +++ b/examples/signed-cookies/main.php @@ -1,7 +1,4 @@ 1, 'name' => 'Alice', 'dept' => 'Engineering', 'salary' => 95000, 'active' => true], @@ -16,18 +14,15 @@ ['id' => 10, 'name' => 'Jack', 'dept' => 'Sales', 'salary' => 91000, 'active' => true], ]; -// --- SELECT (array_column) --- echo "All names:\n"; $names = array_column($employees, 'name'); echo implode(", ", $names) . "\n"; -// keyed by id $name_by_id = array_column($employees, 'name', 'id'); echo "Employee 3: " . $name_by_id[3] . "\n"; echo "Employee 7: " . $name_by_id[7] . "\n"; -// --- WHERE (array_filter) --- echo "\nActive engineers:\n"; $active_eng = array_filter($employees, function($e) { @@ -37,7 +32,6 @@ echo " " . $e['name'] . " - $" . number_format($e['salary']) . "\n"; } -// filter with regex echo "\nNames starting with vowels:\n"; $vowel_names = array_filter($employees, function($e) { return preg_match('/^[AEIOU]/i', $e['name']); @@ -46,7 +40,6 @@ echo " " . $e['name'] . "\n"; } -// --- ORDER BY (usort) --- echo "\nTop earners:\n"; $sorted = $employees; @@ -91,18 +84,15 @@ echo " Range: $" . number_format($min_sal) . " - $" . number_format($max_sal) . "\n"; } -// --- DISTINCT --- echo "\nDistinct departments: "; echo implode(", ", $depts) . "\n"; -// --- COUNT with condition --- $active_count = count(array_filter($employees, function($e) { return $e['active']; })); $inactive_count = count($employees) - $active_count; echo "Active: $active_count, Inactive: $inactive_count\n"; -// --- array_count_values --- echo "\nEmployees per department:\n"; $dept_counts = array_count_values(array_column($employees, 'dept')); @@ -125,7 +115,6 @@ } } -// --- UPDATE (array_walk) --- echo "\nAfter 10% raise for sales:\n"; $updated = $employees; @@ -156,7 +145,6 @@ echo " " . str_pad($emp['name'], 10) . $p['project'] . "\n"; } -// --- LIMIT/OFFSET (array_slice) --- echo "\nPage 2 (3 per page):\n"; $page = array_slice($employees, 3, 3); @@ -164,7 +152,6 @@ echo " " . $e['id'] . ". " . $e['name'] . "\n"; } -// --- compact/extract round-trip --- echo "\nCompact/extract:\n"; $name = "TestUser"; @@ -180,7 +167,6 @@ extract($record); echo "Extract: name=$name, role=$role, level=$level\n"; -// --- array_search --- echo "\nSearch:\n"; $names = array_column($employees, 'name'); @@ -198,7 +184,6 @@ echo " Batch " . ($i + 1) . ": " . implode(", ", $names) . "\n"; } -// --- table display --- echo "\nEmployee table:\n"; $header = sprintf("%-4s %-10s %-13s %10s %s", "ID", "Name", "Dept", "Salary", "Status"); diff --git a/examples/slim-app/main.php b/examples/slim-app/main.php index 7282fc67..c19aa787 100644 --- a/examples/slim-app/main.php +++ b/examples/slim-app/main.php @@ -1,7 +1,4 @@ getState()->label() . "\n"; @@ -196,7 +186,6 @@ function buildOrderMachine(): StateMachine echo "caught: " . $e->getMessage() . "\n"; } -// === test: try/catch/finally === $order2 = buildOrderMachine(); $cleanupRan = false; @@ -212,7 +201,6 @@ function buildOrderMachine(): StateMachine echo "finally ran: " . ($cleanupRan ? "yes" : "no") . "\n"; echo "state after error: " . $order2->getState()->label() . "\n"; -// === test: guard rejection === $order3 = buildOrderMachine(); $order3->addGuard(OrderStatus::Pending, OrderStatus::Paid, function ($from, $to) { @@ -225,7 +213,6 @@ function buildOrderMachine(): StateMachine echo "guard: " . $e->getMessage() . "\n"; } -// === test: enum methods === $statuses = OrderStatus::cases(); $labels = array_map(function ($s) { return $s->label(); }, $statuses); @@ -239,7 +226,6 @@ function buildOrderMachine(): StateMachine $cancelNames = array_map(function ($s) { return $s->value; }, array_values($cancellable)); echo "cancellable: " . implode(", ", $cancelNames) . "\n"; -// === test: from/tryFrom === $found = OrderStatus::from("shipped"); echo "from: " . $found->label() . "\n"; @@ -258,11 +244,9 @@ function buildOrderMachine(): StateMachine $top = $order4->getHistory()->top(); echo "last transition to: " . $top->to . "\n"; -// === test: static property === echo "machines created: " . StateMachine::getInstanceCount() . "\n"; -// === test: output buffering === ob_start(); echo "buffered content"; diff --git a/examples/stdlib-workout/main.php b/examples/stdlib-workout/main.php index cf724380..aad5b595 100644 --- a/examples/stdlib-workout/main.php +++ b/examples/stdlib-workout/main.php @@ -1,20 +1,5 @@ Hello World

", "") . "\n"; echo html_entity_decode("<div>test</div>") . "\n"; -// --- csv parsing --- echo "\n=== CSV Parsing ===\n"; @@ -85,7 +67,6 @@ $avg_age = array_sum($ages) / count($ages); echo sprintf("Average age: %.1f\n", $avg_age); -// --- array operations --- echo "\n=== Array Operations ===\n"; @@ -134,7 +115,6 @@ echo "Flipped: "; print_r($flipped); -// --- array set operations --- echo "\n=== Array Set Operations ===\n"; @@ -152,7 +132,6 @@ echo "Diff key: "; print_r($diff_keys); -// --- array splice --- echo "\n=== Array Splice ===\n"; @@ -161,7 +140,6 @@ echo "After splice: " . implode(', ', $arr) . "\n"; echo "Removed: " . implode(', ', $removed) . "\n"; -// --- sorting --- echo "\n=== Sorting ===\n"; @@ -182,7 +160,6 @@ echo sprintf(" %s: $%.2f\n", $item['name'], $item['price']); } -// --- functional array ops --- echo "\n=== Functional ===\n"; @@ -203,7 +180,6 @@ }); echo "Walked: " . implode(', ', $walked) . "\n"; -// --- compact/extract --- echo "\n=== Compact/Extract ===\n"; @@ -217,7 +193,6 @@ extract($record); echo "$color $size $qty\n"; -// --- regex --- echo "\n=== Regex ===\n"; @@ -234,7 +209,6 @@ $parts = preg_split('/[\s,;]+/', "one, two; three four"); echo "Split: " . implode(' | ', $parts) . "\n"; -// --- math --- echo "\n=== Math ===\n"; @@ -256,7 +230,6 @@ echo "bindec('11111111'): " . bindec('11111111') . "\n"; echo "octdec('377'): " . octdec('377') . "\n"; -// --- type juggling --- echo "\n=== Type Juggling ===\n"; @@ -273,7 +246,6 @@ settype($var, "integer"); echo "settype to int: " . $var . " (" . gettype($var) . ")\n"; -// --- date/time --- echo "\n=== Date/Time ===\n"; diff --git a/examples/stream-pipeline/main.php b/examples/stream-pipeline/main.php index 57df5c77..4f5c8dc8 100644 --- a/examples/stream-pipeline/main.php +++ b/examples/stream-pipeline/main.php @@ -1,7 +1,4 @@ name = "test"; echo "object: {$obj->name}\n"; -// --- multiline string operations --- echo "--- multiline ---\n"; $multi = "line 1\nline 2\nline 3\nline 4\nline 5"; @@ -81,7 +72,6 @@ $joined = implode(" | ", $lines); echo "joined: $joined\n"; -// --- padding and formatting --- echo "--- formatting ---\n"; echo str_pad("left", 10) . "|\n"; @@ -95,7 +85,6 @@ echo sprintf("%+d", 42) . "\n"; echo sprintf("%+d", -42) . "\n"; -// --- edge cases --- echo "--- edge cases ---\n"; echo "empty concat: " . "" . "" . "" . "end\n"; @@ -104,7 +93,6 @@ echo "empty length: " . strlen($empty) . "\n"; echo "null coalesce: " . ($empty ?: "default") . "\n"; -// single char operations $ch = "A"; echo "ord: " . ord($ch) . "\n"; echo "chr: " . chr(65) . "\n"; diff --git a/examples/string-processing/main.php b/examples/string-processing/main.php index b0bf679c..a7b98963 100644 --- a/examples/string-processing/main.php +++ b/examples/string-processing/main.php @@ -1,13 +1,5 @@ '" . slugify($title) . "'\n"; } -// --- template variable replacement --- echo "\n=== Template Replacement ===\n"; @@ -89,7 +78,6 @@ function slugify($text) { $after_fox = strstr($haystack, 'fox'); echo "from first fox: $after_fox\n"; -// --- encoding/decoding --- echo "\n=== Encoding ===\n"; @@ -110,7 +98,6 @@ function slugify($text) { echo "url encoded: $url_enc\n"; echo "url decoded matches: " . (urldecode($url_enc) === $url_data ? 'yes' : 'no') . "\n"; -// --- regex extraction --- echo "\n=== Regex Extraction ===\n"; @@ -196,7 +183,6 @@ function slugify($text) { $chars = array_map(function($h) { return chr(intval($h, 16)); }, $pairs); echo "hex decoded: " . implode('', $chars) . "\n"; -// --- word manipulation --- echo "\n=== Word Manipulation ===\n"; @@ -207,7 +193,6 @@ function slugify($text) { $wrapped = wordwrap("The quick brown fox jumps over the lazy dog and then runs away", 25, "\n", true); echo "wrapped:\n$wrapped\n"; -// --- substr_replace --- echo "\n=== Substr Replace ===\n"; @@ -216,7 +201,6 @@ function slugify($text) { echo "replace end: " . substr_replace($str, "PHP", 6, 5) . "\n"; echo "truncate+append: " . substr_replace($str, "...", 5) . "\n"; -// --- sprintf formatting --- echo "\n=== Sprintf ===\n"; @@ -226,7 +210,6 @@ function slugify($text) { echo sprintf("Float: %.4f, Sci: %e", 3.14159, 0.00123) . "\n"; echo sprintf("Padded: %05d", 42) . "\n"; -// --- addslashes/stripslashes --- echo "\n=== Escape/Unescape ===\n"; @@ -236,7 +219,6 @@ function slugify($text) { echo "unescaped: " . stripslashes($escaped) . "\n"; echo "roundtrip: " . (stripslashes(addslashes($dangerous)) === $dangerous ? 'yes' : 'no') . "\n"; -// --- preg_split --- echo "\n=== Preg Split ===\n"; @@ -248,7 +230,6 @@ function slugify($text) { $clean = preg_split('/\s*,\s*/', $csv_messy); echo "cleaned csv: " . implode('|', $clean) . "\n"; -// --- case-insensitive replace --- echo "\n=== Case-Insensitive Replace ===\n"; @@ -256,7 +237,6 @@ function slugify($text) { $result = str_ireplace('cat', 'dog', $text); echo "$result\n"; -// --- first-class callable --- echo "\n=== First-Class Callable ===\n"; diff --git a/examples/string-similarity/main.php b/examples/string-similarity/main.php index 0697814a..d49eb238 100644 --- a/examples/string-similarity/main.php +++ b/examples/string-similarity/main.php @@ -1,9 +1,5 @@ %-12s distance=%d\n", $pair[0], $pair[1], $dist); } -// spell checker using levenshtein echo "\n=== spell checker ===\n"; $dictionary = ['accept', 'except', 'affect', 'effect', 'advice', 'advise', 'practice', 'practise', 'license', 'licence', 'principal', 'principle']; @@ -37,7 +32,6 @@ echo sprintf(" %-12s -> %-12s (distance: %d)\n", $word, $best, $best_dist); } -// similar_text: longest common subsequence matching echo "\n=== similar_text ===\n"; $comparisons = [ ['World', 'Word'], @@ -52,7 +46,6 @@ $pair[0], $pair[1], $common, $percent); } -// finding most similar strings echo "\n=== find closest match ===\n"; $target = 'javascript'; $candidates = ['java', 'typescript', 'coffeescript', 'livescript', 'javafx', 'ecmascript']; @@ -69,7 +62,6 @@ $i++; } -// soundex: phonetic algorithm echo "\n=== soundex ===\n"; $names = [ ['Robert', 'Rupert'], @@ -85,7 +77,6 @@ echo sprintf(" %-12s (%s) vs %-12s (%s) -> %s\n", $pair[0], $s1, $pair[1], $s2, $match); } -// phonetic search echo "\n=== phonetic search ===\n"; $people = ['Steven', 'Stephen', 'Stefan', 'Stephan', 'Steve', 'Stephanie', 'Stewart', 'Stuart']; @@ -99,14 +90,12 @@ } } -// metaphone echo "\n=== metaphone ===\n"; $words = ['Thompson', 'Thomson', 'Tompson', 'Wright', 'Right', 'Rite']; foreach ($words as $word) { echo sprintf(" %-12s -> %s\n", $word, metaphone($word)); } -// combined fuzzy matching score echo "\n=== combined fuzzy matching ===\n"; function fuzzyScore(string $a, string $b): array { $lev = levenshtein(strtolower($a), strtolower($b)); diff --git a/examples/system-info/main.php b/examples/system-info/main.php index 0744c907..788d3f4b 100644 --- a/examples/system-info/main.php +++ b/examples/system-info/main.php @@ -1,7 +1,4 @@ $fiber) { $result = $fiber->start(); if ($fiber->isSuspended()) { @@ -101,7 +92,6 @@ function fiberRunner(array $jobs): array { } } - // resume suspended fibers $rounds = 0; while (true) { $any_suspended = false; @@ -118,7 +108,6 @@ function fiberRunner(array $jobs): array { if (!$any_suspended || $rounds > 10) break; } - // collect final results foreach ($fibers as $name => $fiber) { if ($fiber->isTerminated()) { $results[$name] = ['status' => 'done', 'value' => $fiber->getReturn()]; @@ -128,7 +117,6 @@ function fiberRunner(array $jobs): array { return $results; } -// --- task queue --- $queue = new TaskQueue(); $queue->add('compute', function() { @@ -173,7 +161,6 @@ function fiberRunner(array $jobs): array { echo str_pad($name, 15) . (string)$result . "\n"; } -// --- fibers --- echo "\n--- fiber runner ---\n"; $results = fiberRunner([ 'counter' => function() { @@ -200,7 +187,6 @@ function fiberRunner(array $jobs): array { echo "\n"; } -// --- error handling --- echo "\n--- error handling ---\n"; $errors = []; set_error_handler(function($errno, $errstr) use (&$errors) { @@ -218,7 +204,6 @@ function fiberRunner(array $jobs): array { restore_error_handler(); -// --- exception chaining --- echo "\n--- exception chain ---\n"; try { try { @@ -232,7 +217,6 @@ function fiberRunner(array $jobs): array { echo "caused by: " . ($prev ? $prev->getMessage() : "none") . "\n"; } -// --- finally --- echo "\n--- finally ---\n"; $log = []; try { diff --git a/examples/template-compiler/main.php b/examples/template-compiler/main.php index 05b2e7ff..5889825c 100644 --- a/examples/template-compiler/main.php +++ b/examples/template-compiler/main.php @@ -1,11 +1,5 @@ 'World']); @@ -80,7 +72,6 @@ function resolve($data, $path) { ]); echo $result . "\n"; -// --- test: nested access --- echo "--- nested ---\n"; $result = compile("{{ user.name }} ({{ user.email }})", [ @@ -93,7 +84,6 @@ function resolve($data, $path) { ]); echo $result . "\n"; -// --- test: conditionals --- echo "--- conditionals ---\n"; $result = compile("{% if admin %}[ADMIN] {% endif %}{{ name }}", [ @@ -108,7 +98,6 @@ function resolve($data, $path) { ]); echo $result . "\n"; -// --- test: loops --- echo "--- loops ---\n"; $result = compile("{% for item in items %}* {{ item }}\n{% endfor %}", [ @@ -125,7 +114,6 @@ function resolve($data, $path) { ]); echo $result; -// --- test: filter pipeline --- function applyFilter($value, $filter) { switch ($filter) { @@ -160,7 +148,6 @@ function compileWithFilters($template, $data) { echo compileWithFilters("{{ name | upper | reverse }}", ['name' => 'hello']) . "\n"; echo compileWithFilters("{{ name | length }}", ['name' => 'hello']) . "\n"; -// --- test: HTML escaping --- function escapeHtml($str) { return str_replace( @@ -175,7 +162,6 @@ function escapeHtml($str) { echo escapeHtml($unsafe) . "\n"; echo escapeHtml('Tom & Jerry "friends" ') . "\n"; -// --- test: indentation helper --- function indent($text, $level, $char = ' ') { $prefix = str_repeat($char, $level); @@ -211,7 +197,6 @@ function indent($text, $level, $char = ' ') { echo "tags: " . implode(', ', $matches[1]) . "\n"; echo "count: " . count($matches[1]) . "\n"; -// --- test: token splitting --- echo "--- token split ---\n"; $expr = " hello + world - foo * bar "; diff --git a/examples/template-engine-ob/main.php b/examples/template-engine-ob/main.php index b59b7b0e..6dd050c5 100644 --- a/examples/template-engine-ob/main.php +++ b/examples/template-engine-ob/main.php @@ -1,7 +1,4 @@ \n" . $view->yieldSection('title', 'Default') . "\n"; echo "\n"; @@ -103,7 +99,6 @@ public static function conditional(bool $condition, callable $whenTrue, ?callabl echo "\n"; }; -// page template $page = function(View $view) use ($layout) { $view->extend($layout); @@ -133,7 +128,6 @@ public static function conditional(bool $condition, callable $whenTrue, ?callabl }); }; -// render $view = new View(); $view->assign('users', [ ['name' => 'Alice', 'role' => 'admin'], @@ -145,7 +139,6 @@ public static function conditional(bool $condition, callable $whenTrue, ?callabl $html = $view->render($page); echo $html . "\n"; -// demonstrate ob_get_level tracking echo "\n--- level tracking ---\n"; echo "level: " . ob_get_level() . "\n"; ob_start(); @@ -157,7 +150,6 @@ public static function conditional(bool $condition, callable $whenTrue, ?callabl echo "captured inner: " . trim($inner) . "\n"; echo "captured outer: " . trim($outer) . "\n"; -// demonstrate ob_get_length ob_start(); echo "hello"; echo " length: " . ob_get_length(); diff --git a/examples/template-engine/main.php b/examples/template-engine/main.php index 41245e72..051ae155 100644 --- a/examples/template-engine/main.php +++ b/examples/template-engine/main.php @@ -1,14 +1,5 @@ & "friends"'); @@ -285,7 +271,6 @@ public function toArray(): array { unset($ctx['age']); echo "after unset: " . count($ctx) . "\n"; -// --- filters --- echo "\n=== Filters ===\n"; $ctx->addFilter('upper', function(string $s): string { @@ -303,7 +288,6 @@ public function toArray(): array { echo $ctx->applyFilter('title', 'hELLO') . "\n"; echo $ctx->applyFilter('unknown', 'passthrough') . "\n"; -// --- late static binding --- echo "\n=== Late Static Binding ===\n"; @@ -323,7 +307,6 @@ public static function tag(): string { echo TextNode::tag() . "\n"; echo BlockNode::tag() . "\n"; -// --- compact/extract --- echo "\n=== Compact/Extract ===\n"; function buildContext(): array { @@ -363,7 +346,6 @@ function buildContext(): array { }, $types); echo "prefixes: " . implode(', ', $prefixes) . "\n"; -// --- spread and defaults --- echo "\n=== Spread ===\n"; function joinParts(string ...$parts): string { diff --git a/examples/text-analysis/main.php b/examples/text-analysis/main.php index 77bf839e..46bcaffd 100644 --- a/examples/text-analysis/main.php +++ b/examples/text-analysis/main.php @@ -1,10 +1,5 @@ 'the quick brown fox jumps over the lazy dog', @@ -77,7 +70,6 @@ function wordFrequency(string $text): array { return $freq; } -// compare documents pairwise $names = array_keys($documents); for ($i = 0; $i < count($names); $i++) { for ($j = $i + 1; $j < count($names); $j++) { @@ -95,7 +87,6 @@ function wordFrequency(string $text): array { } } -// find near-duplicate entries using levenshtein echo "\n=== near-duplicate detection ===\n"; $entries = [ 'John Smith', @@ -129,7 +120,6 @@ function wordFrequency(string $text): array { echo " group " . ($idx + 1) . ": " . implode(', ', $group) . "\n"; } -// password strength analysis using count_chars echo "\n=== password analysis ===\n"; $passwords = ['abc123', 'P@ssw0rd!', 'correcthorsebatterystaple', 'aaa', 'Tr0ub4dor&3']; diff --git a/examples/timezone-dst/main.php b/examples/timezone-dst/main.php index 4faab6c0..6b809529 100644 --- a/examples/timezone-dst/main.php +++ b/examples/timezone-dst/main.php @@ -1,6 +1,4 @@ add(new Event( name: "Team standup", @@ -121,14 +114,12 @@ public function render(string $viewerTz): string { priority: Priority::CRITICAL, )); -// render from different viewer perspectives echo $schedule->render("UTC"); echo "\n"; echo $schedule->render("America/New_York"); echo "\n"; echo $schedule->render("Asia/Tokyo"); -// test timezone conversions echo "\n--- timezone math ---\n"; date_default_timezone_set("UTC"); $dt = new DateTime("2024-07-01 12:00:00", new DateTimeZone("UTC")); @@ -146,7 +137,6 @@ public function render(string $viewerTz): string { $dt->setTimezone(new DateTimeZone("Asia/Kolkata")); echo "Kolkata: " . $dt->format("H:i T") . "\n"; -// test DateTimeZone::getOffset with summer/winter echo "\n--- DST offsets ---\n"; $tz_ny = new DateTimeZone("America/New_York"); @@ -162,7 +152,6 @@ public function render(string $viewerTz): string { $tz_tokyo = new DateTimeZone("Asia/Tokyo"); echo "Tokyo offset: " . $tz_tokyo->getOffset($winter) . "\n"; -// test date format specifiers echo "\n--- format specifiers ---\n"; date_default_timezone_set("America/New_York"); echo date("e", $base) . "\n"; @@ -170,10 +159,8 @@ public function render(string $viewerTz): string { echo date("P", $base) . "\n"; echo date("O", $base) . "\n"; -// ISO 8601 echo date("c", $base) . "\n"; -// filter by priority $critical = $schedule->byPriority(Priority::CRITICAL); echo "\ncritical events: " . count($critical) . "\n"; echo "name: " . $critical[array_key_first($critical)]->name . "\n"; diff --git a/examples/trait-composition/main.php b/examples/trait-composition/main.php index 9980abd7..0c2c85bd 100644 --- a/examples/trait-composition/main.php +++ b/examples/trait-composition/main.php @@ -1,7 +1,4 @@ log("test") . "\n"; echo $app->fileLog("test") . "\n"; -// test 3: trait-qualified alias echo "\n=== Test 3: Trait Alias ===\n"; trait Secret { public function reveal(): string { @@ -90,7 +85,6 @@ protected function toArray(): array { $u = new User("Bob", 25); echo $u->serialize() . "\n"; -// test 5: trait with interface echo "\n=== Test 5: Trait + Interface ===\n"; interface Displayable { public function toString(): string; @@ -107,7 +101,6 @@ class Widget implements Displayable { echo $w->toString() . "\n"; echo ($w instanceof Displayable ? "implements Displayable" : "no") . "\n"; -// test 6: trait properties echo "\n=== Test 6: Trait Properties ===\n"; trait HasCounter { private int $count = 0; @@ -171,7 +164,6 @@ public function process(string $key, string $value): void { echo "Cached: " . $svc->cacheGet('a') . ", " . $svc->cacheGet('b') . "\n"; echo "Events: " . implode(', ', $log) . "\n"; -// test 8: three-way conflict echo "\n=== Test 8: Three-way Conflict ===\n"; trait A { public function hello(): string { return "A"; } diff --git a/examples/trait-static-props/main.php b/examples/trait-static-props/main.php index b70e88bf..bbfcf4aa 100644 --- a/examples/trait-static-props/main.php +++ b/examples/trait-static-props/main.php @@ -1,6 +1,4 @@ 'Jane', 'city' => 'New York', 'active' => 1]; $qs = http_build_query($data); @@ -51,7 +44,6 @@ $nested_qs = http_build_query($nested); echo "nested: $nested_qs\n"; -// --- URL encoding --- echo "\n=== url encoding ===\n"; $raw = "hello world & goodbye/friend"; echo "urlencode: " . urlencode($raw) . "\n"; @@ -66,7 +58,6 @@ echo "double encode: " . urlencode($special) . "\n"; echo "double decode: " . urldecode(urlencode($special)) . "\n"; -// --- quoted printable --- echo "\n=== quoted printable ===\n"; $text = "Subject: Hello World\r\nThis line is fairly short."; $encoded = quoted_printable_encode($text); @@ -78,7 +69,6 @@ $qp_long = quoted_printable_encode($long); echo "long line wrapped: " . (str_contains($qp_long, "=\r\n") ? 'yes' : 'no') . "\n"; -// --- URL builder pattern --- echo "\n=== url builder ===\n"; function buildUrl($base, $path, $params = []) { $parts = parse_url($base); diff --git a/examples/uuid/main.php b/examples/uuid/main.php index ec4d260c..65d224cb 100644 --- a/examples/uuid/main.php +++ b/examples/uuid/main.php @@ -1,7 +1,4 @@ errorCount() . "\n"; echo $result . "\n"; -// === test: failing validation === $badData = [ "name" => "", @@ -293,7 +287,6 @@ public static function make(array $fieldRules): self echo "first name error: " . $result2->firstError("name") . "\n"; echo "first email error: " . $result2->firstError("email") . "\n"; -// === test: callback rules === $v2 = new Validator(); $v2->field("age", @@ -312,7 +305,6 @@ function ($val) { return $val !== 69; }, echo "age 10: " . var_export($result4->passes(), true) . "\n"; echo "age 10 error: " . $result4->firstError("age") . "\n"; -// === test: static factory === $v3 = Validator::make([ "username" => [new Required(), new MinLength(3)], @@ -323,7 +315,6 @@ function ($val) { return $val !== 69; }, echo "factory errors: " . $result5->errorCount() . "\n"; foreach ($result5->allErrors() as $e) echo " $e\n"; -// === test: missing fields === $result6 = $validator->validate([]); echo "empty data errors: " . $result6->errorCount() . "\n"; diff --git a/examples/weakmap-cache/main.php b/examples/weakmap-cache/main.php index 953062c5..f870f302 100644 --- a/examples/weakmap-cache/main.php +++ b/examples/weakmap-cache/main.php @@ -1,8 +1,4 @@ diff --git a/src/bytecode_format.zig b/src/bytecode_format.zig index ef73afe8..8538cfb7 100644 --- a/src/bytecode_format.zig +++ b/src/bytecode_format.zig @@ -12,7 +12,6 @@ const Allocator = std.mem.Allocator; const MAGIC = "ZPHPC\x00"; const FORMAT_VERSION: u16 = 7; -// tag bytes for serialized values const TAG_NULL: u8 = 0; const TAG_BOOL_FALSE: u8 = 1; const TAG_BOOL_TRUE: u8 = 2; @@ -42,16 +41,12 @@ const StringTable = struct { } }; -// ========================================================= -// serialization -// ========================================================= pub fn serialize(allocator: Allocator, result: *const CompileResult) ![]u8 { var buf = std.ArrayListUnmanaged(u8){}; var strtab = StringTable{}; defer strtab.deinit(allocator); - // first pass: intern all strings if (result.file_path.len > 0) _ = try strtab.intern(allocator, result.file_path); for (result.slot_names) |sn| _ = try strtab.intern(allocator, sn); try internChunkStrings(allocator, &strtab, &result.chunk); @@ -84,18 +79,15 @@ pub fn serialize(allocator: Allocator, result: *const CompileResult) ![]u8 { } } - // header try buf.appendSlice(allocator, MAGIC); try writeU16(&buf, allocator, FORMAT_VERSION); - // string table try writeU32(&buf, allocator, @intCast(strtab.entries.items.len)); for (strtab.entries.items) |s| { try writeU32(&buf, allocator, @intCast(s.len)); try buf.appendSlice(allocator, s); } - // top-level metadata try writeU16(&buf, allocator, result.local_count); try writeU16(&buf, allocator, @intCast(result.slot_names.len)); for (result.slot_names) |sn| { @@ -109,16 +101,13 @@ pub fn serialize(allocator: Allocator, result: *const CompileResult) ![]u8 { try buf.append(allocator, if (result.strict_types) 1 else 0); try writeU32(&buf, allocator, compiler.closureCounter()); - // main chunk try serializeChunk(&buf, allocator, &strtab, &result.chunk, result.source); - // functions try writeU32(&buf, allocator, @intCast(result.functions.items.len)); for (result.functions.items) |*func| { try serializeFunction(&buf, allocator, &strtab, func, result.source); } - // type hints try writeU32(&buf, allocator, @intCast(result.type_hints.items.len)); for (result.type_hints.items) |th| { try writeU32(&buf, allocator, try strtab.intern(allocator, th.name)); @@ -177,11 +166,9 @@ fn internValueStrings(allocator: Allocator, strtab: *StringTable, val: Value) !v } fn serializeChunk(buf: *std.ArrayListUnmanaged(u8), allocator: Allocator, strtab: *StringTable, chunk: *const Chunk, source: []const u8) !void { - // code try writeU32(buf, allocator, @intCast(chunk.code.items.len)); try buf.appendSlice(allocator, chunk.code.items); - // constants try writeU16(buf, allocator, @intCast(chunk.constants.items.len)); for (chunk.constants.items) |val| { try serializeValue(buf, allocator, strtab, val); @@ -318,9 +305,6 @@ fn writeF64(buf: *std.ArrayListUnmanaged(u8), allocator: Allocator, val: f64) !v try buf.appendSlice(allocator, &bytes); } -// ========================================================= -// deserialization -// ========================================================= const Reader = struct { data: []const u8, @@ -382,13 +366,11 @@ const DeserCtx = struct { pub fn deserialize(allocator: Allocator, data: []const u8) DeserializeError!CompileResult { var r = Reader{ .data = data }; - // header const magic = r.readSlice(6) catch return error.InvalidFormat; if (!std.mem.eql(u8, magic, MAGIC)) return error.InvalidFormat; const version = r.readU16() catch return error.InvalidFormat; if (version != FORMAT_VERSION) return error.InvalidFormat; - // string table const str_count = r.readU32() catch return error.InvalidFormat; var strings = try allocator.alloc([]const u8, str_count); var string_allocs = std.ArrayListUnmanaged([]const u8){}; @@ -406,7 +388,6 @@ pub fn deserialize(allocator: Allocator, data: []const u8) DeserializeError!Comp strings[i] = owned; } - // top-level metadata const local_count = r.readU16() catch return error.InvalidFormat; const slot_name_count = r.readU16() catch return error.InvalidFormat; const slot_names = allocator.alloc([]const u8, slot_name_count) catch return error.OutOfMemory; @@ -441,11 +422,9 @@ pub fn deserialize(allocator: Allocator, data: []const u8) DeserializeError!Comp .string_allocs = &string_allocs, }; - // main chunk var chunk = deserializeChunk(&r, &ctx) catch return error.InvalidFormat; errdefer chunk.deinit(allocator); - // functions const func_count = r.readU32() catch return error.InvalidFormat; var functions = std.ArrayListUnmanaged(ObjFunction){}; errdefer { @@ -464,7 +443,6 @@ pub fn deserialize(allocator: Allocator, data: []const u8) DeserializeError!Comp try functions.append(allocator, func); } - // type hints var type_hints = std.ArrayListUnmanaged(TypeHint){}; errdefer { for (type_hints.items) |th| if (th.param_types.len > 0) allocator.free(th.param_types); @@ -682,9 +660,7 @@ fn deserializeValue(r: *Reader, ctx: *DeserCtx) !Value { }; } -// ========================================================= // standalone executable support -// ========================================================= const TRAILER_MAGIC = "ZPHPEXE\x00"; const TRAILER_SIZE = 16; // 8 bytes magic + 4 bytes offset + 4 bytes length @@ -708,7 +684,6 @@ pub fn appendToExecutable(allocator: Allocator, exe_path: []const u8, bc_data: [ const len_bytes: [4]u8 = @bitCast(bc_length); try file.writeAll(&len_bytes); - // make executable const out_z = try allocator.dupeZ(u8, out_path); defer allocator.free(out_z); _ = std.c.chmod(out_z.ptr, 0o755); diff --git a/src/env.zig b/src/env.zig index 2ba69484..d7173320 100644 --- a/src/env.zig +++ b/src/env.zig @@ -14,7 +14,6 @@ pub fn loadEnvFile(allocator: std.mem.Allocator) void { while (i < content.len and (content[i] == ' ' or content[i] == '\t' or content[i] == '\r' or content[i] == '\n')) i += 1; if (i >= content.len) break; - // full-line comment if (content[i] == '#') { while (i < content.len and content[i] != '\n') i += 1; continue; diff --git a/src/fmt.zig b/src/fmt.zig index ed008714..f5e0c254 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -228,7 +228,6 @@ const Formatter = struct { } } - // walk nodes fn formatRoot(self: *Formatter) void { const root = self.ast.nodes[0]; @@ -263,7 +262,6 @@ const Formatter = struct { prev_tag = node.tag; } - // trailing newline if (self.out.items.len > 0 and self.out.items[self.out.items.len - 1] != '\n') { self.newline(); } @@ -552,7 +550,6 @@ const Formatter = struct { } } - // statements fn formatEcho(self: *Formatter, node: Ast.Node) void { const tok_lex = self.ast.tokens[node.main_token].lexeme(self.source); @@ -996,7 +993,6 @@ const Formatter = struct { self.write(";"); } - // classes fn formatClassDecl(self: *Formatter, node: Ast.Node) void { const name_tok = node.main_token; @@ -1147,9 +1143,7 @@ const Formatter = struct { var result = TokenModifiers{}; if (name_tok == 0) return result; var i: i64 = @as(i64, name_tok) - 1; - // skip `&` if present (return-by-reference) if (i >= 0 and self.ast.tokens[@intCast(i)].tag == .amp) i -= 1; - // skip `function` keyword if (i >= 0 and self.ast.tokens[@intCast(i)].tag == .kw_function) i -= 1 else return result; while (i >= 0) { const tag = self.ast.tokens[@intCast(i)].tag; @@ -1176,7 +1170,6 @@ const Formatter = struct { else => self.write("public "), } if (node.tag == .static_class_property) self.write("static "); - // emit type hint if present self.emitPropertyTypeHint(node.main_token); self.write(self.ast.tokens[node.main_token].lexeme(self.source)); if (node.data.lhs != 0) { @@ -1364,7 +1357,6 @@ const Formatter = struct { } } - // expressions fn formatBinaryOp(self: *Formatter, node: Ast.Node) void { self.formatNode(node.data.lhs); diff --git a/src/h2.zig b/src/h2.zig index 4c1bfeab..7a2b0a45 100644 --- a/src/h2.zig +++ b/src/h2.zig @@ -146,7 +146,6 @@ pub const H2Session = struct { while (sent < ulen) { const n = tls.write(self.io.ssl, data[sent..ulen]) catch |err| { if (err == error.WouldBlock) { - // buffer remaining for later try self.send_buf.appendSlice(self.allocator, data[sent..ulen]); return; } @@ -163,7 +162,6 @@ pub const H2Session = struct { while (sent < self.send_buf.items.len) { const n = tls.write(self.io.ssl, self.send_buf.items[sent..]) catch |err| { if (err == error.WouldBlock) { - // shift remaining to front const remaining = self.send_buf.items.len - sent; std.mem.copyForwards(u8, self.send_buf.items[0..remaining], self.send_buf.items[sent..]); self.send_buf.items.len = remaining; diff --git a/src/integration_tests.zig b/src/integration_tests.zig index 57fbb408..ce5437a6 100644 --- a/src/integration_tests.zig +++ b/src/integration_tests.zig @@ -21,9 +21,7 @@ fn expectOutput(source: []const u8, expected: []const u8) !void { try std.testing.expectEqualStrings(expected, vm.output.items); } -// ========================================================================== // basic operations -// ========================================================================== test "echo integer" { try expectOutput(" {count, stmt...} root, - // statements expression_stmt, // lhs = expression echo_stmt, // main_token = echo, lhs = extra index -> {count, expr...} return_stmt, // lhs = expression (0 = bare return) @@ -70,7 +69,6 @@ pub const Ast = struct { inline_html, // main_token = inline_html token empty_stmt, // no-op (e.g. __HALT_COMPILER terminator) - // literals integer_literal, float_literal, string_literal, @@ -81,24 +79,19 @@ pub const Ast = struct { variable_variable, // lhs = inner expression ($$var, ${expr}) identifier, - // binary (non-short-circuit, main_token = operator) binary_op, // lhs = left, rhs = right pipe_expr, // lhs = value, rhs = callable - // assignment (main_token = assignment operator) assign, // lhs = target, rhs = value - // unary prefix_op, // main_token = operator, lhs = operand postfix_op, // main_token = operator, lhs = operand - // short-circuit (main_token = operator) logical_and, // lhs, rhs logical_or, // lhs, rhs null_coalesce, // lhs, rhs ternary, // lhs = condition, rhs = extra index -> {then, else}. then=0 means short ternary - // postfix expressions call, // lhs = callee, rhs = extra index -> {count, arg...} array_access, // lhs = array, rhs = index expr array_push_target, // lhs = array ($arr[] push target) @@ -109,20 +102,16 @@ pub const Ast = struct { nullsafe_property_access, // main_token = ?->, lhs = object, rhs = property node nullsafe_method_call, // main_token = method name, lhs = object, rhs = extra index -> {count, arg...} - // casts cast_expr, // main_token = type identifier (int/string/etc), lhs = operand - // closures closure_expr, // main_token = function, lhs = extra index -> {count, param...}, rhs = extra index -> {body, use_count, use_vars...} - // exceptions throw_expr, // lhs = expression to throw print_expr, // lhs = expression to print; pushes int 1 try_catch, // lhs = try body, rhs = extra index -> {catch_count, catch_nodes..., finally_node_or_0} catch_clause, // main_token = variable, lhs = type name node (0 = catch all), rhs = body block - // classes class_decl, // main_token = class name, lhs = extra index -> {count, member_nodes...}, rhs = extra index -> {parent_node, implements_count, implements_nodes...} class_method, // main_token = method name, lhs = extra index -> {count, param...}, rhs = body block class_property, // main_token = property variable, lhs = default value (0 = none) @@ -144,28 +133,22 @@ pub const Ast = struct { dynamic_static_call, // main_token = 0, lhs = class name node, rhs = extra index -> {method_expr, count, arg...} static_prop_access, // main_token = $variable, lhs = class name node - // compound array_literal, // main_token = [, lhs = extra index -> {count, element...} array_element, // lhs = value, rhs = key (0 = no key) array_spread, // lhs = expression to spread grouped_expr, // lhs = inner expression - // multi-expression (for loop init/update) expr_list, // lhs = extra index -> {count, expr...} - // scope global_stmt, // lhs = extra index -> {count, variable_nodes...} static_var, // main_token = variable, lhs = default expression (0 = none) - // generators yield_expr, // lhs = value expression (0 = yield null) yield_pair_expr, // lhs = key expression, rhs = value expression yield_from_expr, // lhs = iterable expression - // variadic splat_expr, // lhs = expression to spread (used in function call args) - // first-class callable callable_ref, // lhs = callee expression (function name identifier node) // file inclusion @@ -173,11 +156,9 @@ pub const Ast = struct { // lhs = path expression require_expr, - // enums enum_decl, // main_token = enum name, lhs = extra index -> {count, member_nodes...}, rhs = extra index -> {backed_type_token_or_0, implements_count, impl_nodes...} enum_case, // main_token = case name, lhs = backing value expression (0 = none) - // namespaces namespace_decl, // main_token = namespace keyword, lhs = extra index -> {count, name_token_indices...} use_stmt, // main_token = use keyword, lhs = extra index -> {count, name_token_indices...}, rhs = alias token (0 = no alias) use_fn_stmt, // same layout as use_stmt but for 'use function' diff --git a/src/pipeline/bytecode.zig b/src/pipeline/bytecode.zig index e71f148b..3d877acd 100644 --- a/src/pipeline/bytecode.zig +++ b/src/pipeline/bytecode.zig @@ -75,7 +75,6 @@ pub const OpCode = enum(u8) { echo, halt, - // arrays array_new, // push new empty array array_push, // pop value, append to array at stack top array_set_elem, // pop value, pop key, set on array at stack top @@ -93,13 +92,11 @@ pub const OpCode = enum(u8) { cow_separate_local, // u16: slot - if the local holds a shared array, separate it in place (write unshared copy back to slot). non-vivifying, non-throwing, pushes NOTHING. emitted before by-ref native args / unset bases ensure_array_static_prop, // u16 class-name const, u16 prop-name const - read Class::$prop, vivify, COW-separate if shared + write back, push (for Class::$prop[]=/[k] op= writes) - // exceptions throw, push_handler, // u16: catch offset pop_handler, instance_check, // pop class name string, pop object, push bool - // classes class_decl, // u16: class name, u8: method count, then method_count * (u16 name, u8 arity) new_obj, // u16: class name constant, u8: arg count new_obj_dynamic, // class name on stack, u8: arg count @@ -125,19 +122,15 @@ pub const OpCode = enum(u8) { get_static_prop_dyn_both, // no operand: class name then property name on stack set_static_prop_dyn, // no operand: class name, property name, value on stack - // scope get_global, // u16: var name constant (copy from frame 0) get_static, // u16: var name constant, u16: func name constant (get persistent static) set_static, // u16: var name constant, u16: func name constant (save persistent static) - // variadic array_spread, // pop array, push each element onto the array below it splat_call, // pop array, spread as args to function call - // file inclusion require, // u8: 0=require, 1=require_once, 2=include, 3=include_once (path string on stack) - // foreach iteration iter_begin, // push index 0 (array already on stack) iter_check, // u16: exit offset. peek array+index, push key+value or jump iter_advance, // pop index, push index+1 @@ -171,16 +164,13 @@ pub const OpCode = enum(u8) { // only when something else still references it foreach_ref_bind, - // generators yield_value, // pop value, suspend generator, push received value on resume yield_pair, // pop value, pop key, suspend generator generator_return, // pop value, mark generator completed yield_from, // pop iterable, delegate yield, push return value - // object clone_obj, // shallow copy object on top of stack - // unset unset_var, // u16: name constant index - remove variable from current scope unset_prop, // u16: prop name - pop object, remove property unset_prop_dynamic, // pop prop name, pop object, remove property @@ -340,7 +330,6 @@ pub const OpCode = enum(u8) { // net stack effect: +1 = pushes a value, 0 = neutral, -1 = consumes one net pub fn stackEffect(self: OpCode) i8 { return switch (self) { - // push a value .constant, .op_null, .op_true, .op_false, .dup, .get_var, .get_local, .get_global, .get_static, .get_static_prop, .get_class_const, .array_new, .clone_obj, .isset_prop, .isset_index, @@ -511,6 +500,11 @@ pub const ObjFunction = struct { is_generator: bool = false, is_arrow: bool = false, is_static: bool = false, + // Class/trait method declaration metadata. Keeping this on the compiled + // function lets trait imports preserve modifiers instead of defaulting + // every imported method to public and non-final. + method_visibility: u8 = 0, // 0=public, 1=protected, 2=private + is_final: bool = false, // `&function foo() { ... }` - return is a reference, not a copy. when // such a function is called via `$r = &foo(...)`, the caller's $r binds // to the storage the function returned a ref to. the return-expression diff --git a/src/pipeline/compiler.zig b/src/pipeline/compiler.zig index c7321908..13f80ae1 100644 --- a/src/pipeline/compiler.zig +++ b/src/pipeline/compiler.zig @@ -272,9 +272,7 @@ pub const Compiler = struct { source_offset: usize, }; - // ================================================================== // node dispatch - // ================================================================== pub fn compileNode(self: *Compiler, idx: u32) Error!void { // early-bound class/interface declarations are relocated to a prelude @@ -364,7 +362,6 @@ pub const Compiler = struct { } self.finally_depth = saved_fd; if (ref_return_emitted) { - // tryCompileRefReturn already emitted return_ref } else if (node.data.lhs != 0) { try self.emitOp(.return_val); } else { @@ -577,9 +574,7 @@ pub const Compiler = struct { } } - // ================================================================== // literals - // ================================================================== fn compileInteger(self: *Compiler, node: Ast.Node) Error!void { const lexeme = self.ast.tokenSlice(node.main_token); @@ -595,9 +590,7 @@ pub const Compiler = struct { try self.emitConstant(idx); } - // ================================================================== // variables - // ================================================================== pub fn compileGetVar(self: *Compiler, node: Ast.Node) Error!void { const name = self.ast.tokenSlice(node.main_token); @@ -717,9 +710,7 @@ pub const Compiler = struct { return "."; } - // ================================================================== // destructuring - // ================================================================== pub fn compileDestructure(self: *Compiler, target: Ast.Node) Error!void { if (target.tag == .list_destructure) { @@ -868,9 +859,7 @@ pub const Compiler = struct { try self.emitU16(name_idx); } - // ================================================================== // const expression evaluation - // ================================================================== // stringify a folded constant scalar for compile-time concat. returns null // for values that can't be folded here (arrays, deferred-constant / @@ -1116,7 +1105,6 @@ pub const Compiler = struct { }; for (known) |k| if (std.mem.eql(u8, k.n, prop_name)) break :blk Value{ .int = k.v }; } - // encode as deferred sentinel: "\x00CC\x00ClassName\x00CONST_NAME" const sentinel = std.fmt.allocPrint(self.allocator, "\x00CC\x00{s}\x00{s}", .{ class_name, prop_name }) catch break :blk Value.null; self.string_allocs.append(self.allocator, sentinel) catch break :blk Value.null; break :blk .{ .string = sentinel }; @@ -1163,9 +1151,7 @@ pub const Compiler = struct { }; } - // ================================================================== // name resolution - // ================================================================== pub fn resolveClassName(self: *Compiler, name: []const u8) []const u8 { if (name.len > 0 and name[0] == '\\') return name[1..]; @@ -1232,9 +1218,7 @@ pub const Compiler = struct { return self.ast.tokens[prop_node.main_token].tag == .variable; } - // ================================================================== // loop helpers - // ================================================================== pub fn patchBreaks(self: *Compiler, prev_breaks: *std.ArrayListUnmanaged(LoopJump)) Error!void { for (self.break_jumps.items) |bj| { @@ -1269,9 +1253,7 @@ pub const Compiler = struct { self.continue_jumps.deinit(self.allocator); } - // ================================================================== // slot management - // ================================================================== pub fn getOrCreateSlot(self: *Compiler, name: []const u8) u16 { if (self.local_slots.get(name)) |slot| return slot; @@ -1296,9 +1278,7 @@ pub const Compiler = struct { return names; } - // ================================================================== // variable emit helpers - // ================================================================== // for an arrow sub-compiler: is `name` bound in some enclosing scope, so // the arrow may capture it? walks the arrow_parent chain and forces every @@ -1432,9 +1412,7 @@ pub const Compiler = struct { return true; } - // ================================================================== // bytecode emit helpers - // ================================================================== pub fn emitOp(self: *Compiler, op: OpCode) Error!void { try self.chunk.write(self.allocator, @intFromEnum(op), self.current_source_offset); @@ -1489,9 +1467,7 @@ pub const Compiler = struct { return self.chunk.addConstant(self.allocator, value); } - // ================================================================== // number parsing - // ================================================================== pub fn parsePhpInt(s: []const u8) i64 { return switch (parsePhpIntLiteral(s)) { diff --git a/src/pipeline/compiler_class.zig b/src/pipeline/compiler_class.zig index a5f5efa3..5ad2a726 100644 --- a/src/pipeline/compiler_class.zig +++ b/src/pipeline/compiler_class.zig @@ -458,7 +458,6 @@ fn buildTypeString(self: *Compiler, start_tok: u32, end_tok: u32) Error![]const const tag = self.ast.tokens[i].tag; const lexeme = self.ast.tokens[i].lexeme(self.ast.source); - // separators reset the per-segment state const is_separator = std.mem.eql(u8, lexeme, "|") or std.mem.eql(u8, lexeme, "&") or std.mem.eql(u8, lexeme, "(") or std.mem.eql(u8, lexeme, ")"); @@ -735,7 +734,6 @@ pub fn compileFunction(self: *Compiler, node: Ast.Node) Error!void { param_names[i] = self.ast.tokenSlice(pnode.main_token); ref_flags[i] = (pnode.data.rhs & 2) != 0; if ((pnode.data.rhs & 1) != 0) { - // variadic param - always last is_variadic = true; try defaults.append(self.allocator, .null); } else if (pnode.data.lhs != 0) { @@ -855,7 +853,6 @@ pub fn compileFunction(self: *Compiler, node: Ast.Node) Error!void { if (return_type.len > 0) self.functions.items[self.functions.items.len - 1].return_type_kind = returnTypeKind(return_type); } - // function-level attributes if (!std.mem.startsWith(u8, name, "__closure_")) { const func_attrs = extractAttributes(self, node.main_token); if (func_attrs.len > 0) { @@ -1020,7 +1017,6 @@ pub fn compileClosure(self: *Compiler, node: Ast.Node) Error!void { break :blk rp; } else &[_]bool{}; - // check for ref captures var has_ref_capture = false; for (use_vars) |use_var_node| { if (self.ast.nodes[use_var_node].data.rhs != 0) { @@ -1065,7 +1061,6 @@ pub fn compileClosure(self: *Compiler, node: Ast.Node) Error!void { try self.type_hints.append(self.allocator, .{ .name = owned_name, .param_types = param_types, .return_type = return_type }); } - // closure-level attributes const closure_attrs = extractAttributes(self, node.main_token); if (closure_attrs.len > 0) { const attr_defs = try self.allocator.alloc(AttributeDef, closure_attrs.len); @@ -1336,7 +1331,6 @@ pub fn compileClassDecl(self: *Compiler, node: Ast.Node) Error!void { } } - // count promoted constructor params var promoted_count: u16 = 0; var constructor_params: []const u32 = &.{}; for (members) |member_idx| { @@ -1574,7 +1568,6 @@ pub fn compileClassDecl(self: *Compiler, node: Ast.Node) Error!void { try self.emitU16(try propertyTypeConst(self, tmember.data.rhs)); try self.emitU16(try docCommentConst(self, tmember.main_token)); } - // promoted constructor params as properties for (constructor_params) |p| { const pnode = self.ast.nodes[p]; const promotion = (pnode.data.rhs >> 2) & 3; @@ -1707,7 +1700,6 @@ pub fn compileClassDecl(self: *Compiler, node: Ast.Node) Error!void { } } - // class-level attributes const class_attrs = extractAttributes(self, node.main_token); try emitAttributeData(self, class_attrs); freeAttrSlice(self.allocator, class_attrs); @@ -1775,7 +1767,6 @@ pub fn compileClassDecl(self: *Compiler, node: Ast.Node) Error!void { } for (props_with_attrs.items) |pa| freeAttrSlice(self.allocator, pa.attrs); - // constant attributes var consts_with_attrs = std.ArrayListUnmanaged(MemberAttr){}; defer consts_with_attrs.deinit(self.allocator); for (members) |member_idx| { @@ -2176,7 +2167,6 @@ pub fn compileAnonymousClass(self: *Compiler, node: Ast.Node) Error!void { try self.emitByte(0); // constant attrs try self.emitByte(0); // param attrs - // now instantiate with constructor args const ctor_args_slice = self.ast.extra_data[ctor_args_start .. ctor_args_start + ctor_arg_count]; if (@import("compiler_expr.zig").hasSplatOrNamed(self.ast, ctor_args_slice)) { try @import("compiler_expr.zig").emitSpreadArgs(self, ctor_args_slice); @@ -2253,12 +2243,10 @@ pub fn compileInterfaceDecl(self: *Compiler, node: Ast.Node) Error!void { } } - // interface-level attributes const iface_attrs = extractAttributes(self, node.main_token); try emitAttributeData(self, iface_attrs); freeAttrSlice(self.allocator, iface_attrs); - // method attributes const MemberAttr = struct { name: []const u8, attrs: []const ParsedAttr }; var methods_with_attrs = std.ArrayListUnmanaged(MemberAttr){}; defer methods_with_attrs.deinit(self.allocator); @@ -2363,7 +2351,6 @@ pub fn compileTraitDecl(self: *Compiler, node: Ast.Node) Error!void { } } - // collect static property members var static_props = std.ArrayListUnmanaged(u32){}; defer static_props.deinit(self.allocator); for (members) |member_idx| { @@ -2442,12 +2429,10 @@ pub fn compileTraitDecl(self: *Compiler, node: Ast.Node) Error!void { try self.emitByte(if (pmember.data.lhs != 0) @as(u8, 1) else @as(u8, 0)); } - // trait-level attributes const trait_attrs = extractAttributes(self, node.main_token); try emitAttributeData(self, trait_attrs); freeAttrSlice(self.allocator, trait_attrs); - // method attributes const MemberAttr = struct { name: []const u8, attrs: []const ParsedAttr }; var methods_with_attrs = std.ArrayListUnmanaged(MemberAttr){}; defer methods_with_attrs.deinit(self.allocator); @@ -2471,7 +2456,6 @@ pub fn compileTraitDecl(self: *Compiler, node: Ast.Node) Error!void { } for (methods_with_attrs.items) |ma| freeAttrSlice(self.allocator, ma.attrs); - // property attributes var props_with_attrs = std.ArrayListUnmanaged(MemberAttr){}; defer props_with_attrs.deinit(self.allocator); for (members) |member_idx| { @@ -2575,7 +2559,6 @@ pub fn compileEnumDecl(self: *Compiler, node: Ast.Node) Error!void { try self.emitU16(iname_idx); } - // trait names var enum_traits = std.ArrayListUnmanaged([]const u8){}; defer enum_traits.deinit(self.allocator); for (members) |member_idx| { @@ -2597,12 +2580,10 @@ pub fn compileEnumDecl(self: *Compiler, node: Ast.Node) Error!void { try self.emitU16(tname_idx); } - // enum-level attributes const enum_attrs = extractAttributes(self, node.main_token); try emitAttributeData(self, enum_attrs); freeAttrSlice(self.allocator, enum_attrs); - // method attributes const MemberAttr = struct { name: []const u8, attrs: []const ParsedAttr }; var methods_with_attrs = std.ArrayListUnmanaged(MemberAttr){}; defer methods_with_attrs.deinit(self.allocator); @@ -2822,6 +2803,8 @@ fn compileClassMethodBody(self: *Compiler, class_name: []const u8, member: Ast.N .is_generator = method_gen, .returns_ref = method_returns_ref, .is_static = member.tag == .static_class_method, + .method_visibility = @intCast((member.data.rhs >> 30) & 0x3), + .is_final = ((member.data.rhs >> 28) & 1) != 0, .locals_only = method_lo, .params = param_names[0..param_nodes.len], .defaults = defaults_owned, @@ -3098,7 +3081,6 @@ fn opcodeWidth(b: u8) usize { .less_local_local_jif => 7, // 1 + u8 = 2 bytes .require, .call_indirect, .call_indirect_spread, .method_call_dynamic, .static_call_dyn_both => 2, - // variable-length: scan past inline operands .class_decl, .interface_decl, .enum_decl => 1, // all other opcodes are 1 byte (no operands) else => 1, diff --git a/src/pipeline/compiler_expr.zig b/src/pipeline/compiler_expr.zig index ca534fca..6c5cf6b4 100644 --- a/src/pipeline/compiler_expr.zig +++ b/src/pipeline/compiler_expr.zig @@ -858,7 +858,6 @@ pub fn compileCall(self: *Compiler, node: Ast.Node) Error!void { const prop_idx = try self.addConstant(.{ .string = prop_name }); try self.emitOp(.isset_prop); try self.emitU16(prop_idx); - // stack: [obj, bool] const false_jump = try self.emitJump(.jump_if_false); try self.emitOp(.pop); // drop true bool → [obj] try self.emitOp(.get_prop); @@ -1131,7 +1130,6 @@ pub fn compileCallableRef(self: *Compiler, node: Ast.Node) Error!void { try self.emitOp(.constant); try self.emitU16(idx); } else if (callee.tag == .method_call) { - // $obj->method(...) => [$obj, 'method'] try self.emitOp(.array_new); try self.compileNode(callee.data.lhs); try self.emitOp(.array_push); @@ -1141,7 +1139,6 @@ pub fn compileCallableRef(self: *Compiler, node: Ast.Node) Error!void { try self.emitU16(method_idx); try self.emitOp(.array_push); } else if (callee.tag == .static_call) { - // ClassName::method(...) => ['ClassName', 'method'] const class_node = self.ast.nodes[callee.data.lhs]; const class_name = try resolveNodeClassName(self, class_node); const method_name = self.ast.tokenSlice(callee.main_token); diff --git a/src/pipeline/compiler_stmt.zig b/src/pipeline/compiler_stmt.zig index 24c9a5be..c42a58e9 100644 --- a/src/pipeline/compiler_stmt.zig +++ b/src/pipeline/compiler_stmt.zig @@ -277,7 +277,6 @@ pub fn compileForeach(self: *Compiler, node: Ast.Node) Error!void { try self.string_allocs.append(self.allocator, synth); break :blk synth; }; - // stack: [key, value] try self.emitOp(.pop); // discard the value copy - we bind instead try self.emitSetVar(key_name); // $key = key (peek; key still on stack) try self.emitOp(.pop); // -> [] @@ -574,14 +573,12 @@ pub fn compileTryCatch(self: *Compiler, node: Ast.Node) Error!void { const handler_offset_pos = self.chunk.offset(); try self.emitU16(0xffff); - // compile try body try self.compileNode(node.data.lhs); // normal exit: pop handler and jump past catches try self.emitOp(.pop_handler); const skip_catches = try self.emitJump(.jump); - // patch catch offset to here self.patchJump(handler_offset_pos); // exception is on the stack when we arrive here @@ -630,7 +627,6 @@ pub fn compileTryCatch(self: *Compiler, node: Ast.Node) Error!void { // none matched, stack: [exc] - skip to next catch const skip = try self.emitJump(.jump); - // match: stack: [exc, bool(true)] for (match_jumps.items) |mj| self.patchJump(mj); try self.emitOp(.pop); // remove bool -> [exc] @@ -647,7 +643,6 @@ pub fn compileTryCatch(self: *Compiler, node: Ast.Node) Error!void { // skip lands here, stack: [exc] (preserved for next catch) self.patchJump(skip); } else { - // untyped catch-all if (catch_node.main_token != 0) { const var_name = self.ast.tokenSlice(catch_node.main_token); try self.emitSetVar(var_name); @@ -688,7 +683,6 @@ pub fn compileTryCatch(self: *Compiler, node: Ast.Node) Error!void { self.patchJump(skip_to_normal); try self.compileNode(finally_node); } else { - // no finally: just re-throw try self.emitOp(.throw); self.patchJump(skip_catches); diff --git a/src/pipeline/compiler_strings.zig b/src/pipeline/compiler_strings.zig index 8d4e6ad0..11e92211 100644 --- a/src/pipeline/compiler_strings.zig +++ b/src/pipeline/compiler_strings.zig @@ -305,7 +305,6 @@ fn emitInterpolationExpr(self: *Compiler, expr: []const u8) (Allocator.Error || j = paren_end + 1; } else if (j + 1 < expr.len and expr[j] == '-' and expr[j + 1] == '>') { j += 2; - // dynamic property name: {$obj->$var} if (j < expr.len and expr[j] == '$') { var k = j + 1; while (k < expr.len and isVarChar(expr[k])) k += 1; @@ -559,7 +558,6 @@ pub fn processEscapes(allocator: Allocator, s: []const u8) ![]const u8 { i += 2; }, 'x' => { - // \xNN hex escape var val: u8 = 0; var consumed: usize = 2; var digits: usize = 0; @@ -598,7 +596,6 @@ pub fn processEscapes(allocator: Allocator, s: []const u8) ![]const u8 { i += consumed; }, 'u' => { - // \u{NNNN} unicode escape if (i + 3 < s.len and s[i + 2] == '{') { var end = i + 3; while (end < s.len and s[end] != '}') end += 1; diff --git a/src/pipeline/lexer.zig b/src/pipeline/lexer.zig index 3f86e168..132c47f3 100644 --- a/src/pipeline/lexer.zig +++ b/src/pipeline/lexer.zig @@ -26,7 +26,6 @@ pub const Lexer = struct { }; } - // -- html mode --------------------------------------------------------- fn lexHtml(self: *Lexer) Token { const start = self.pos; @@ -132,7 +131,6 @@ pub const Lexer = struct { }; } - // -- operator lexers --------------------------------------------------- fn lexDot(self: *Lexer, start: usize) Token { if (self.pos < self.source.len and isDigit(self.source[self.pos])) { @@ -237,15 +235,12 @@ pub const Lexer = struct { } } - // skip to end of line while (self.pos < self.source.len and self.source[self.pos] != '\n') self.pos += 1; if (self.pos < self.source.len) self.pos += 1; // skip \n - // scan for closing label while (self.pos < self.source.len) { const line_start = self.pos; - // skip leading whitespace while (self.pos < self.source.len and (self.source[self.pos] == ' ' or self.source[self.pos] == '\t')) { self.pos += 1; } @@ -264,7 +259,6 @@ pub const Lexer = struct { } } - // skip to next line _ = line_start; while (self.pos < self.source.len and self.source[self.pos] != '\n') self.pos += 1; if (self.pos < self.source.len) self.pos += 1; @@ -316,7 +310,6 @@ pub const Lexer = struct { return self.makeToken(.question, start); } - // -- identifiers and keywords ------------------------------------------ fn lexIdentifier(self: *Lexer, start: usize) Token { while (self.pos < self.source.len and isIdentChar(self.source[self.pos])) self.pos += 1; @@ -325,7 +318,6 @@ pub const Lexer = struct { return self.makeToken(.identifier, start); } - // -- numbers ----------------------------------------------------------- fn lexNumber(self: *Lexer, start: usize) Token { const first = self.source[start]; @@ -430,7 +422,6 @@ pub const Lexer = struct { return true; } - // -- strings ----------------------------------------------------------- fn lexStringBody(self: *Lexer, quote: u8, start: usize) Token { var brace_depth: u32 = 0; @@ -521,7 +512,6 @@ pub const Lexer = struct { self.pos = self.source.len; } - // -- primitives -------------------------------------------------------- fn advance(self: *Lexer) u8 { const c = self.source[self.pos]; @@ -546,7 +536,6 @@ pub const Lexer = struct { return .{ .tag = .eof, .start = p, .end = p }; } - // -- character classification ------------------------------------------ fn isWhitespace(c: u8) bool { return c == ' ' or c == '\t' or c == '\n' or c == '\r'; @@ -569,9 +558,7 @@ pub const Lexer = struct { } }; -// ========================================================================== // tests -// ========================================================================== fn expectTokens(source: []const u8, expected: []const Tag) !void { var lexer = Lexer.init(source); diff --git a/src/pipeline/parser.zig b/src/pipeline/parser.zig index 4a4b34e8..4039e903 100644 --- a/src/pipeline/parser.zig +++ b/src/pipeline/parser.zig @@ -76,12 +76,9 @@ const Parser = struct { found_yield: bool = false, pending_top_stmts: std.ArrayListUnmanaged(u32) = .{}, - // ====================================================================== // root - // ====================================================================== fn parseRoot(self: *Parser) Error!void { - // reserve root at index 0 _ = try self.addNode(.{ .tag = .root, .main_token = 0, .data = .{} }); var stmts = std.ArrayListUnmanaged(u32){}; @@ -134,10 +131,6 @@ const Parser = struct { return self.addNode(.{ .tag = .echo_stmt, .main_token = echo_tok, .data = .{ .lhs = extra } }); } - // ====================================================================== - // statements - // ====================================================================== - fn parseStatement(self: *Parser) Error!u32 { if (self.peek() == .hash_bracket) { self.skipAttributes(); @@ -634,9 +627,7 @@ const Parser = struct { return self.addNode(.{ .tag = .block, .main_token = brace_tok, .data = .{ .lhs = extra } }); } - // ====================================================================== // control flow - // ====================================================================== fn parseIfStmt(self: *Parser) Error!u32 { return self.parseIfStmtInner(false); @@ -1085,7 +1076,6 @@ const Parser = struct { } } - // optional variable var var_tok: u32 = 0; if (self.peek() == .variable) { var_tok = self.advance(); @@ -1255,10 +1245,26 @@ const Parser = struct { return self.addNode(.{ .tag = .new_expr, .main_token = name_tok, .data = .{ .lhs = extra, .rhs = name_extra } }); } + // Property metadata stored in the low byte of class_property rhs. + // bits 0-1: read visibility, bit 2: readonly, bits 3-4: set visibility, + // bit 5: explicit asymmetric set visibility, bit 7: final. + inline fn encodePropertyFlags( + visibility: u32, + set_visibility: u32, + has_set_visibility: bool, + is_readonly: bool, + is_final: bool, + ) u32 { + return visibility | + (if (is_readonly) @as(u32, 1) << 2 else 0) | + (set_visibility << 3) | + (if (has_set_visibility) @as(u32, 1) << 5 else 0) | + (if (is_final) @as(u32, 1) << 7 else 0); + } + fn parseAnonymousClass(self: *Parser, new_tok: u32) Error!u32 { _ = self.advance(); // class - // constructor args var ctor_args = std.ArrayListUnmanaged(u32){}; defer ctor_args.deinit(self.allocator); if (self.peek() == .l_paren) { @@ -1341,6 +1347,15 @@ const Parser = struct { } if (!has_set_vis) set_visibility = visibility; + // PHP 8.4 asymmetric visibility is only valid on instance + // properties, and set visibility may not be wider than read visibility. + const starts_property = self.peek() == .variable or self.isTypeName() or + self.peek() == .question or self.peek() == .l_paren; + if (has_set_vis and (!starts_property or set_visibility < visibility or is_static)) { + try self.addError(.unexpected_token); + return error.ParseError; + } + if (self.peek() == .kw_use) { try members.append(self.allocator, try self.parseTraitUse()); } else if (self.peek() == .kw_function) { @@ -1355,11 +1370,16 @@ const Parser = struct { try members.append(self.allocator, method); } } else if (self.peek() == .variable) { + // Asymmetric set visibility requires an explicit property type. + if (has_set_vis) { + try self.addError(.unexpected_token); + return error.ParseError; + } const prop = try self.parseClassProperty(); if (is_static) { self.nodes.items[prop].tag = .static_class_property; } - self.nodes.items[prop].data.rhs = visibility | (if (is_readonly) @as(u32, 4) else 0) | (set_visibility << 3) | (if (has_set_vis) @as(u32, 1) << 5 else 0); + self.nodes.items[prop].data.rhs = encodePropertyFlags(visibility, set_visibility, has_set_vis, is_readonly, is_final); try members.append(self.allocator, prop); } else if (self.peek() == .kw_const) { const cd = try self.parseConstDecl(); @@ -1374,7 +1394,7 @@ const Parser = struct { if (is_static) { self.nodes.items[prop].tag = .static_class_property; } - var rhs: u32 = visibility | (if (is_readonly) @as(u32, 4) else 0) | (set_visibility << 3) | (if (has_set_vis) @as(u32, 1) << 5 else 0); + var rhs: u32 = encodePropertyFlags(visibility, set_visibility, has_set_vis, is_readonly, is_final); if (tr[0] != tr[1]) { const ext = try self.addExtra(&tr); rhs |= (ext + 1) << 16; @@ -1410,7 +1430,6 @@ const Parser = struct { self.found_yield = true; const tok = self.advance(); // yield - // yield from $expr if (self.peek() == .identifier) { const next_tok = self.tokens[self.pos].lexeme(self.source); if (std.mem.eql(u8, next_tok, "from")) { @@ -1420,7 +1439,6 @@ const Parser = struct { } } - // bare yield (no value) if (self.peek() == .semicolon or self.peek() == .r_paren or self.peek() == .r_bracket or self.peek() == .comma or self.peek() == .eof) { @@ -1429,7 +1447,6 @@ const Parser = struct { const expr = try self.parseExprPrec(1); - // yield $key => $value if (self.peek() == .fat_arrow) { _ = self.advance(); const value = try self.parseExprPrec(1); @@ -1519,6 +1536,15 @@ const Parser = struct { } if (!has_set_vis) set_visibility = visibility; + // PHP 8.4 asymmetric visibility is only valid on instance + // properties, and set visibility may not be wider than read visibility. + const starts_property = self.peek() == .variable or self.isTypeName() or + self.peek() == .question or self.peek() == .l_paren; + if (has_set_vis and (!starts_property or set_visibility < visibility or is_static)) { + try self.addError(.unexpected_token); + return error.ParseError; + } + if (self.peek() == .kw_use) { try members.append(self.allocator, try self.parseTraitUse()); } else if (self.peek() == .kw_function) { @@ -1537,12 +1563,17 @@ const Parser = struct { try members.append(self.allocator, method); } } else if (self.peek() == .variable) { + // Asymmetric set visibility requires an explicit property type. + if (has_set_vis) { + try self.addError(.unexpected_token); + return error.ParseError; + } const prop = try self.parseClassProperty(); if (is_static) { self.nodes.items[prop].tag = .static_class_property; } - // bits 0-1: read visibility, bit 2: readonly, bits 3-4: set visibility, bit 5: has asymmetric set - self.nodes.items[prop].data.rhs = visibility | (if (is_readonly) @as(u32, 4) else 0) | (set_visibility << 3) | (if (has_set_vis) @as(u32, 1) << 5 else 0); + // Property flags are encoded by encodePropertyFlags(); type metadata remains in bits 16+. + self.nodes.items[prop].data.rhs = encodePropertyFlags(visibility, set_visibility, has_set_vis, is_readonly, is_final); try members.append(self.allocator, prop); } else if (self.peek() == .kw_const) { const cd = try self.parseConstDecl(); @@ -1557,7 +1588,7 @@ const Parser = struct { if (is_static) { self.nodes.items[prop].tag = .static_class_property; } - var rhs: u32 = visibility | (if (is_readonly) @as(u32, 4) else 0) | (set_visibility << 3) | (if (has_set_vis) @as(u32, 1) << 5 else 0); + var rhs: u32 = encodePropertyFlags(visibility, set_visibility, has_set_vis, is_readonly, is_final); if (tr[0] != tr[1]) { const ext = try self.addExtra(&tr); rhs |= (ext + 1) << 16; @@ -1694,44 +1725,125 @@ const Parser = struct { while (self.peek() != .r_brace and self.peek() != .eof) { self.skipAttributes(); + var is_static = false; var is_abstract = false; + var is_final = false; + var is_readonly = false; + var visibility: u32 = 0; // 0=public, 1=protected, 2=private + var set_visibility: u32 = 0; + var has_set_vis = false; + while (self.peek() == .kw_public or self.peek() == .kw_protected or self.peek() == .kw_private or self.peek() == .kw_static or - self.peek() == .kw_abstract or self.peek() == .kw_readonly) + self.peek() == .kw_abstract or self.peek() == .kw_final or self.peek() == .kw_readonly) { - if (self.peek() == .kw_static) is_static = true; - if (self.peek() == .kw_abstract) is_abstract = true; + if (self.peek() == .kw_static) { + is_static = true; + _ = self.advance(); + continue; + } + if (self.peek() == .kw_abstract) { + is_abstract = true; + _ = self.advance(); + continue; + } + if (self.peek() == .kw_final) { + is_final = true; + _ = self.advance(); + continue; + } + if (self.peek() == .kw_readonly) { + is_readonly = true; + _ = self.advance(); + continue; + } + + const vis_val: u32 = + if (self.peek() == .kw_protected) 1 else if (self.peek() == .kw_private) 2 else 0; + _ = self.advance(); + + if (self.peek() == .l_paren and + self.peekAt(1) == .identifier and + std.mem.eql(u8, self.lexemeAt(1), "set") and + self.peekAt(2) == .r_paren) + { + _ = self.advance(); + _ = self.advance(); + _ = self.advance(); + set_visibility = vis_val; + has_set_vis = true; + } else { + visibility = vis_val; + } + } + + if (!has_set_vis) set_visibility = visibility; + + const starts_property = self.peek() == .variable or self.isTypeName() or + self.peek() == .question or self.peek() == .l_paren; + if (has_set_vis and (!starts_property or set_visibility < visibility or is_static)) { + try self.addError(.unexpected_token); + return error.ParseError; } if (self.peek() == .kw_function) { if (is_abstract) { - try members.append(self.allocator, try self.parseInterfaceMethod()); + const method = try self.parseInterfaceMethod(); + self.nodes.items[method].data.rhs |= visibility << 28; + try members.append(self.allocator, method); continue; } + const method = try self.parseClassMethod(); if (is_static) { self.nodes.items[method].tag = .static_class_method; } + self.nodes.items[method].data.rhs |= + (visibility << 30) | + (if (is_final) @as(u32, 1) << 28 else 0); try members.append(self.allocator, method); } else if (self.peek() == .variable) { + if (has_set_vis) { + try self.addError(.unexpected_token); + return error.ParseError; + } + const prop = try self.parseClassProperty(); if (is_static) { self.nodes.items[prop].tag = .static_class_property; } + self.nodes.items[prop].data.rhs = encodePropertyFlags( + visibility, + set_visibility, + has_set_vis, + is_readonly, + is_final, + ); try members.append(self.allocator, prop); } else if (self.isTypeName() or self.peek() == .question or self.peek() == .l_paren) { const tr = self.collectTypeHint(); + if (self.peek() == .variable) { const prop = try self.parseClassProperty(); if (is_static) { self.nodes.items[prop].tag = .static_class_property; } + + var rhs = encodePropertyFlags( + visibility, + set_visibility, + has_set_vis, + is_readonly, + is_final, + ); if (tr[0] != tr[1]) { const ext = try self.addExtra(&tr); - self.nodes.items[prop].data.rhs |= (ext + 1) << 16; + rhs |= (ext + 1) << 16; } + + self.nodes.items[prop].data.rhs = rhs; try members.append(self.allocator, prop); } else { _ = self.advance(); @@ -1744,10 +1856,14 @@ const Parser = struct { _ = self.advance(); } } - _ = try self.expect(.r_brace); + _ = try self.expect(.r_brace); const extra = try self.addExtraList(members.items); - return self.addNode(.{ .tag = .trait_decl, .main_token = name_tok, .data = .{ .lhs = extra } }); + return self.addNode(.{ + .tag = .trait_decl, + .main_token = name_tok, + .data = .{ .lhs = extra }, + }); } fn parseEnumDecl(self: *Parser) Error!u32 { @@ -2255,7 +2371,6 @@ const Parser = struct { return; } - // DNF: (Foo&Bar)|Baz if (self.peek() == .l_paren) { _ = self.advance(); while (self.isTypeName()) { @@ -2275,12 +2390,10 @@ const Parser = struct { if (!self.isTypeName()) return; self.skipTypeName(); - // union: int|string|null or intersection: Foo&Bar if (self.peek() == .pipe) { while (self.peek() == .pipe) { _ = self.advance(); if (self.peek() == .l_paren) { - // DNF group mid-union _ = self.advance(); while (self.isTypeName()) { self.skipTypeName(); @@ -2360,10 +2473,18 @@ const Parser = struct { break; } const type_range = self.collectTypeHint(); - // reference: &$param + + // PHP 8.4 asymmetric promoted-property validation. + if (set_promotion > 0) { + if (promotion == 0) promotion = 1; + + if (set_promotion < promotion or type_range[0] == type_range[1]) { + try self.addError(.unexpected_token); + return error.ParseError; + } + } const is_ref = self.peek() == .amp; if (is_ref) _ = self.advance(); - // variadic: ...$args const is_variadic = self.peek() == .ellipsis; if (is_variadic) _ = self.advance(); @@ -2388,9 +2509,7 @@ const Parser = struct { return self.addNode(.{ .tag = .variable, .main_token = tok, .data = .{ .lhs = default, .rhs = flags } }); } - // ====================================================================== // expressions - // ====================================================================== fn parseExpression(self: *Parser) Error!u32 { return self.parseExprPrec(0); @@ -2400,7 +2519,6 @@ const Parser = struct { var left = try self.parsePrefixExpr(); while (true) { - // postfix: call, index, property, increment/decrement switch (self.peek()) { .l_paren => { left = try self.parseCallExpr(left); @@ -2433,13 +2551,11 @@ const Parser = struct { else => {}, } - // ternary if (self.peek() == .question and infixPrec(.question) > min_prec) { left = try self.parseTernary(left); continue; } - // infix const prec = infixPrec(self.peek()); if (prec == 0 or prec <= min_prec) { // php allows assignment as rhs of any operator: false === $x = expr @@ -2814,14 +2930,11 @@ const Parser = struct { return self.addNode(.{ .tag = .array_element, .main_token = 0, .data = .{ .lhs = expr } }); } - // ====================================================================== // postfix expressions - // ====================================================================== fn parseCallExpr(self: *Parser, callee: u32) Error!u32 { const paren_tok = self.advance(); // ( - // first-class callable: foo(...) if (self.peek() == .ellipsis and self.peekAt(1) == .r_paren) { _ = self.advance(); // ... _ = self.advance(); // ) @@ -2864,7 +2977,6 @@ const Parser = struct { fn parseIndexExpr(self: *Parser, array: u32) Error!u32 { const bracket_tok = self.advance(); // [ if (self.peek() == .r_bracket) { - // $arr[] - array push syntax _ = self.advance(); return self.addNode(.{ .tag = .array_push_target, .main_token = bracket_tok, .data = .{ .lhs = array } }); } @@ -2876,7 +2988,6 @@ const Parser = struct { fn parsePropExpr(self: *Parser, object: u32, nullsafe: bool) Error!u32 { _ = self.advance(); // -> or ?-> - // dynamic property: $obj->{expr} if (self.peek() == .l_brace) { _ = self.advance(); const expr = try self.parseExpression(); @@ -2892,11 +3003,9 @@ const Parser = struct { } const name_tok = self.advance(); - // method call: $obj->method(...) or $obj?->method(...) if (self.peek() == .l_paren) { _ = self.advance(); // ( - // first-class callable: $obj->method(...) if (self.peek() == .ellipsis and self.peekAt(1) == .r_paren) { _ = self.advance(); // ... _ = self.advance(); // ) @@ -2922,7 +3031,6 @@ const Parser = struct { return self.addNode(.{ .tag = tag, .main_token = name_tok, .data = .{ .lhs = object, .rhs = extra } }); } - // property access: $obj->prop or $obj?->prop const prop = try self.addNode(.{ .tag = .identifier, .main_token = name_tok, .data = .{} }); const tag: Ast.Node.Tag = if (nullsafe) .nullsafe_property_access else .property_access; return self.addNode(.{ .tag = tag, .main_token = name_tok, .data = .{ .lhs = object, .rhs = prop } }); @@ -2973,7 +3081,6 @@ const Parser = struct { return self.addNode(.{ .tag = .static_prop_access, .main_token = class_tok, .data = .{ .lhs = class_node } }); } - // dynamic static call: Class::{expr}() if (self.peek() == .l_brace) { _ = self.advance(); // { const method_expr = try self.parseExpression(); @@ -3007,7 +3114,6 @@ const Parser = struct { if (self.peek() == .l_paren) { _ = self.advance(); - // first-class callable: ClassName::method(...) if (self.peek() == .ellipsis and self.peekAt(1) == .r_paren) { _ = self.advance(); // ... _ = self.advance(); // ) @@ -3051,9 +3157,7 @@ const Parser = struct { return self.addNode(.{ .tag = .ternary, .main_token = q_tok, .data = .{ .lhs = cond, .rhs = extra } }); } - // ====================================================================== // precedence - // ====================================================================== fn infixPrec(tag: Tag) u8 { return switch (tag) { @@ -3107,10 +3211,6 @@ const Parser = struct { }; } - // ====================================================================== - // utilities - // ====================================================================== - fn peek(self: *const Parser) Tag { if (self.pos >= self.tokens.len) return .eof; return self.tokens[self.pos].tag; diff --git a/src/pipeline/parser_tests.zig b/src/pipeline/parser_tests.zig index 5bf70697..a4a4d511 100644 --- a/src/pipeline/parser_tests.zig +++ b/src/pipeline/parser_tests.zig @@ -531,85 +531,237 @@ fn renderNode(ast: *const Ast, idx: u32, buf: *Buf) !void { } } -// ========================================================================== // tests -// ========================================================================== -test "integer literal" { try expectParse(" strlen(...);", "(|> $x (callable_ref strlen))"); } -test "pipe operator chained" { try expectParse(" trim(...) |> strtoupper(...);", "(|> (|> $x (callable_ref trim)) (callable_ref strtoupper))"); } -test "pipe precedence vs arithmetic" { try expectParse(" sqrt(...);", "(|> (+ 5 2) (callable_ref sqrt))"); } -test "pipe precedence vs comparison" { try expectParse(" strlen(...) == 4;", "(== (|> $x (callable_ref strlen)) 4)"); } -test "assignment" { try expectParse(" strlen(...);", "(|> $x (callable_ref strlen))"); +} +test "pipe operator chained" { + try expectParse(" trim(...) |> strtoupper(...);", "(|> (|> $x (callable_ref trim)) (callable_ref strtoupper))"); +} +test "pipe precedence vs arithmetic" { + try expectParse(" sqrt(...);", "(|> (+ 5 2) (callable_ref sqrt))"); +} +test "pipe precedence vs comparison" { + try expectParse(" strlen(...) == 4;", "(== (|> $x (callable_ref strlen)) 4)"); +} +test "assignment" { + try expectParse("b;", "(-> $a b)"); } -test "method call" { try expectParse("b();", "(-> $a b)"); } -test "chained access" { try expectParse("b->c;", "(-> (-> $a b) c)"); } -test "ternary" { try expectParse("b;", "(-> $a b)"); +} +test "method call" { + try expectParse("b();", "(-> $a b)"); +} +test "chained access" { + try expectParse("b->c;", "(-> (-> $a b) c)"); +} +test "ternary" { + try expectParse(" $b;", "(<=> $a $b)"); } -test "string concat" { try expectParse(" 1, 'b' => 2];", "['a' => 1, 'b' => 2]"); } -test "empty array" { try expectParse("HiBC", "(html) $a (html) (echo $b) (html)"); } -test "complex precedence" { try expectParse(" 1, 'b' => 2];", "['a' => 1, 'b' => 2]"); +} +test "empty array" { + try expectParse("HiBC", "(html) $a (html) (echo $b) (html)"); +} +test "complex precedence" { + try expectParse(" 0 and ver[0] == 'v') ver = ver[1..]; - // reject dev versions if (std.mem.startsWith(u8, ver, "dev-")) return .{ .valid = false }; if (std.mem.endsWith(u8, ver, "-dev")) return .{ .valid = false }; @@ -573,7 +570,6 @@ fn parseConstraintPart(raw: []const u8) ConstraintPart { var s = std.mem.trim(u8, raw, " "); if (s.len == 0 or std.mem.eql(u8, s, "*")) return .{ .kind = .any }; - // caret: ^1.2.3 -> >=1.2.3 <2.0.0 if (s[0] == '^') { const ver = parseSemVer(s[1..]); if (!ver.valid) return .{ .kind = .any }; @@ -606,10 +602,8 @@ fn parseConstraintPart(raw: []const u8) ConstraintPart { return .{ .kind = .tilde, .min = ver, .max = max }; } - // range: >=1.0 <2.0 (space separated) if (std.mem.startsWith(u8, s, ">=")) { const rest = s[2..]; - // check for space-separated upper bound if (std.mem.indexOf(u8, rest, " <")) |pos| { const min_str = std.mem.trim(u8, rest[0..pos], " "); const max_str = std.mem.trim(u8, rest[pos + 2 ..], " "); @@ -627,10 +621,8 @@ fn parseConstraintPart(raw: []const u8) ConstraintPart { return .{ .kind = .any }; } - // wildcard: 1.0.*, 1.* if (std.mem.endsWith(u8, s, ".*")) { const prefix = s[0 .. s.len - 2]; - // could be "1" or "1.0" if (std.mem.indexOf(u8, prefix, ".")) |dot| { const major = std.fmt.parseInt(u32, prefix[0..dot], 10) catch return .{ .kind = .any }; const minor = std.fmt.parseInt(u32, prefix[dot + 1 ..], 10) catch return .{ .kind = .any }; @@ -641,7 +633,6 @@ fn parseConstraintPart(raw: []const u8) ConstraintPart { } } - // exact version const ver = parseSemVer(s); if (ver.valid) return .{ .kind = .exact, .min = ver, .max = ver, .max_inclusive = true }; @@ -780,7 +771,6 @@ fn resolveAll( var queue = std.ArrayListUnmanaged(QueueItem){}; defer queue.deinit(allocator); - // seed with direct deps var it = direct_deps.iterator(); while (it.next()) |entry| { try queue.append(allocator, .{ @@ -833,7 +823,6 @@ fn resolveAll( } } -// commands pub fn install(allocator: Allocator) !void { const start = std.time.milliTimestamp(); @@ -1046,7 +1035,6 @@ pub fn add(allocator: Allocator, name: []const u8) !void { defer allocator.free(constraint); try upsertComposerRequire(allocator, name, constraint); - // regenerate autoloader from updated composer.json const source = std.fs.cwd().readFileAlloc(allocator, "composer.json", 1024 * 1024) catch ""; defer if (source.len > 0) allocator.free(source); @@ -1082,7 +1070,6 @@ pub fn remove(allocator: Allocator, name: []const u8) !void { ComposerJson{ .allocator = allocator }; defer composer.deinit(); - // read existing lock const existing = try readLockFile(allocator) orelse { tui.err("no zphp.lock found"); return; @@ -1156,7 +1143,6 @@ pub fn remove(allocator: Allocator, name: []const u8) !void { tui.blank(); } -// unit tests test "parseSemVer basic" { const v = parseSemVer("1.2.3"); @@ -1235,7 +1221,6 @@ test "tilde two-part constraint" { } test "tilde single-part constraint" { - // ~1 means >=1 <2 const c = parseConstraint("~1"); try std.testing.expect(c.parts[0].kind == .tilde); diff --git a/src/runtime/vm.zig b/src/runtime/vm.zig index 83f85925..a457e5ee 100644 --- a/src/runtime/vm.zig +++ b/src/runtime/vm.zig @@ -196,7 +196,6 @@ pub const NativeContext = struct { pub fn invokeCallableRef(self: *NativeContext, callable: Value, args: []Value) RuntimeError!Value { if (callable == .string) return self.vm.callByNameRef(callable.string, args); - // __invoke on an object instance if (callable == .object) { if (std.mem.eql(u8, callable.object.class_name, "Closure")) { return self.vm.callValueCallable(callable.object.get("__callable"), args); @@ -343,7 +342,9 @@ pub const ClassDef = struct { has_default: bool = false, visibility: Visibility = .public, set_visibility: Visibility = .public, + has_set_visibility: bool = false, is_readonly: bool = false, + is_final: bool = false, is_promoted: bool = false, type_str: []const u8 = "", doc_comment: []const u8 = "", @@ -935,7 +936,6 @@ pub const VM = struct { locals_buf: [*]Value = undefined, locals_sp: usize = 0, locals_cap: usize = 0, - // single-entry function lookup cache fn_cache_name: []const u8 = "", fn_cache_func: ?*const ObjFunction = null, // per-frame sp save for inline call/ret in fastLoop @@ -1632,7 +1632,6 @@ pub const VM = struct { try c.put(a, "E_DEPRECATED", .{ .int = 8192 }); try c.put(a, "E_USER_DEPRECATED", .{ .int = 16384 }); try c.put(a, "E_ALL", .{ .int = 30719 }); - // phpinfo() / php_ini section flags try c.put(a, "INFO_GENERAL", .{ .int = 1 }); try c.put(a, "INFO_CREDITS", .{ .int = 2 }); try c.put(a, "INFO_CONFIGURATION", .{ .int = 4 }); @@ -2963,7 +2962,6 @@ pub const VM = struct { .concat => { const b = self.pop(); const a = self.pop(); - // fast path: both strings if (a == .string and b == .string) { const as = a.string; const bs = b.string; @@ -3245,7 +3243,6 @@ pub const VM = struct { } if (has_named) { if (self.functions.get(name)) |func| { - // resolve named args to positional var resolved: [16]Value = .{.null} ** 16; var assigned: [16]bool = .{false} ** 16; var pos: usize = 0; @@ -3338,7 +3335,6 @@ pub const VM = struct { } } if (!ok) continue; - // fill defaults const count = @max(pos, func.required_params); for (0..count) |i| { if (resolved[i] == .null and i < func.defaults.len) { @@ -3401,7 +3397,6 @@ pub const VM = struct { } }, .call_indirect_spread => { - // stack: [... args_array, func_name] const name_val = self.pop(); const args_val = self.pop(); if (args_val != .array) { @@ -5446,7 +5441,6 @@ pub const VM = struct { ic.concat_buf.items.len >= current.string.len and current.string.ptr == ic.concat_buf.items.ptr) { - // append directly to the buffer if (append_val == .string) { try ic.concat_buf.appendSlice(self.allocator, append_val.string); } else { @@ -6078,7 +6072,9 @@ pub const VM = struct { gop.value_ptr.* = .{ .start = cap_pos, .len = 1, .has_refs = false }; } // closures defined inside class methods inherit class scope + const lexical_class = self.currentDefiningClass(); if (std.mem.eql(u8, var_name, "$this") and val == .object) { + // instance method closure: LSB = $this's runtime class try self.captures.append(self.allocator, .{ .closure_name = closure_name, .var_name = "$__closure_scope", @@ -6086,9 +6082,17 @@ pub const VM = struct { }); const gop2 = try self.capture_index.getOrPut(self.allocator, closure_name); gop2.value_ptr.len += 1; + if (lexical_class) |class_name| { + try self.captures.append(self.allocator, .{ + .closure_name = closure_name, + .var_name = "$__closure_defclass", + .value = .{ .string = class_name }, + }); + const gop3 = try self.capture_index.getOrPut(self.allocator, closure_name); + gop3.value_ptr.len += 1; + } } else if (!gop.found_existing) { - // first capture for this closure - check if we're in a static class method - const scope = self.currentFrame().called_class orelse self.currentDefiningClass(); + const scope = self.currentFrame().called_class orelse lexical_class; if (scope) |class_name| { try self.captures.append(self.allocator, .{ .closure_name = closure_name, @@ -6098,6 +6102,15 @@ pub const VM = struct { const gop2 = try self.capture_index.getOrPut(self.allocator, closure_name); gop2.value_ptr.len += 1; } + if (lexical_class) |class_name| { + try self.captures.append(self.allocator, .{ + .closure_name = closure_name, + .var_name = "$__closure_defclass", + .value = .{ .string = class_name }, + }); + const gop2 = try self.capture_index.getOrPut(self.allocator, closure_name); + gop2.value_ptr.len += 1; + } } }, @@ -6170,11 +6183,9 @@ pub const VM = struct { self.deinitFrameSlot(self.frame_count); } - // restore stack and push exception self.sp = handler.sp; self.push(exception); - // jump to catch code self.currentFrame().ip = handler.catch_ip; }, @@ -6591,12 +6602,10 @@ pub const VM = struct { try def.interfaces.append(self.allocator, parent_names[pi]); } - // interface-level attributes const iface_attrs = try self.readAttributeDefs(); for (iface_attrs) |a| try def.attributes.append(self.allocator, a); if (iface_attrs.len > 0) self.allocator.free(iface_attrs); - // method attributes const iface_method_attr_count = self.readByte(); for (0..iface_method_attr_count) |_| { const ma_name_idx = self.readU16(); @@ -6700,7 +6709,6 @@ pub const VM = struct { try self.trait_constants.put(self.allocator, trait_name, tcs); } - // trait-level attributes const trait_attrs = try self.readAttributeDefs(); const trait_method_attr_count = self.readByte(); var trait_method_attrs: [32]struct { name: []const u8, attrs: []const AttributeDef } = undefined; @@ -6896,7 +6904,6 @@ pub const VM = struct { return error.RuntimeError; } if (self.native_fns.get(cn)) |native| { - // native constructor var args_buf: [16]Value = undefined; for (0..ac) |i| args_buf[i] = self.stack[self.sp - ac + i]; self.dropN(ac); @@ -6933,7 +6940,6 @@ pub const VM = struct { } self.pending_exception = exc; } else { - // throwBuiltinException already dispatched to handler continue; } return error.RuntimeError; @@ -7460,7 +7466,6 @@ pub const VM = struct { // fall through to the regular slot/property write } - // IC: slot-indexed fast path if (self.ic) |ic| { const sp_idx = InlineCache.propIndex(@intFromPtr(self.currentChunk()), sp_ip); const sp_entry = &ic.prop[sp_idx]; @@ -7584,7 +7589,6 @@ pub const VM = struct { try self.triggerLazyInit(obj_val.object); } - // generator method dispatch if (obj_val == .generator) { const gen = obj_val.generator; self.dropN(ac); @@ -7954,7 +7958,6 @@ pub const VM = struct { } } - // check visibility const mvr = self.findMethodVisibility(obj.class_name, method_name); if (!self.checkVisibility(mvr.defining_class, mvr.visibility)) { const suffix = self.visScopeSuffix(); @@ -8007,7 +8010,6 @@ pub const VM = struct { return error.RuntimeError; }; if (self.native_fns.get(full_name)) |native| { - // populate IC if (self.ic) |ic| { const mc_ip2 = self.currentFrame().ip - 4; const mc_chunk_key2 = @intFromPtr(self.currentChunk()); @@ -8083,7 +8085,6 @@ pub const VM = struct { self.setErrorMsg("Fatal error: Uncaught ArgumentCountError: {s}\n", .{msg}); return error.RuntimeError; } - // populate IC if (self.ic) |ic| { const mc_ip2 = self.currentFrame().ip - 4; const mc_chunk_key2 = @intFromPtr(self.currentChunk()); @@ -8168,7 +8169,6 @@ pub const VM = struct { } const arr = args_val.array; - // check for named args var has_named = false; for (arr.entries.items) |entry| { if (entry.key == .string) { @@ -8201,7 +8201,6 @@ pub const VM = struct { pos += 1; } } - // fill defaults ac = @max(pos, func.required_params); for (0..ac) |i| { if (resolved_buf[i] == .null and i < func.defaults.len) { @@ -8487,7 +8486,6 @@ pub const VM = struct { }, .method_call_dynamic_spread => { - // stack: [object, method_name, args_array] const args_val = self.pop(); const method_name_val = self.pop(); const obj_val = self.pop(); @@ -8512,7 +8510,6 @@ pub const VM = struct { // push object and args back in method_call layout self.push(obj_val); for (arr.entries.items) |entry| self.push(entry.value); - // reuse method_call logic const full_name = self.resolveMethod(obj.class_name, method_name) catch { if (self.hasMethod(obj.class_name, "__call")) { const obj_id = @intFromPtr(obj); @@ -9028,7 +9025,6 @@ pub const VM = struct { if (try self.throwBuiltinException("Error", msg)) continue; return error.RuntimeError; }; - // accept FQN with leading backslash const class_name = if (raw_class_name.len > 0 and raw_class_name[0] == '\\') raw_class_name[1..] else raw_class_name; if (!self.classes.contains(class_name)) { try self.tryAutoload(class_name); @@ -9190,7 +9186,6 @@ pub const VM = struct { }, .static_call_dyn_both_spread => { - // stack: [class_name, method_name, args_array] const args_val = self.pop(); const method_val = self.pop(); const class_val = self.pop(); @@ -9424,7 +9419,6 @@ pub const VM = struct { return error.RuntimeError; }; - // unwrap IteratorAggregate by calling getIterator if (iterable == .object and self.hasMethod(iterable.object.class_name, "getIterator")) { iterable = self.callMethod(iterable.object, "getIterator", &.{}) catch { if (self.dispatchPendingException(base_frame)) continue; @@ -9503,7 +9497,6 @@ pub const VM = struct { self.genRelease(outer_gen); return; } - // inner already completed self.push(inner.return_value); } else if (iterable == .array) { const arr = iterable.array; @@ -9689,7 +9682,6 @@ pub const VM = struct { } return name; } - // direct FQN with leading backslash if (name.len > 0 and name[0] == '\\') return name[1..]; if (std.mem.eql(u8, name, "static")) { const f = self.currentFrame(); @@ -10313,7 +10305,6 @@ pub const VM = struct { gen.current_value = entry.value; return; } - // array exhausted const arr = arr_state.arr; gen.delegate = null; self.arrayRelease(arr); @@ -10402,7 +10393,6 @@ pub const VM = struct { } gen.state = .completed; self.handler_count = saved_handler_count; - // unwind any leftover generator frames while (self.frame_count > return_frame) { self.frame_count -= 1; self.deinitFrameSlot(self.frame_count); @@ -10640,9 +10630,7 @@ pub const VM = struct { return "Object"; } - // ================================================================== // opcode handlers (extracted from runLoop for readability) - // ================================================================== fn handleClassDecl(self: *VM) RuntimeError!void { const class_modifiers = self.readByte(); @@ -10693,32 +10681,56 @@ pub const VM = struct { const prop_has_default = try self.allocator.alloc(u8, prop_count); const prop_vis = try self.allocator.alloc(ClassDef.Visibility, prop_count); const prop_set_vis = try self.allocator.alloc(ClassDef.Visibility, prop_count); + const prop_has_set_vis = try self.allocator.alloc(bool, prop_count); const prop_readonly = try self.allocator.alloc(bool, prop_count); + const prop_final = try self.allocator.alloc(bool, prop_count); const prop_promoted = try self.allocator.alloc(bool, prop_count); const prop_type = try self.allocator.alloc([]const u8, prop_count); const prop_doc = try self.allocator.alloc([]const u8, prop_count); + defer self.allocator.free(prop_names); defer self.allocator.free(prop_has_default); defer self.allocator.free(prop_vis); defer self.allocator.free(prop_set_vis); + defer self.allocator.free(prop_has_set_vis); defer self.allocator.free(prop_readonly); + defer self.allocator.free(prop_final); defer self.allocator.free(prop_promoted); defer self.allocator.free(prop_type); defer self.allocator.free(prop_doc); + for (0..prop_count) |pi| { const pname_idx = self.readU16(); prop_names[pi] = self.currentChunk().constants.items[pname_idx].string; prop_has_default[pi] = self.readByte(); + const vis_byte = self.readByte(); + prop_vis[pi] = @enumFromInt(vis_byte & 0x03); prop_readonly[pi] = (vis_byte & 0x04) != 0; + const has_asymm = (vis_byte & 0x20) != 0; - prop_set_vis[pi] = if (has_asymm) @enumFromInt((vis_byte >> 3) & 0x03) else prop_vis[pi]; + prop_has_set_vis[pi] = has_asymm; + + prop_set_vis[pi] = if (has_asymm) + @enumFromInt((vis_byte >> 3) & 0x03) + else + prop_vis[pi]; + prop_promoted[pi] = (vis_byte & 0x40) != 0; + prop_final[pi] = (vis_byte & 0x80) != 0; + const type_idx = self.readU16(); - prop_type[pi] = if (type_idx == 0xffff) "" else self.currentChunk().constants.items[type_idx].string; + prop_type[pi] = if (type_idx == 0xffff) + "" + else + self.currentChunk().constants.items[type_idx].string; + const doc_idx_p = self.readU16(); - prop_doc[pi] = if (doc_idx_p == 0xffff) "" else self.currentChunk().constants.items[doc_idx_p].string; + prop_doc[pi] = if (doc_idx_p == 0xffff) + "" + else + self.currentChunk().constants.items[doc_idx_p].string; } const static_prop_count = self.readU16(); @@ -10728,26 +10740,37 @@ pub const VM = struct { const sprop_visibility = try self.allocator.alloc(u8, static_prop_count); const sprop_type = try self.allocator.alloc([]const u8, static_prop_count); const sprop_doc = try self.allocator.alloc([]const u8, static_prop_count); + defer self.allocator.free(sprop_names); defer self.allocator.free(sprop_has_default); defer self.allocator.free(sprop_is_const); defer self.allocator.free(sprop_visibility); defer self.allocator.free(sprop_type); defer self.allocator.free(sprop_doc); + for (0..static_prop_count) |pi| { const pname_idx = self.readU16(); sprop_names[pi] = self.currentChunk().constants.items[pname_idx].string; sprop_has_default[pi] = self.readByte(); sprop_visibility[pi] = self.readByte(); sprop_is_const[pi] = self.readByte(); + const t_idx = self.readU16(); - sprop_type[pi] = if (t_idx == 0xffff) "" else self.currentChunk().constants.items[t_idx].string; + sprop_type[pi] = if (t_idx == 0xffff) + "" + else + self.currentChunk().constants.items[t_idx].string; + const sd_idx = self.readU16(); - sprop_doc[pi] = if (sd_idx == 0xffff) "" else self.currentChunk().constants.items[sd_idx].string; + sprop_doc[pi] = if (sd_idx == 0xffff) + "" + else + self.currentChunk().constants.items[sd_idx].string; } const sdefaults = try self.popDefaultsAlloc(sprop_has_default); defer self.allocator.free(sdefaults); + const defaults = try self.popDefaultsAlloc(prop_has_default); defer self.allocator.free(defaults); @@ -10758,17 +10781,28 @@ pub const VM = struct { dj += 1; break :blk v; } else Value{ .null = {} }; - // the class def is a durable holder of this default - retain so - // arrays / objects in defaults are properly refcounted (copyValue - // in initObjectProperties relies on refcount > 0 to clone) + + // The ClassDef owns this default, so retain arrays/objects here. retainValue(default_val); + + const effective_readonly = prop_readonly[pi] or def.is_readonly; + // PHP 8.4 implicitly finalizes only asymmetric private(set). + // Symmetric private private(set) and private readonly are not final. + const reflection_final = + prop_final[pi] or + (prop_has_set_vis[pi] and + prop_set_vis[pi] == .private and + prop_vis[pi] != .private); + try def.properties.append(self.allocator, .{ .name = prop_names[pi], .default = default_val, .has_default = prop_has_default[pi] == 1, .visibility = prop_vis[pi], .set_visibility = prop_set_vis[pi], - .is_readonly = prop_readonly[pi] or def.is_readonly, + .has_set_visibility = prop_has_set_vis[pi], + .is_readonly = effective_readonly, + .is_final = reflection_final, .is_promoted = prop_promoted[pi], .type_str = prop_type[pi], .doc_comment = prop_doc[pi], @@ -10825,7 +10859,6 @@ pub const VM = struct { self.error_msg = msg; return error.RuntimeError; } - // reject overrides of final methods var pcls_iter = parent_cls.methods.iterator(); while (pcls_iter.next()) |pe| { if (!pe.value_ptr.is_final) continue; @@ -10880,12 +10913,10 @@ pub const VM = struct { try def.used_traits.append(self.allocator, trait_name); } - // class-level attributes const class_attrs = try self.readAttributeDefs(); for (class_attrs) |a| try def.attributes.append(self.allocator, a); if (class_attrs.len > 0) self.allocator.free(class_attrs); - // method attributes const method_attr_count = self.readByte(); for (0..method_attr_count) |_| { const ma_name_idx = self.readU16(); @@ -10894,7 +10925,6 @@ pub const VM = struct { try def.method_attributes.put(self.allocator, ma_name, ma_attrs); } - // property attributes const prop_attr_count = self.readByte(); for (0..prop_attr_count) |_| { const pa_name_idx = self.readU16(); @@ -10903,7 +10933,6 @@ pub const VM = struct { try def.property_attributes.put(self.allocator, pa_name, pa_attrs); } - // constant attributes const const_attr_count = self.readByte(); for (0..const_attr_count) |_| { const ca_name_idx = self.readU16(); @@ -10912,7 +10941,6 @@ pub const VM = struct { try def.constant_attributes.put(self.allocator, ca_name, ca_attrs); } - // parameter attributes const param_attr_method_count = self.readByte(); for (0..param_attr_method_count) |_| { const pam_name_idx = self.readU16(); @@ -10931,6 +10959,30 @@ pub const VM = struct { if (def.parent) |parent_name| { if (!self.classes.contains(parent_name)) try self.tryAutoload(parent_name); + + var current_parent: ?[]const u8 = parent_name; + while (current_parent) |parent_class_name| { + const parent_cls = self.classes.get(parent_class_name) orelse break; + + for (parent_cls.properties.items) |parent_prop| { + if (!parent_prop.is_final) continue; + + for (def.properties.items) |child_prop| { + if (!std.mem.eql(u8, parent_prop.name, child_prop.name)) continue; + + const msg = try std.fmt.allocPrint( + self.allocator, + "Cannot override final property {s}::${s}", + .{ parent_class_name, parent_prop.name }, + ); + try self.strings.append(self.allocator, msg); + self.error_msg = msg; + return error.RuntimeError; + } + } + + current_parent = parent_cls.parent; + } } def.slot_layout = try self.buildSlotLayout(&def); @@ -11168,7 +11220,6 @@ pub const VM = struct { try def.interfaces.append(self.allocator, "UnitEnum"); if (backed_type_byte != 0) try def.interfaces.append(self.allocator, "BackedEnum"); - // traits const enum_trait_count = self.readByte(); var enum_trait_names: [16][]const u8 = undefined; for (0..enum_trait_count) |ti| { @@ -11178,12 +11229,10 @@ pub const VM = struct { try self.applyTrait(&def, enum_name, trait_name, &.{}, &.{}); } - // enum-level attributes const enum_attrs = try self.readAttributeDefs(); for (enum_attrs) |a| try def.attributes.append(self.allocator, a); if (enum_attrs.len > 0) self.allocator.free(enum_attrs); - // method attributes const enum_method_attr_count = self.readByte(); for (0..enum_method_attr_count) |_| { const ma_name_idx = self.readU16(); @@ -11305,7 +11354,6 @@ pub const VM = struct { if (val != .string) return val; const s = val.string; if (s.len == 0) return val; - // check for Class::CONST pattern if (std.mem.indexOf(u8, s, "::")) |sep| { const class_name = s[0..sep]; const const_name = s[sep + 2 ..]; @@ -11391,7 +11439,6 @@ pub const VM = struct { if (!self.traits.contains(trait_name)) { try self.tryAutoload(trait_name); } - // recursively apply sub-traits first if (self.trait_uses.get(trait_name)) |subs| { for (subs) |sub| { try self.applyTrait(def, class_name, sub, &.{}, &.{}); @@ -11440,7 +11487,16 @@ pub const VM = struct { if (!self.functions.contains(alias_method)) { try self.functions.put(self.allocator, alias_method, tm.func); try self.indexFunctionByChunk(alias_method, &tm.func.chunk); - try def.addMethod(self.allocator, .{ .name = rule.alias, .arity = tm.func.arity, .visibility = if (rule.visibility != 0) rule_vis else .public }); + try def.addMethod(self.allocator, .{ + .name = rule.alias, + .arity = tm.func.arity, + .is_static = tm.func.is_static, + .is_final = tm.func.is_final, + .visibility = if (rule.visibility != 0) + rule_vis + else + @enumFromInt(tm.func.method_visibility), + }); } } } @@ -11471,7 +11527,9 @@ pub const VM = struct { try def.addMethod(self.allocator, .{ .name = tm.name, .arity = tm.func.arity, - .visibility = vis_override orelse .public, + .is_static = tm.func.is_static, + .is_final = tm.func.is_final, + .visibility = vis_override orelse @enumFromInt(tm.func.method_visibility), }); } } @@ -11512,9 +11570,7 @@ pub const VM = struct { } } - // ================================================================== // closure and frame helpers - // ================================================================== fn ensureClosureInstance(self: *VM) !void { const compile_name = self.peek().string; @@ -11606,6 +11662,7 @@ pub const VM = struct { const new_start: u32 = @intCast(self.captures.items.len); var has_this = false; var has_scope = false; + var has_defclass = false; for (src) |cap| { if (std.mem.eql(u8, cap.var_name, "$__closure_scope")) { has_scope = true; @@ -11624,6 +11681,23 @@ pub const VM = struct { } continue; } + if (std.mem.eql(u8, cap.var_name, "$__closure_defclass")) { + has_defclass = true; + switch (scope_action) { + .preserve => try self.captures.append(self.allocator, .{ + .closure_name = new_name, + .var_name = "$__closure_defclass", + .value = cap.value, + }), + .clear => {}, + .set => |s| try self.captures.append(self.allocator, .{ + .closure_name = new_name, + .var_name = "$__closure_defclass", + .value = .{ .string = s }, + }), + } + continue; + } var new_cap = CaptureEntry{ .closure_name = new_name, .var_name = cap.var_name, @@ -11657,6 +11731,16 @@ pub const VM = struct { .preserve, .clear => {}, } } + if (!has_defclass) { + switch (scope_action) { + .set => |s| try self.captures.append(self.allocator, .{ + .closure_name = new_name, + .var_name = "$__closure_defclass", + .value = .{ .string = s }, + }), + .preserve, .clear => {}, + } + } const new_len: u16 = @intCast(self.captures.items.len - new_start); try self.capture_index.put(self.allocator, new_name, .{ .start = new_start, .len = new_len, .has_refs = cr.has_refs }); } else { @@ -11670,11 +11754,18 @@ pub const VM = struct { }); } switch (scope_action) { - .set => |s| try self.captures.append(self.allocator, .{ - .closure_name = new_name, - .var_name = "$__closure_scope", - .value = .{ .string = s }, - }), + .set => |s| { + try self.captures.append(self.allocator, .{ + .closure_name = new_name, + .var_name = "$__closure_scope", + .value = .{ .string = s }, + }); + try self.captures.append(self.allocator, .{ + .closure_name = new_name, + .var_name = "$__closure_defclass", + .value = .{ .string = s }, + }); + }, .preserve, .clear => {}, } const cap_len = self.captures.items.len - new_start; @@ -11793,7 +11884,6 @@ pub const VM = struct { break :blk s; }; - // bind args to param slots const bind_count = @min(ac, func.arity); for (0..bind_count) |i| { locals[i] = try self.bindFrameArg(self.stack[self.sp - ac + i]); @@ -12653,7 +12743,6 @@ pub const VM = struct { } } } else if (arg_instr_count == 2) { - // get_var/get_local + get_prop const first_ip = instrs[i]; const second_ip = instrs[i + 1]; if (code[second_ip] == @intFromEnum(OpCode.get_prop)) { @@ -13123,7 +13212,6 @@ pub const VM = struct { std.mem.eql(u8, target_class, "Exception") or std.mem.eql(u8, target_class, "Error")) { - // walk the builtin hierarchy var bp = builtin_parent; while (true) { if (std.mem.eql(u8, bp, target_class)) return true; @@ -13378,6 +13466,44 @@ pub const VM = struct { return null; } + fn closureDefClassForFrame(self: *VM, frame: *const CallFrame) ?[]const u8 { + const compile_name = if (frame.func) |fn_| fn_.name else ""; + if (!std.mem.startsWith(u8, compile_name, "__closure_")) return null; + if (frame.call_name) |inst_name| { + if (self.capture_index.get(inst_name)) |cr| { + const caps = self.captures.items[cr.start .. cr.start + cr.len]; + for (caps) |cap| { + if (std.mem.eql(u8, cap.var_name, "$__closure_defclass") and cap.value == .string) + return cap.value.string; + } + } + } + if (self.chunk_to_func_names.get(@intFromPtr(frame.chunk))) |names| { + for (names.items) |name| { + if (!std.mem.startsWith(u8, name, "__closure_")) continue; + if (self.capture_index.get(name)) |cr| { + const caps = self.captures.items[cr.start .. cr.start + cr.len]; + for (caps) |cap| { + if (std.mem.eql(u8, cap.var_name, "$__closure_defclass") and cap.value == .string) + return cap.value.string; + } + } + } + } + return null; + } + + pub fn closureDefClassByName(self: *VM, name: []const u8) ?[]const u8 { + if (self.capture_index.get(name)) |cr| { + const caps = self.captures.items[cr.start .. cr.start + cr.len]; + for (caps) |cap| { + if (std.mem.eql(u8, cap.var_name, "$__closure_defclass") and cap.value == .string) + return cap.value.string; + } + } + return null; + } + pub fn closureThisByName(self: *VM, name: []const u8) Value { if (self.capture_index.get(name)) |cr| { const caps = self.captures.items[cr.start .. cr.start + cr.len]; @@ -13442,6 +13568,9 @@ pub const VM = struct { pub fn currentDefiningClass(self: *VM) ?[]const u8 { // bound closure scope takes priority over frame walk if (self.frame_count > 0) { + if (self.closureDefClassForFrame(&self.frames[self.frame_count - 1])) |scope| + return scope; + // fall back to $__closure_scope for closures created before the if (self.closureScopeForFrame(&self.frames[self.frame_count - 1])) |scope| return scope; } @@ -14087,6 +14216,7 @@ pub const VM = struct { const caps = self.captures.items[cr.start .. cr.start + cr.len]; for (caps) |cap| { if (std.mem.eql(u8, cap.var_name, "$__closure_scope")) continue; + if (std.mem.eql(u8, cap.var_name, "$__closure_defclass")) continue; if (cap.ref_cell) |cell| { if (ref_slots) |rs| try rs.put(self.allocator, cap.var_name, cell); } else { @@ -14095,7 +14225,6 @@ pub const VM = struct { } } - // arrow functions inherit parent scope if (self.frame_count > 0) { const orig_name = self.getOrigClosureName(name); if (self.functions.get(orig_name)) |func| { @@ -14172,7 +14301,6 @@ pub const VM = struct { } if (val == .string) { const s = val.string; - // deferred class constant: "\x00CC\x00ClassName\x00CONST_NAME" if (s.len > 4 and s[0] == 0 and s[1] == 'C' and s[2] == 'C' and s[3] == 0) { const rest = s[4..]; if (std.mem.indexOfScalar(u8, rest, 0)) |sep| { @@ -14382,7 +14510,6 @@ pub const VM = struct { fn tryWeakCoerce(self: *VM, val: Value, type_str: []const u8) RuntimeError!?Value { var t = type_str; if (t.len > 0 and t[0] == '?') t = t[1..]; - // bail on intersection types for (t) |c| if (c == '&' or c == '(' or c == ')') return null; // unions: try each member in PHP's priority order, returning the first // that coerces successfully without lossy conversion. PHP's rule is to @@ -15445,9 +15572,7 @@ pub const VM = struct { return self.callByName(name, args); } - // ================================================================== // fibers - // ================================================================== fn executeFiber(self: *VM, fiber: *Fiber, base_frame: usize, base_sp: usize, base_handler: usize) RuntimeError!Value { const prev_fiber = self.current_fiber; @@ -15494,7 +15619,6 @@ pub const VM = struct { } fn saveFiberState(self: *VM, fiber: *Fiber, base_frame: usize, base_sp: usize, base_handler: usize) !void { - // clear previously saved state self.cleanupFiberFrames(fiber); fiber.saved_frames.clearRetainingCapacity(); fiber.saved_stack.clearRetainingCapacity(); @@ -15521,7 +15645,6 @@ pub const VM = struct { } self.frame_count = base_frame; - // save stack values for (self.stack[base_sp..self.sp]) |val| { try fiber.saved_stack.append(self.allocator, val); } @@ -15562,7 +15685,6 @@ pub const VM = struct { self.frame_count = base_frame + fiber.saved_frames.items.len; fiber.saved_frames.clearRetainingCapacity(); - // restore stack for (fiber.saved_stack.items) |val| { self.stack[self.sp] = val; self.sp += 1; @@ -15582,10 +15704,6 @@ pub const VM = struct { fiber.saved_handlers.clearRetainingCapacity(); } - // ================================================================== - // helpers - // ================================================================== - fn readByte(self: *VM) u8 { const frame = &self.frames[self.frame_count - 1]; const byte = frame.chunk.code.items[frame.ip]; @@ -15731,9 +15849,7 @@ pub const VM = struct { return self.stack[self.sp - 1]; } - // =================================================================== // object refcounting (Stage 1) - see .handoff/refcounting.md - // =================================================================== // an object handle was copied into a new reference pub fn objRetain(obj: *PhpObject) void { diff --git a/src/serve.zig b/src/serve.zig index d0fb9b67..a8e62ce3 100644 --- a/src/serve.zig +++ b/src/serve.zig @@ -68,7 +68,6 @@ const Request = struct { } }; -// connection state machine const ConnState = enum { tls_handshaking, http_reading, h2_active, ws_idle, closing }; @@ -186,7 +185,6 @@ fn WorkQueue(comptime capacity: usize) type { var queue: WorkQueue(1024) = .{}; -// worker state const MAX_CONNS = 1024; @@ -377,7 +375,6 @@ fn certMtime(path: []const u8) i128 { return stat.mtime; } -// main serve entry point pub fn serve(allocator: Allocator, config: ServeConfig) !void { env.loadEnvFile(allocator); @@ -595,7 +592,6 @@ fn eventLoop(w: *Worker) void { while (!queue.shutdown) { _ = posix.poll(w.poll_fds[0..w.n_fds], 1000) catch continue; - // check wake pipe if (w.poll_fds[0].revents & posix.POLL.IN != 0) { var drain: [64]u8 = undefined; _ = posix.read(w.wake_pipe[0], &drain) catch {}; @@ -604,7 +600,6 @@ fn eventLoop(w: *Worker) void { } } - // process ready connections var i: usize = 1; while (i < w.n_fds) : (i += 1) { const revents = w.poll_fds[i].revents; @@ -824,7 +819,6 @@ fn processHttpRead(w: *Worker, c: *Connection) void { const conn_hdr = req.getHeader("Connection"); c.keep_alive = if (conn_hdr) |h| !std.ascii.eqlIgnoreCase(h, "close") else true; - // websocket upgrade if (w.ws_enabled) { const upgrade_hdr = req.getHeader("Upgrade"); if (upgrade_hdr != null and std.ascii.eqlIgnoreCase(upgrade_hdr.?, "websocket")) { @@ -838,7 +832,6 @@ fn processHttpRead(w: *Worker, c: *Connection) void { } } - // static file if (tryServeStatic(w.allocator, c, w.doc_root, &req, c.keep_alive)) { shiftBuffer(c, consumed); if (!c.keep_alive) c.state = .closing; @@ -944,7 +937,6 @@ fn processH2Read(w: *Worker, conn: *Connection) void { fn handleH2Request(w: *Worker, conn: *Connection, session: *h2.H2Session, stream: *h2.H2Stream) void { const stream_id = stream.stream_id; - // build Request from h2 stream var req = Request{ .method = stream.method, .uri = stream.path, @@ -963,7 +955,6 @@ fn handleH2Request(w: *Worker, conn: *Connection, session: *h2.H2Session, stream req.header_count = stream.header_count; req.body = stream.body.items; - // try static file if (w.doc_root.len > 0 and tryServeStaticH2(w.allocator, session, stream_id, w.doc_root, &req)) { stream.resetRequest(w.allocator); return; @@ -981,7 +972,6 @@ fn handleH2Request(w: *Worker, conn: *Connection, session: *h2.H2Session, stream stream.resetRequest(w.allocator); return; }; - // override SERVER_PROTOCOL for h2 if (w.vm.request_vars.get("$_SERVER")) |sv| { if (sv == .array) sv.array.set(w.allocator, .{ .string = "SERVER_PROTOCOL" }, .{ .string = "HTTP/2.0" }) catch {}; } @@ -1035,7 +1025,6 @@ fn tryServeStaticH2(allocator: Allocator, session: *h2.H2Session, stream_id: i32 return true; } -// WebSocket processing fn handleWsUpgrade(w: *Worker, c: *Connection, ws_key: []const u8) void { var accept_buf: [28]u8 = undefined; @@ -1141,7 +1130,6 @@ fn processWsRead(w: *Worker, c: *Connection) void { if (consumed_total > 0) shiftBuffer(c, consumed_total); } -// unchanged helper functions below fn parseRequest(raw: []const u8) Request { var req = Request{ .raw = raw }; @@ -1360,7 +1348,6 @@ fn parseMultipart(a: Allocator, vm: *VM, body: []const u8, boundary: []const u8, const part_end = if (next >= 2) next - 2 else next; const part = body[pos..part_end]; - // parse part headers const hdr_end_pos = std.mem.indexOf(u8, part, "\r\n\r\n") orelse { pos = next + delim.len + 2; continue; @@ -1399,7 +1386,6 @@ fn parseMultipart(a: Allocator, vm: *VM, body: []const u8, boundary: []const u8, try file_entry.set(a, .{ .string = "size" }, .{ .int = @intCast(part_body.len) }); try file_entry.set(a, .{ .string = "error" }, .{ .int = 0 }); - // write to temp file const tmp_path = writeTempFile(a, part_body) catch blk: { try file_entry.set(a, .{ .string = "error" }, .{ .int = 6 }); // UPLOAD_ERR_NO_TMP_DIR break :blk try a.dupe(u8, ""); @@ -1414,7 +1400,6 @@ fn parseMultipart(a: Allocator, vm: *VM, body: []const u8, boundary: []const u8, try post_arr.set(a, .{ .string = name_owned }, .{ .string = val }); } - // advance past delimiter pos = next + delim.len; if (pos + 2 <= body.len and body[pos] == '-' and body[pos + 1] == '-') break; if (pos + 2 <= body.len and body[pos] == '\r' and body[pos + 1] == '\n') pos += 2; @@ -1431,7 +1416,6 @@ fn findPartHeader(headers: []const u8, name: []const u8) ?[]const u8 { } fn extractParam(header: []const u8, name: []const u8) ?[]const u8 { - // look for name="value" pattern var search_buf: [64]u8 = undefined; const needle = std.fmt.bufPrint(&search_buf, "{s}=\"", .{name}) catch return null; const start = (std.mem.indexOf(u8, header, needle) orelse return null) + needle.len; diff --git a/src/stdlib/arrays.zig b/src/stdlib/arrays.zig index 29b68bf4..c4783261 100644 --- a/src/stdlib/arrays.zig +++ b/src/stdlib/arrays.zig @@ -584,7 +584,6 @@ pub const SortField = enum { value, key }; pub fn mergeSort(comptime T: type, items: []T, ctx: *NativeContext, callback: Value, comptime field: SortField) RuntimeError!void { if (items.len <= 1) return; if (items.len <= 16) { - // insertion sort for small slices for (1..items.len) |i| { const tmp = items[i]; var j: usize = i; @@ -746,10 +745,8 @@ fn array_filter(ctx: *NativeContext, args: []const Value) RuntimeError!Value { .string => |s| .{ .string = s }, }; const keep = if (flag == 1) - // ARRAY_FILTER_USE_BOTH try ctx.invokeCallable(args[1], &.{ entry.value, key_val }) else if (flag == 2) - // ARRAY_FILTER_USE_KEY try ctx.invokeCallable(args[1], &.{key_val}) else try ctx.invokeCallable(args[1], &.{entry.value}); @@ -772,7 +769,6 @@ fn native_usort(ctx: *NativeContext, args: []const Value) RuntimeError!Value { fn native_range(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 2) return .null; - // character range: single-char strings if (args[0] == .string and args[0].string.len == 1 and args[1] == .string and args[1].string.len == 1) { const lo = args[0].string[0]; const hi = args[1].string[0]; @@ -915,7 +911,6 @@ fn array_splice(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } } - // re-index numeric keys var next_int: i64 = 0; for (arr.entries.items) |*entry| { if (entry.key == .int) { @@ -1395,7 +1390,6 @@ fn array_rand(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return error.RuntimeError; } if (num == 1 and (args.len < 2 or Value.toInt(args[1]) == 1 and args.len == 1)) { - // single-arg form returns scalar const idx = std.crypto.random.intRangeAtMost(usize, 0, arr.entries.items.len - 1); return switch (arr.entries.items[idx].key) { .int => |i| .{ .int = i }, diff --git a/src/stdlib/bcmath.zig b/src/stdlib/bcmath.zig index a0e81cea..1f38a787 100644 --- a/src/stdlib/bcmath.zig +++ b/src/stdlib/bcmath.zig @@ -123,7 +123,6 @@ fn copyAndPadRight(allocator: Allocator, n: BcNum, new_scale: usize) !BcNum { var i: usize = n.scale; while (i < new_scale) : (i += 1) try out.digits.append(allocator, 0); } else { - // shrinking scale: truncate fractional tail const drop = n.scale - new_scale; try out.digits.appendSlice(allocator, n.digits.items[0 .. n.digits.items.len - drop]); } @@ -143,7 +142,6 @@ fn copyAndPadBoth(allocator: Allocator, n: BcNum, target_int_len: usize, target_ return out; } -// compare absolute values, returning -1/0/1 fn cmpAbs(a: BcNum, b: BcNum) i32 { const al = a.integerLen(); const bl = b.integerLen(); @@ -234,7 +232,6 @@ fn subAbs(allocator: Allocator, a: BcNum, b: BcNum) !BcNum { } out.digits.items[i] = @intCast(diff); } - // strip leading zeros var lead: usize = 0; const int_len = out.digits.items.len - out.scale; while (lead + 1 < int_len and out.digits.items[lead] == 0) lead += 1; @@ -338,10 +335,8 @@ fn bcDivInternal(allocator: Allocator, a: BcNum, b: BcNum, target_scale: usize) defer div_digits.deinit(allocator); try div_digits.appendSlice(allocator, b.digits.items); - // strip leading zeros from divisor while (div_digits.items.len > 1 and div_digits.items[0] == 0) _ = div_digits.orderedRemove(0); - // long division: produce quotient digit-by-digit var quot = std.ArrayListUnmanaged(u8){}; errdefer quot.deinit(allocator); var rem = std.ArrayListUnmanaged(u8){}; @@ -368,7 +363,6 @@ fn bcDivInternal(allocator: Allocator, a: BcNum, b: BcNum, target_scale: usize) carry = v / 10; } if (carry > 0) try prod.insert(allocator, 0, carry); - // compare prod to rem const cmp = cmpDigits(prod.items, rem.items); if (cmp > 0) break; q = test_q; @@ -376,7 +370,6 @@ fn bcDivInternal(allocator: Allocator, a: BcNum, b: BcNum, target_scale: usize) try quot.append(allocator, q); if (q > 0) { - // subtract q*div from rem var prod = std.ArrayListUnmanaged(u8){}; defer prod.deinit(allocator); var carry: u8 = 0; @@ -390,7 +383,6 @@ fn bcDivInternal(allocator: Allocator, a: BcNum, b: BcNum, target_scale: usize) } if (carry > 0) try prod.insert(allocator, 0, carry); - // subtract prod from rem const diff_len = rem.items.len; var pad: usize = 0; if (prod.items.len < diff_len) pad = diff_len - prod.items.len; @@ -424,7 +416,6 @@ fn bcDivInternal(allocator: Allocator, a: BcNum, b: BcNum, target_scale: usize) out.scale = wanted; quot.deinit(allocator); - // strip leading zeros var lead: usize = 0; const int_len_out = out.digits.items.len - out.scale; while (lead + 1 < int_len_out and out.digits.items[lead] == 0) lead += 1; @@ -442,7 +433,6 @@ fn cmpDigits(a: []const u8, b: []const u8) i32 { return 0; } -// ---------------- bcscale state ---------------- var global_scale_lock = std.Thread.Mutex{}; var global_scale: usize = 0; @@ -466,7 +456,6 @@ fn resolveScale(args: []const Value, scale_idx: usize) usize { return currentScale(); } -// ---------------- top-level functions ---------------- fn argToString(args: []const Value, idx: usize) ?[]const u8 { if (args.len <= idx) return null; @@ -629,7 +618,6 @@ fn bcPow(ctx: *NativeContext, args: []const Value) RuntimeError!Value { exp = -exp; } - // result = 1 var r = try parseBc(ctx.allocator, "1"); errdefer r.deinit(ctx.allocator); var base = try parseBc(ctx.allocator, "0"); @@ -704,7 +692,6 @@ fn bcPowmod(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } - // result = 1 var result = try parseBc(ctx.allocator, "1"); errdefer result.deinit(ctx.allocator); @@ -719,7 +706,6 @@ fn bcPowmod(ctx: *NativeContext, args: []const Value) RuntimeError!Value { base = new_base; } - // square-and-multiply with bcnum-sized exponent var two = try parseBc(ctx.allocator, "2"); defer two.deinit(ctx.allocator); @@ -751,7 +737,6 @@ fn bcPowmod(ctx: *NativeContext, args: []const Value) RuntimeError!Value { result = new_r; } - // exp = exp / 2 { const new_exp = (try bcDivInternal(ctx.allocator, exp, two, 0)) orelse return .null; exp.deinit(ctx.allocator); @@ -1051,7 +1036,6 @@ fn bcRound(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return try returnStr(ctx, out); } -// ---------------- registration ---------------- pub const entries = .{ .{ "bcadd", bcAdd }, diff --git a/src/stdlib/crypto.zig b/src/stdlib/crypto.zig index 05e79c89..d00b10cf 100644 --- a/src/stdlib/crypto.zig +++ b/src/stdlib/crypto.zig @@ -194,7 +194,6 @@ fn native_password_verify(ctx: *NativeContext, args: []const Value) RuntimeError const password = args[0].string; const hash = args[1].string; - // argon2 family: delegate to libsodium if (std.mem.startsWith(u8, hash, "$argon2")) { const r = ctx.vm.callByName("sodium_crypto_pwhash_str_verify", &.{ .{ .string = hash }, @@ -250,7 +249,6 @@ fn native_password_get_info(ctx: *NativeContext, args: []const Value) RuntimeErr if (hash.len >= 7 and hash[0] == '$' and hash[1] == '2' and (hash[2] == 'y' or hash[2] == 'b' or hash[2] == 'a')) { algo = .{ .string = try ctx.createString("2y") }; algo_name = "bcrypt"; - // parse cost: $2y$XX$ const cost_start: usize = 4; if (hash.len >= 6 and hash[cost_start - 1] == '$') { const dollar_after = std.mem.indexOfScalarPos(u8, hash, cost_start, '$') orelse hash.len; @@ -619,13 +617,11 @@ fn native_hash_hkdf(ctx: *NativeContext, args: []const Value) RuntimeError!Value const out_len: usize = if (length > 0) @intCast(length) else hlen; if (out_len > 255 * hlen) return .{ .bool = false }; - // Extract: PRK = HMAC(salt, IKM) const zero_salt = [_]u8{0} ** 64; const effective_salt: []const u8 = if (salt.len > 0) salt else zero_salt[0..hlen]; var prk: [64]u8 = undefined; computeHmac(algo, ikm, effective_salt, prk[0..hlen]); - // Expand const out = try ctx.allocator.alloc(u8, out_len); var t_prev: [64]u8 = undefined; var t_prev_len: usize = 0; @@ -815,7 +811,6 @@ fn native_hash_pbkdf2(ctx: *NativeContext, args: []const Value) RuntimeError!Val }; const out = try ctx.allocator.alloc(u8, out_bytes); - // basic PBKDF2-HMAC implementation var block_index: u32 = 1; var written: usize = 0; while (written < out_bytes) : (block_index += 1) { diff --git a/src/stdlib/curl.zig b/src/stdlib/curl.zig index d3429ed2..5de103fc 100644 --- a/src/stdlib/curl.zig +++ b/src/stdlib/curl.zig @@ -98,7 +98,6 @@ fn curlInit(ctx: *NativeContext, args: []const Value) RuntimeError!Value { try obj.set(ctx.allocator, "__header_out", .{ .bool = false }); try obj.set(ctx.allocator, "__http_code", .{ .int = 0 }); - // store slist pointers for cleanup try obj.set(ctx.allocator, "__slist_ptr", .{ .int = 0 }); if (args.len > 0 and args[0] == .string) { @@ -119,7 +118,6 @@ fn curlSetopt(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i64, value: Value) RuntimeError!Value { - // string options if (option == c.CURLOPT_URL or option == c.CURLOPT_USERAGENT or option == c.CURLOPT_REFERER or @@ -150,7 +148,6 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = code == c.CURLE_OK }; } - // long options if (option == c.CURLOPT_PORT or option == c.CURLOPT_TIMEOUT or option == c.CURLOPT_TIMEOUT_MS or @@ -190,7 +187,6 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = true }; } - // CURLOPT_FOLLOWLOCATION if (option == c.CURLOPT_FOLLOWLOCATION) { const v: c_long = switch (value) { .int => @intCast(value.int), @@ -201,7 +197,6 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = code == c.CURLE_OK }; } - // CURLOPT_POST if (option == c.CURLOPT_POST) { const v: c_long = switch (value) { .int => @intCast(value.int), @@ -212,7 +207,6 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = code == c.CURLE_OK }; } - // CURLOPT_POSTFIELDS if (option == c.CURLOPT_POSTFIELDS) { const s = switch (value) { .string => value.string, @@ -225,11 +219,9 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = len_code == c.CURLE_OK }; } - // CURLOPT_HTTPHEADER if (option == c.CURLOPT_HTTPHEADER) { if (value != .array) return .{ .bool = false }; - // free previous slist if any const prev_v = obj.get("__slist_ptr"); if (prev_v == .int and prev_v.int != 0) { const prev: *c.struct_curl_slist = @ptrFromInt(@as(usize, @intCast(prev_v.int))); @@ -251,7 +243,6 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = true }; } - // CURLOPT_PUT if (option == c.CURLOPT_PUT) { const v: c_long = switch (value) { .int => @intCast(value.int), @@ -273,7 +264,6 @@ fn applySetopt(ctx: *NativeContext, handle: *c.CURL, obj: *PhpObject, option: i6 return .{ .bool = code == c.CURLE_OK }; } - // CURLINFO_HEADER_OUT (debug) if (option == c.CURLINFO_HEADER_OUT) { const v = switch (value) { .bool => value.bool, @@ -352,7 +342,6 @@ fn curlExec(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const result = c.curl_easy_perform(handle); - // store error info if (result != c.CURLE_OK) { const err_msg = c.curl_easy_strerror(result); const msg = std.mem.span(err_msg); @@ -388,7 +377,6 @@ fn curlClose(ctx: *NativeContext, args: []const Value) RuntimeError!Value { pub fn cleanupHandle(obj: *PhpObject) void { if (obj.pooled) return; - // free slist const slist_v = obj.get("__slist_ptr"); if (slist_v == .int and slist_v.int != 0) { const sl: *c.struct_curl_slist = @ptrFromInt(@as(usize, @intCast(slist_v.int))); @@ -396,7 +384,6 @@ pub fn cleanupHandle(obj: *PhpObject) void { obj.properties.put(std.heap.page_allocator, "__slist_ptr", .{ .int = 0 }) catch {}; } - // free curl handle if (getHandle(obj)) |handle| { c.curl_easy_cleanup(handle); obj.properties.put(std.heap.page_allocator, "__curl_ptr", .{ .int = 0 }) catch {}; @@ -431,7 +418,6 @@ fn curlGetinfo(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } fn getInfoOption(ctx: *NativeContext, handle: *c.CURL, option: i64) RuntimeError!Value { - // string info types if (option == c.CURLINFO_EFFECTIVE_URL or option == c.CURLINFO_CONTENT_TYPE or option == c.CURLINFO_REDIRECT_URL or @@ -447,7 +433,6 @@ fn getInfoOption(ctx: *NativeContext, handle: *c.CURL, option: i64) RuntimeError return .{ .string = owned }; } - // long info types if (option == c.CURLINFO_RESPONSE_CODE or option == c.CURLINFO_HTTP_CONNECTCODE or option == c.CURLINFO_FILETIME or @@ -464,7 +449,6 @@ fn getInfoOption(ctx: *NativeContext, handle: *c.CURL, option: i64) RuntimeError return .{ .int = @intCast(val) }; } - // double info types if (option == c.CURLINFO_TOTAL_TIME or option == c.CURLINFO_NAMELOOKUP_TIME or option == c.CURLINFO_CONNECT_TIME or @@ -679,7 +663,6 @@ fn curlReset(ctx: *NativeContext, args: []const Value) RuntimeError!Value { try obj.set(ctx.allocator, "__errno", .{ .int = 0 }); try obj.set(ctx.allocator, "__http_code", .{ .int = 0 }); - // free slist const slist_v = obj.get("__slist_ptr"); if (slist_v == .int and slist_v.int != 0) { const sl: *c.struct_curl_slist = @ptrFromInt(@as(usize, @intCast(slist_v.int))); @@ -1050,7 +1033,6 @@ pub fn register(vm: *VM, a: std.mem.Allocator) !void { try vm.php_constants.put(a, "CURL_LOCK_DATA_DNS", .{ .int = 3 }); try vm.php_constants.put(a, "CURL_LOCK_DATA_SSL_SESSION", .{ .int = 4 }); - // CURLOPT constants try vm.php_constants.put(a, "CURLOPT_URL", .{ .int = c.CURLOPT_URL }); try vm.php_constants.put(a, "CURLOPT_PORT", .{ .int = c.CURLOPT_PORT }); try vm.php_constants.put(a, "CURLOPT_RETURNTRANSFER", .{ .int = 19913 }); @@ -1098,7 +1080,6 @@ pub fn register(vm: *VM, a: std.mem.Allocator) !void { try vm.php_constants.put(a, "CURLOPT_INTERFACE", .{ .int = c.CURLOPT_INTERFACE }); try vm.php_constants.put(a, "CURLOPT_UNIX_SOCKET_PATH", .{ .int = c.CURLOPT_UNIX_SOCKET_PATH }); - // CURLINFO constants try vm.php_constants.put(a, "CURLINFO_EFFECTIVE_URL", .{ .int = c.CURLINFO_EFFECTIVE_URL }); try vm.php_constants.put(a, "CURLINFO_HTTP_CODE", .{ .int = c.CURLINFO_RESPONSE_CODE }); try vm.php_constants.put(a, "CURLINFO_RESPONSE_CODE", .{ .int = c.CURLINFO_RESPONSE_CODE }); @@ -1121,18 +1102,15 @@ pub fn register(vm: *VM, a: std.mem.Allocator) !void { try vm.php_constants.put(a, "CURLINFO_SCHEME", .{ .int = c.CURLINFO_SCHEME }); try vm.php_constants.put(a, "CURLINFO_HEADER_OUT", .{ .int = c.CURLINFO_HEADER_OUT }); - // CURLAUTH constants try vm.php_constants.put(a, "CURLAUTH_BASIC", .{ .int = c.CURLAUTH_BASIC }); try vm.php_constants.put(a, "CURLAUTH_DIGEST", .{ .int = c.CURLAUTH_DIGEST }); try vm.php_constants.put(a, "CURLAUTH_BEARER", .{ .int = c.CURLAUTH_BEARER }); - // CURL_HTTP_VERSION constants try vm.php_constants.put(a, "CURL_HTTP_VERSION_NONE", .{ .int = 0 }); try vm.php_constants.put(a, "CURL_HTTP_VERSION_1_0", .{ .int = 1 }); try vm.php_constants.put(a, "CURL_HTTP_VERSION_1_1", .{ .int = 2 }); try vm.php_constants.put(a, "CURL_HTTP_VERSION_2_0", .{ .int = 3 }); - // error code constants try vm.php_constants.put(a, "CURLE_OK", .{ .int = c.CURLE_OK }); try vm.php_constants.put(a, "CURLE_UNSUPPORTED_PROTOCOL", .{ .int = c.CURLE_UNSUPPORTED_PROTOCOL }); try vm.php_constants.put(a, "CURLE_URL_MALFORMAT", .{ .int = c.CURLE_URL_MALFORMAT }); diff --git a/src/stdlib/datetime.zig b/src/stdlib/datetime.zig index 08e2ca71..3afc155f 100644 --- a/src/stdlib/datetime.zig +++ b/src/stdlib/datetime.zig @@ -69,7 +69,6 @@ pub const entries = .{ }; pub fn register(vm: *VM, a: Allocator) !void { - // DateTimeInterface var iface = vm_mod.InterfaceDef{ .name = "DateTimeInterface" }; try iface.methods.append(a, "format"); try iface.methods.append(a, "getTimestamp"); @@ -82,7 +81,6 @@ pub fn register(vm: *VM, a: Allocator) !void { } try vm.classes.put(a, "DateTimeInterface", dti_const); - // DateTime class var dt_def = ClassDef{ .name = "DateTime" }; try dt_def.properties.append(a, .{ .name = "timestamp", .default = .{ .int = 0 }, .visibility = .private }); try dt_def.interfaces.append(a, "DateTimeInterface"); @@ -133,7 +131,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "DateTime::getOffset", dtGetOffset); try vm.native_fns.put(a, "DateTimeImmutable::getOffset", dtGetOffset); - // DateTimeImmutable var dti_def = ClassDef{ .name = "DateTimeImmutable" }; try dti_def.properties.append(a, .{ .name = "timestamp", .default = .{ .int = 0 }, .visibility = .private }); try dti_def.interfaces.append(a, "DateTimeInterface"); @@ -181,7 +178,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "DateTime::createFromInterface", dtCreateFromInterface); try vm.native_fns.put(a, "DateTimeImmutable::createFromInterface", dtiCreateFromInterface); - // DateTimeZone class var dtz_def = ClassDef{ .name = "DateTimeZone" }; try dtz_def.properties.append(a, .{ .name = "timezone", .default = .{ .string = "UTC" }, .visibility = .private }); try dtz_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 1 }); @@ -203,7 +199,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "DateTimeZone::getLocation", dtzGetLocation); try vm.native_fns.put(a, "DateTimeZone::getTransitions", dtzGetTransitions); - // DateInterval var di_def = ClassDef{ .name = "DateInterval" }; try di_def.properties.append(a, .{ .name = "y", .default = .{ .int = 0 } }); try di_def.properties.append(a, .{ .name = "m", .default = .{ .int = 0 } }); @@ -223,7 +218,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "DateInterval::createFromDateString", diCreateFromDateString); try vm.native_fns.put(a, "DateInterval::format", diFormat); - // DatePeriod var dp_def = ClassDef{ .name = "DatePeriod" }; try dp_def.static_props.put(a, "EXCLUDE_START_DATE", .{ .int = 1 }); try dp_def.static_props.put(a, "INCLUDE_END_DATE", .{ .int = 2 }); @@ -473,7 +467,6 @@ fn dtConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; var ts: i64 = std.time.timestamp(); - // extract timezone from second arg var tz_name = ctx.vm.default_tz_name; if (args.len >= 2) { tz_name = extractTimezoneName(args[1..]); @@ -504,13 +497,11 @@ fn dtConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { min = std.fmt.parseInt(i64, s[14..16], 10) catch 0; pos = 16; } - // skip fractional seconds if (pos < s.len and s[pos] == '.') { pos += 1; while (pos < s.len and s[pos] >= '0' and s[pos] <= '9') pos += 1; } while (pos < s.len and s[pos] == ' ') pos += 1; - // try trailing timezone var explicit_offset: ?i64 = null; var explicit_name: ?[]const u8 = null; if (pos < s.len) { @@ -738,7 +729,6 @@ pub fn formatTimestampTzMicros(ctx: *NativeContext, timestamp: i64, format: []co try buf.appendSlice(a, s); }, 'c' => { - // ISO 8601: YYYY-MM-DDTHH:MM:SS+00:00 var tmp: [32]u8 = undefined; const s = std.fmt.bufPrint(&tmp, "{d}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}", .{ year_day.year, @@ -972,7 +962,6 @@ fn applyIntervalTz(ts: i64, interval: *PhpObject, sign: i64, tz_name: []const u8 const day: i64 = c.day + direction * d; var local_ts = dateToTimestamp(year, month, day, c.hour, c.min, c.sec); - // convert local back to UTC if (tz) |t| { const off_out: i64 = @as(i64, tzOffsetAt(t, local_ts)); local_ts -= off_out; @@ -1287,7 +1276,6 @@ fn createBareDt(ctx: *NativeContext, class_name: []const u8, args: []const Value if (args.len >= 1 and args[0] == .string) { const s = args[0].string; if (s.len == 0 or std.mem.eql(u8, s, "now")) { - // current } else if (s.len >= 2 and s[0] == '@') { ts = std.fmt.parseInt(i64, s[1..], 10) catch ts; } else if (s.len >= 10 and s[4] == '-' and s[7] == '-') { @@ -1342,12 +1330,10 @@ fn native_date_format(ctx: *NativeContext, args: []const Value) RuntimeError!Val return formatTimestampTz(ctx, ts, args[1].string, offset, tz_name); } -// procedural alias for DateInterval::createFromDateString fn native_date_interval_create_from_date_string(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return diCreateFromDateString(ctx, args); } -// procedural alias for DateInterval::format($interval, $format) fn native_date_interval_format(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 2 or args[0] != .object or args[1] != .string) return .{ .bool = false }; const saved = ctx.vm.currentFrame().vars.get("$this"); @@ -1699,13 +1685,11 @@ fn parseDateTimeFormat(format: []const u8, datetime: []const u8, now: i64) ?Pars di += len; }, 'e', 'T' => { - // timezone name: consume identifier-ish chars const start = di; while (di < datetime.len and (isAlpha(datetime[di]) or datetime[di] == '/' or datetime[di] == '_' or datetime[di] == '+' or datetime[di] == '-' or (datetime[di] >= '0' and datetime[di] <= '9'))) : (di += 1) {} if (di == start) return null; }, 'O', 'P' => { - // +0200 or +02:00 if (di >= datetime.len) return null; const sign: i64 = if (datetime[di] == '+') 1 else if (datetime[di] == '-') -1 else return null; di += 1; @@ -2057,7 +2041,6 @@ fn dtzListAbbreviations(ctx: *NativeContext, _: []const Value) RuntimeError!Valu // the lowercased table name). reusing the tz_table entry's name as // lowercase is fine — PHP accepts case-insensitive zone names var key_buf: [16]u8 = undefined; - // emit std side if (z.std_abbrev.len > 0 and z.std_abbrev.len < key_buf.len) { for (z.std_abbrev, 0..) |c, i| key_buf[i] = std.ascii.toLower(c); const key = try ctx.allocator.dupe(u8, key_buf[0..z.std_abbrev.len]); @@ -2109,7 +2092,6 @@ fn dtzListAbbreviations(ctx: *NativeContext, _: []const Value) RuntimeError!Valu } fn canonicalizeZoneName(ctx: *NativeContext, lower_name: []const u8) ![]const u8 { - // turn "america/new_york" into "America/New_York" const out = try ctx.allocator.dupe(u8, lower_name); var capitalize_next = true; for (out, 0..) |c, i| { @@ -2245,7 +2227,6 @@ fn native_strtotime(_: *NativeContext, args: []const Value) RuntimeError!Value { const input = args[0].string; const base: i64 = if (args.len >= 2) Value.toInt(args[1]) else std.time.timestamp(); - // @timestamp - unix timestamp literal if (input.len >= 2 and input[0] == '@') { const ts = std.fmt.parseInt(i64, input[1..], 10) catch return Value{ .bool = false }; return .{ .int = ts }; @@ -2352,7 +2333,6 @@ fn native_strtotime(_: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .int = dateToTimestamp(year, month, day, hour, min, sec) - tz_offset }; } - // DD.MM.YYYY EU date format if (input.len >= 10 and input[2] == '.' and input[5] == '.') { const day = std.fmt.parseInt(i64, input[0..2], 10) catch null; const month = std.fmt.parseInt(i64, input[3..5], 10) catch null; @@ -2418,7 +2398,6 @@ fn tryParseBareTime(input: []const u8) ?TimeOfDay { var i: usize = 0; while (i < s.len and s[i] >= '0' and s[i] <= '9') i += 1; if (i == 0 or i > 2 or i >= s.len or s[i] != ':') return null; - // walk minutes (and optional seconds) i += 1; var d: usize = 0; while (i < s.len and s[i] >= '0' and s[i] <= '9') : (i += 1) d += 1; @@ -2478,7 +2457,6 @@ fn tryParseTextualDate(input: []const u8) ?i64 { s = s[match_len..]; if (leading_day) |dd| { - // DD Month YYYY [HH:MM[:SS]] while (s.len > 0 and (s[0] == ' ' or s[0] == ',')) s = s[1..]; var yend: usize = 0; while (yend < s.len and s[yend] >= '0' and s[yend] <= '9') yend += 1; @@ -2489,7 +2467,6 @@ fn tryParseTextualDate(input: []const u8) ?i64 { return dateToTimestamp(year, month_num.?, dd, tod.hour, tod.min, tod.sec); } - // Month DD[,] YYYY [HH:MM[:SS]] while (s.len > 0 and s[0] == ' ') s = s[1..]; var dend: usize = 0; while (dend < s.len and s[dend] >= '0' and s[dend] <= '9') dend += 1; @@ -2675,7 +2652,6 @@ fn native_date_parse_from_format(ctx: *NativeContext, args: []const Value) Runti } }, 'u' => { - // microseconds, variable digits var end = di; while (end < datetime.len and datetime[end] >= '0' and datetime[end] <= '9') end += 1; if (end > di) { @@ -2689,7 +2665,6 @@ fn native_date_parse_from_format(ctx: *NativeContext, args: []const Value) Runti } }, 'v' => { - // milliseconds 3 digits if (di + 3 <= datetime.len) { if (std.fmt.parseInt(i64, datetime[di..di+3], 10)) |ms| { fraction = @as(f64, @floatFromInt(ms)) / 1000.0; @@ -2705,11 +2680,9 @@ fn native_date_parse_from_format(ctx: *NativeContext, args: []const Value) Runti } }, 'D', 'l' => { - // skip alphabetic day name while (di < datetime.len and std.ascii.isAlphabetic(datetime[di])) di += 1; }, 'M', 'F' => { - // month name - look up var end = di; while (end < datetime.len and std.ascii.isAlphabetic(datetime[end])) end += 1; if (end > di) { @@ -2868,7 +2841,6 @@ fn native_microtime(ctx: *NativeContext, args: []const Value) RuntimeError!Value return .{ .string = result }; } -// date/time utilities pub fn dateToTimestamp(year: i64, month: i64, day: i64, hour: i64, min: i64, sec: i64) i64 { // normalize month overflow/underflow (e.g. month 13 -> january next year) @@ -2891,7 +2863,6 @@ pub fn parseRelativeTime(input: []const u8, base: i64) Value { while (s.len > 0 and s[s.len - 1] == ' ') s = s[0 .. s.len - 1]; if (s.len == 0) return .{ .bool = false }; - // "now" if (eqlLower(s, "now")) return .{ .int = base }; if (startsWithLower(s, "now ")) { var rest = s[4..]; @@ -2926,10 +2897,8 @@ pub fn parseRelativeTime(input: []const u8, base: i64) Value { // RFC 2822 / 7231 - "Mon, 15 Jan 2024 10:30:00 GMT" if (tryParseRfc2822(s)) |ts| return .{ .int = ts }; - // textual month dates if (tryParseTextualDate(s)) |ts| return .{ .int = ts }; - // "today", "yesterday", "tomorrow", "midnight", "noon" if (tryParseKeyword(s, base)) |ts| return .{ .int = ts }; // ordinal weekday: "first Monday of March 2025", "second Tuesday of next month", "last Friday of December" @@ -2938,7 +2907,6 @@ pub fn parseRelativeTime(input: []const u8, base: i64) Value { // "first day of ..." / "last day of ..." if (tryParseFirstLastDay(s, base)) |ts| return .{ .int = ts }; - // "next/last " or "next/last month/year" if (tryParseNextLast(s, base)) |ts| return .{ .int = ts }; // " this|next|last week" - the named weekday within the ISO @@ -3031,7 +2999,6 @@ fn tryParseFirstLastDay(input: []const u8, base: i64) ?i64 { var sec = comps.sec; if (eqlLower(s, "this month")) { - // use current month } else if (eqlLower(s, "next month")) { month += 1; if (month > 12) { month = 1; year += 1; } @@ -3082,7 +3049,6 @@ fn tryParseNextLast(input: []const u8, base: i64) ?i64 { } while (s.len > 0 and s[0] == ' ') s = s[1..]; - // next/last month or year if (eqlLower(s, "month") or eqlLower(s, "year")) { const comps = baseComponents(base); var year = comps.year; @@ -3111,7 +3077,6 @@ fn tryParseNextLast(input: []const u8, base: i64) ?i64 { return target_monday + comps.hour * 3600 + comps.min * 60 + comps.sec; } - // next/last [time-of-day] if (parseWeekdayName(s)) |target_dow| { const wname_len = weekdayNameLen(s) orelse s.len; var rest = s[wname_len..]; @@ -3201,7 +3166,6 @@ fn parseNumericRelative(input: []const u8, base: i64) Value { s = s[unit.consumed..]; while (s.len > 0 and s[0] == ' ') s = s[1..]; - // check for "ago" suffix var effective_sign = sign; if (startsWithLower(s, "ago")) { effective_sign = -effective_sign; @@ -3445,7 +3409,6 @@ const TzEntry = struct { // us dst: second sunday of march 2:00 -> first sunday of november 2:00 // eu dst: last sunday of march 1:00 utc -> last sunday of october 1:00 utc const tz_table = [_]TzEntry{ - // north america .{ .name = "america/new_york", .std_offset = -5 * 3600, .dst_offset = -4 * 3600, .dst_rule = .us, .std_abbrev = "EST", .dst_abbrev = "EDT" }, .{ .name = "america/chicago", .std_offset = -6 * 3600, .dst_offset = -5 * 3600, .dst_rule = .us, .std_abbrev = "CST", .dst_abbrev = "CDT" }, .{ .name = "america/denver", .std_offset = -7 * 3600, .dst_offset = -6 * 3600, .dst_rule = .us, .std_abbrev = "MST", .dst_abbrev = "MDT" }, @@ -3458,7 +3421,6 @@ const tz_table = [_]TzEntry{ .{ .name = "america/sao_paulo", .std_offset = -3 * 3600, .dst_offset = -3 * 3600, .dst_rule = .none, .std_abbrev = "-03", .dst_abbrev = "-03" }, .{ .name = "america/argentina/buenos_aires", .std_offset = -3 * 3600, .dst_offset = -3 * 3600, .dst_rule = .none, .std_abbrev = "-03", .dst_abbrev = "-03" }, .{ .name = "pacific/honolulu", .std_offset = -10 * 3600, .dst_offset = -10 * 3600, .dst_rule = .none, .std_abbrev = "HST", .dst_abbrev = "HST" }, - // europe .{ .name = "europe/london", .std_offset = 0, .dst_offset = 3600, .dst_rule = .eu, .std_abbrev = "GMT", .dst_abbrev = "BST" }, .{ .name = "europe/paris", .std_offset = 3600, .dst_offset = 2 * 3600, .dst_rule = .eu, .std_abbrev = "CET", .dst_abbrev = "CEST" }, .{ .name = "europe/berlin", .std_offset = 3600, .dst_offset = 2 * 3600, .dst_rule = .eu, .std_abbrev = "CET", .dst_abbrev = "CEST" }, @@ -3475,7 +3437,6 @@ const tz_table = [_]TzEntry{ .{ .name = "europe/bucharest", .std_offset = 2 * 3600, .dst_offset = 3 * 3600, .dst_rule = .eu, .std_abbrev = "EET", .dst_abbrev = "EEST" }, .{ .name = "europe/lisbon", .std_offset = 0, .dst_offset = 3600, .dst_rule = .eu, .std_abbrev = "WET", .dst_abbrev = "WEST" }, .{ .name = "europe/warsaw", .std_offset = 3600, .dst_offset = 2 * 3600, .dst_rule = .eu, .std_abbrev = "CET", .dst_abbrev = "CEST" }, - // asia .{ .name = "asia/tokyo", .std_offset = 9 * 3600, .dst_offset = 9 * 3600, .dst_rule = .none, .std_abbrev = "JST", .dst_abbrev = "JST" }, .{ .name = "asia/shanghai", .std_offset = 8 * 3600, .dst_offset = 8 * 3600, .dst_rule = .none, .std_abbrev = "CST", .dst_abbrev = "CST" }, .{ .name = "asia/hong_kong", .std_offset = 8 * 3600, .dst_offset = 8 * 3600, .dst_rule = .none, .std_abbrev = "HKT", .dst_abbrev = "HKT" }, @@ -3489,7 +3450,6 @@ const tz_table = [_]TzEntry{ .{ .name = "asia/karachi", .std_offset = 5 * 3600, .dst_offset = 5 * 3600, .dst_rule = .none, .std_abbrev = "PKT", .dst_abbrev = "PKT" }, .{ .name = "asia/dhaka", .std_offset = 6 * 3600, .dst_offset = 6 * 3600, .dst_rule = .none, .std_abbrev = "+06", .dst_abbrev = "+06" }, .{ .name = "asia/kathmandu", .std_offset = 5 * 3600 + 2700, .dst_offset = 5 * 3600 + 2700, .dst_rule = .none, .std_abbrev = "+0545", .dst_abbrev = "+0545" }, - // oceania .{ .name = "australia/sydney", .std_offset = 10 * 3600, .dst_offset = 11 * 3600, .dst_rule = .au, .std_abbrev = "AEST", .dst_abbrev = "AEDT" }, .{ .name = "australia/melbourne", .std_offset = 10 * 3600, .dst_offset = 11 * 3600, .dst_rule = .au, .std_abbrev = "AEST", .dst_abbrev = "AEDT" }, .{ .name = "australia/hobart", .std_offset = 10 * 3600, .dst_offset = 11 * 3600, .dst_rule = .au, .std_abbrev = "AEST", .dst_abbrev = "AEDT" }, @@ -3503,7 +3463,6 @@ const tz_table = [_]TzEntry{ .{ .name = "africa/lagos", .std_offset = 3600, .dst_offset = 3600, .dst_rule = .none, .std_abbrev = "WAT", .dst_abbrev = "WAT" }, .{ .name = "africa/johannesburg", .std_offset = 2 * 3600, .dst_offset = 2 * 3600, .dst_rule = .none, .std_abbrev = "SAST", .dst_abbrev = "SAST" }, .{ .name = "africa/nairobi", .std_offset = 3 * 3600, .dst_offset = 3 * 3600, .dst_rule = .none, .std_abbrev = "EAT", .dst_abbrev = "EAT" }, - // aliases .{ .name = "utc", .std_offset = 0, .dst_offset = 0, .dst_rule = .none, .std_abbrev = "UTC", .dst_abbrev = "UTC" }, .{ .name = "gmt", .std_offset = 0, .dst_offset = 0, .dst_rule = .none, .std_abbrev = "GMT", .dst_abbrev = "GMT" }, .{ .name = "us/eastern", .std_offset = -5 * 3600, .dst_offset = -4 * 3600, .dst_rule = .us, .std_abbrev = "EST", .dst_abbrev = "EDT" }, @@ -3681,7 +3640,6 @@ test "embedded tzdata resolves non-table zones without system zoneinfo" { const nyr = tzifLookupUtc(ny, ts) orelse return error.TzifParseFailed; try std.testing.expectEqual(@as(i32, -5 * 3600), nyr.offset); // EST in January - // bogus names resolve to nothing try std.testing.expect(embeddedZoneInfo("Not/AReal_Zone") == null); } @@ -3740,7 +3698,6 @@ fn tzifLookupUtc(bytes: []const u8, utc_ts: i64) ?TzifRes { } }.f; - // largest transition <= utc_ts var lo: usize = 0; var hi: usize = timecnt; while (lo < hi) { @@ -3880,7 +3837,6 @@ fn parseFixedOffset(s: []const u8) ?i32 { // find nth occurrence of target_dow (0=sun) in given month/year, or last if n=5 fn nthWeekday(year: i64, month: i64, n: u8, target_dow: u8) i64 { if (n == 5) { - // last occurrence const last_day = daysInMonth(month, year); var day = last_day; while (day >= 1) : (day -= 1) { @@ -3994,7 +3950,6 @@ fn parseTimezoneOffset(s: []const u8) ?i64 { if (s.len > 3 and std.mem.eql(u8, s[0..3], "GMT") and (s[3] == '+' or s[3] == '-')) { return parseTimezoneOffset(s[3..]); } - // named: look up in table if (lookupTimezone(s)) |tz| { return @intCast(tz.std_offset); } @@ -4028,14 +3983,12 @@ fn tryParseRfc2822(input: []const u8) ?i64 { while (s.len > 0 and s[0] == ' ') s = s[1..]; } - // DD Mon YYYY HH:MM:SS var dend: usize = 0; while (dend < s.len and s[dend] >= '0' and s[dend] <= '9') dend += 1; if (dend == 0 or dend > 2 or dend >= s.len or s[dend] != ' ') return null; const day = std.fmt.parseInt(i64, s[0..dend], 10) catch return null; s = s[dend + 1 ..]; - // month abbreviation const short_months = [_][]const u8{ "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" }; var month: i64 = 0; for (short_months, 1..) |name, i| { @@ -4049,7 +4002,6 @@ fn tryParseRfc2822(input: []const u8) ?i64 { if (s.len == 0 or s[0] != ' ') return null; s = s[1..]; - // YYYY if (s.len < 4) return null; const year = std.fmt.parseInt(i64, s[0..4], 10) catch return null; s = s[4..]; @@ -4077,7 +4029,6 @@ fn tryParseOrdinalWeekday(input: []const u8, base: i64) ?i64 { var s = input; while (s.len > 0 and s[0] == ' ') s = s[1..]; - // parse ordinal: first/second/third/fourth/fifth/last or 1st/2nd/3rd/4th/5th const ordinals = [_]struct { name: []const u8, val: i64 }{ .{ .name = "first", .val = 1 }, .{ .name = "second", .val = 2 }, @@ -4101,10 +4052,8 @@ fn tryParseOrdinalWeekday(input: []const u8, base: i64) ?i64 { if (ordinal == 0) return null; } - // parse weekday name while (s.len > 0 and s[0] == ' ') s = s[1..]; const target_dow = parseWeekdayName(s) orelse return null; - // advance past weekday name const full_days = [_][]const u8{ "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday" }; const short_days = [_][]const u8{ "mon", "tue", "wed", "thu", "fri", "sat", "sun" }; var consumed: usize = 0; @@ -4125,7 +4074,6 @@ fn tryParseOrdinalWeekday(input: []const u8, base: i64) ?i64 { s = s[consumed..]; while (s.len > 0 and s[0] == ' ') s = s[1..]; - // expect "of" if (!startsWithLower(s, "of ")) return null; s = s[3..]; while (s.len > 0 and s[0] == ' ') s = s[1..]; @@ -4142,7 +4090,6 @@ fn tryParseOrdinalWeekday(input: []const u8, base: i64) ?i64 { month -= 1; if (month < 1) { month = 12; year -= 1; } } else if (startsWithLower(s, "this month")) { - // use current } else if (parseMonthName(s)) |m| { month = m; const mlen = monthNameLen(s); @@ -4438,14 +4385,12 @@ fn parseRelativeDuration(input: []const u8) RelDuration { while (i < input.len and (input[i] == ' ' or input[i] == '\t')) : (i += 1) {} if (i >= input.len) break; - // optional sign var sign: i64 = 1; if (input[i] == '+' or input[i] == '-') { if (input[i] == '-') sign = -1; i += 1; } - // digits const num_start = i; while (i < input.len and isDigit(input[i])) : (i += 1) {} if (i == num_start) { @@ -4455,10 +4400,8 @@ fn parseRelativeDuration(input: []const u8) RelDuration { } const value = sign * (std.fmt.parseInt(i64, input[num_start..i], 10) catch 0); - // optional whitespace before unit while (i < input.len and (input[i] == ' ' or input[i] == '\t')) : (i += 1) {} - // unit const unit_start = i; while (i < input.len and isAlpha(input[i])) : (i += 1) {} const unit = input[unit_start..i]; diff --git a/src/stdlib/dom.zig b/src/stdlib/dom.zig index 190a3304..0404b23d 100644 --- a/src/stdlib/dom.zig +++ b/src/stdlib/dom.zig @@ -195,7 +195,6 @@ fn getOwnerDocObj(obj: *PhpObject) ?*PhpObject { return null; } -// ---------------- DOMDocument methods ---------------- fn domDocConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { ensureGlobalInit(); @@ -235,7 +234,6 @@ fn domDocLoadXML(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const src = args[0].string; const opts = parseOptions(args, 1); - // replace any prior doc if (getDocPtr(obj)) |old| { c.xmlFreeDoc(old); try setNodePtr(obj, ctx.allocator, null); @@ -327,7 +325,6 @@ fn domDocSaveXML(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } if (node) |n| { - // dump single node const buf = c.xmlBufferCreate(); defer c.xmlBufferFree(buf); const fmt: c_int = if (formatOutputOn(obj)) 1 else 0; @@ -444,7 +441,6 @@ fn domDocCreateElementNS(ctx: *NativeContext, args: []const Value) RuntimeError! const ns_uri: ?[]const u8 = if (args[0] == .string) args[0].string else null; const qname = args[1].string; - // split qname into prefix:localname var prefix_buf: ?[:0]u8 = null; var local_buf: [:0]u8 = undefined; if (std.mem.indexOfScalar(u8, qname, ':')) |colon| { @@ -669,7 +665,6 @@ fn readProperty(ctx: *NativeContext, obj: *PhpObject, prop: []const u8) RuntimeE const node = node_opt.?; if (std.mem.eql(u8, prop, "nodeName")) { - // for documents, return "#document" if (node.type == c.XML_DOCUMENT_NODE or node.type == c.XML_HTML_DOCUMENT_NODE) { return .{ .string = try dupString(ctx, "#document") }; } @@ -844,7 +839,6 @@ fn readProperty(ctx: *NativeContext, obj: *PhpObject, prop: []const u8) RuntimeE return .null; } -// ---------------- DOMNode write methods ---------------- fn domNodeAppendChild(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .object) return .{ .bool = false }; @@ -852,7 +846,6 @@ fn domNodeAppendChild(ctx: *NativeContext, args: []const Value) RuntimeError!Val const parent = getNodePtr(obj) orelse return .{ .bool = false }; const child = getNodePtr(args[0].object) orelse return .{ .bool = false }; - // unlink first if attached c.xmlUnlinkNode(child); const added = c.xmlAddChild(parent, child); if (added == null) return .{ .bool = false }; @@ -969,7 +962,6 @@ fn domNodeLookupNamespaceURI(ctx: *NativeContext, args: []const Value) RuntimeEr return try cstrToValue(ctx, ns.*.href); } -// ---------------- DOMElement methods ---------------- fn domElementGetAttribute(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .string) return .{ .string = try dupString(ctx, "") }; @@ -1035,7 +1027,6 @@ fn domElementSetAttributeNS(ctx: *NativeContext, args: []const Value) RuntimeErr var ns_ptr: ?*c.xmlNs = null; if (args[0] == .string and args[0].string.len > 0) { const uri_z = try dupZ(ctx, args[0].string); - // resolve / create namespace if (std.mem.indexOfScalar(u8, qname, ':')) |colon| { const prefix_z = try dupZ(ctx, qname[0..colon]); ns_ptr = c.xmlSearchNs(node.doc, node, @ptrCast(prefix_z.ptr)); @@ -1124,7 +1115,6 @@ fn domElementGetAttributeNode(ctx: *NativeContext, args: []const Value) RuntimeE return wrapNode(ctx, @ptrCast(attr), getOwnerDocObj(obj) orelse obj); } -// ---------------- DOMCharacterData methods ---------------- fn domCdAppendData(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .string) return .null; @@ -1150,7 +1140,6 @@ fn domCdSubstringData(ctx: *NativeContext, args: []const Value) RuntimeError!Val return .{ .string = try dupString(ctx, slice[off..end]) }; } -// ---------------- DOMNodeList ---------------- fn makeNodeList(ctx: *NativeContext, owner_doc: *PhpObject, nodes: []*c.xmlNode) !Value { const list_obj = try ctx.createObject("DOMNodeList"); @@ -1240,7 +1229,6 @@ fn getThisGlobal() ?*PhpObject { var vm_singleton: ?*VM = null; -// ---------------- DOMNamedNodeMap ---------------- fn makeNamedNodeMap(ctx: *NativeContext, owner_doc: *PhpObject, element: *c.xmlNode) !Value { const map_obj = try ctx.createObject("DOMNamedNodeMap"); @@ -1290,7 +1278,6 @@ fn domNNMCount(_: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .int = @intCast(items.array.entries.items.len) }; } -// ---------------- DOMXPath ---------------- fn domXpathConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .object) return .null; @@ -1330,7 +1317,6 @@ fn domXpathQuery(ctx: *NativeContext, args: []const Value) RuntimeError!Value { defer c.xmlXPathFreeContext(xctx); if (context_node) |cn| xctx.*.node = cn; - // register any user namespaces const ns_map = obj.get("__namespaces"); if (ns_map == .array) { for (ns_map.array.entries.items) |e| { @@ -1442,7 +1428,6 @@ fn domXpathEvaluate(ctx: *NativeContext, args: []const Value) RuntimeError!Value } } -// ---------------- registration ---------------- pub fn register(vm: *VM, a: Allocator) !void { vm_singleton = vm; diff --git a/src/stdlib/filesystem.zig b/src/stdlib/filesystem.zig index 5c720d94..8efab1be 100644 --- a/src/stdlib/filesystem.zig +++ b/src/stdlib/filesystem.zig @@ -397,7 +397,6 @@ pub fn cleanupHandles(objects: std.ArrayListUnmanaged(*PhpObject)) void { for (objects.items) |obj| cleanupHandle(obj); } -// file read/write const c_curl = @cImport({ @cInclude("curl/curl.h"); @@ -762,7 +761,6 @@ fn native_realpath(ctx: *NativeContext, args: []const Value) RuntimeError!Value return .{ .string = try ctx.createString(resolved) }; } -// directory operations fn native_mkdir(_: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len == 0 or args[0] != .string) return .{ .bool = false }; @@ -814,11 +812,9 @@ fn native_rename(_: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = true }; } -// directory listing fn native_scandir(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len == 0 or args[0] != .string) return .{ .bool = false }; - // SCANDIR_SORT_ASCENDING=0, _DESCENDING=1, _NONE=2 const order: i64 = if (args.len >= 2 and args[1] == .int) args[1].int else 0; var dir = std.fs.cwd().openDir(args[0].string, .{ .iterate = true }) catch return Value{ .bool = false }; defer dir.close(); @@ -1061,7 +1057,6 @@ fn globMatchFlags(pattern: []const u8, name: []const u8, flags: i64) bool { if (pi < pattern.len and ni < name.len) { // FNM_PATHNAME: '/' in name must be matched literally; * and ? don't cross it if (pathname and name[ni] == '/' and pattern[pi] != '/') { - // fall through to backtrack } else if (pattern[pi] == '?' or eq(pattern[pi], name[ni], casefold)) { pi += 1; ni += 1; @@ -1112,7 +1107,6 @@ fn globMatchFlags(pattern: []const u8, name: []const u8, flags: i64) bool { return true; } -// file info fn native_is_readable(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len == 0 or args[0] != .string) return .{ .bool = false }; @@ -1168,7 +1162,6 @@ fn native_fileinode(_: *NativeContext, args: []const Value) RuntimeError!Value { fn native_filetype(_: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len == 0 or args[0] != .string) return .{ .bool = false }; const stat = std.fs.cwd().statFile(args[0].string) catch { - // might be a directory var dir = std.fs.cwd().openDir(args[0].string, .{}) catch return Value{ .bool = false }; dir.close(); return .{ .string = "dir" }; @@ -1226,7 +1219,6 @@ fn native_readfile(ctx: *NativeContext, args: []const Value) RuntimeError!Value return .{ .int = @intCast(content.len) }; } -// file handle operations (fopen/fclose/fread/fwrite/fgets/feof/fseek/ftell) fn native_gzopen(ctx: *NativeContext, args: []const Value) RuntimeError!Value { // gzopen($path, $mode) opens a gzipped file with transparent compression. @@ -1561,7 +1553,6 @@ fn native_fread(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .string = "" }; } if (n < length) try obj.set(ctx.allocator, "__eof", .{ .bool = true }); - // shrink to actual read size if (n < length) { const exact = try ctx.allocator.alloc(u8, n); @memcpy(exact, buf[0..n]); diff --git a/src/stdlib/ftp.zig b/src/stdlib/ftp.zig index 58837ae3..27758ddc 100644 --- a/src/stdlib/ftp.zig +++ b/src/stdlib/ftp.zig @@ -77,7 +77,6 @@ fn readReply(allocator: std.mem.Allocator, fd: posix.socket_t) !Reply { const code_str = line[0..3]; const code = std.fmt.parseInt(u16, code_str, 10) catch return error.BadReply; if (line[3] == '-') { - // multi-line while (true) { const n = try readLine(fd, &line); if (n == 0) break; @@ -253,7 +252,6 @@ fn native_ftp_size(ctx: *NativeContext, args: []const Value) RuntimeError!Value const r = runCmd(ctx.allocator, fd, line) catch return .{ .int = -1 }; defer ctx.allocator.free(r.body); if (r.code != 213) return .{ .int = -1 }; - // 213 NNN\r\n var iter = std.mem.tokenizeAny(u8, r.body, " \r\n"); _ = iter.next(); // code const num_str = iter.next() orelse return .{ .int = -1 }; @@ -274,7 +272,6 @@ fn native_ftp_mdtm(ctx: *NativeContext, args: []const Value) RuntimeError!Value _ = iter.next(); const ts = iter.next() orelse return .{ .int = -1 }; if (ts.len < 14) return .{ .int = -1 }; - // YYYYMMDDHHMMSS UTC const year = std.fmt.parseInt(i32, ts[0..4], 10) catch return .{ .int = -1 }; const mon = std.fmt.parseInt(u8, ts[4..6], 10) catch return .{ .int = -1 }; const day = std.fmt.parseInt(u8, ts[6..8], 10) catch return .{ .int = -1 }; @@ -287,7 +284,6 @@ fn native_ftp_mdtm(ctx: *NativeContext, args: []const Value) RuntimeError!Value } fn daysFromCivil(y_in: i32, m: u8, d: u8) i64 { - // Howard Hinnant algorithm var y: i32 = y_in; if (m <= 2) y -= 1; const era: i32 = @divFloor(if (y >= 0) y else y - 399, 400); diff --git a/src/stdlib/gd.zig b/src/stdlib/gd.zig index 2e155c1a..a5eae4a2 100644 --- a/src/stdlib/gd.zig +++ b/src/stdlib/gd.zig @@ -76,7 +76,6 @@ fn argImg(args: []const Value, idx: usize) ?*c.gdImageStruct { return getImg(obj); } -// ---------------- create / destroy ---------------- fn imgCreate(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const w = argInt(args, 0) orelse return .{ .bool = false }; @@ -149,7 +148,6 @@ fn imgCreateFromString(ctx: *NativeContext, args: []const Value) RuntimeError!Va return .{ .bool = false }; } -// ---------------- output ---------------- fn writeImageTo(ctx: *NativeContext, im: *c.gdImageStruct, args: []const Value, kind: enum { png, jpeg, gif }, quality: c_int) !Value { if (args.len > 1 and args[1] == .string) { @@ -196,7 +194,6 @@ fn imgGif(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return writeImageTo(ctx, im, args, .gif, 0); } -// ---------------- colors ---------------- fn imgColorAllocate(_: *NativeContext, args: []const Value) RuntimeError!Value { const im = argImg(args, 0) orelse return .{ .bool = false }; @@ -267,7 +264,6 @@ fn imgColorsForIndex(ctx: *NativeContext, args: []const Value) RuntimeError!Valu return .{ .array = arr }; } -// ---------------- drawing ---------------- fn imgSetPixel(_: *NativeContext, args: []const Value) RuntimeError!Value { const im = argImg(args, 0) orelse return .{ .bool = false }; @@ -355,7 +351,6 @@ fn imgFill(_: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = true }; } -// ---------------- text ---------------- fn imgString(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const im = argImg(args, 0) orelse return .{ .bool = false }; @@ -406,7 +401,6 @@ fn imgTtfText(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .array = arr }; } -// ---------------- copy ---------------- fn imgCopy(_: *NativeContext, args: []const Value) RuntimeError!Value { const dst = argImg(args, 0) orelse return .{ .bool = false }; @@ -527,7 +521,6 @@ fn imgCrop(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return wrapImg(ctx, dst); } -// ---------------- dimensions ---------------- fn imgSx(_: *NativeContext, args: []const Value) RuntimeError!Value { const im = argImg(args, 0) orelse return .{ .bool = false }; @@ -539,7 +532,6 @@ fn imgSy(_: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .int = @intCast(im.sy) }; } -// ---------------- alpha / interlace ---------------- fn imgAlphaBlending(_: *NativeContext, args: []const Value) RuntimeError!Value { const im = argImg(args, 0) orelse return .{ .bool = false }; @@ -562,7 +554,6 @@ fn imgInterlace(_: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = true }; } -// ---------------- info ---------------- fn imgGetSize(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .string) return .{ .bool = false }; @@ -677,7 +668,6 @@ fn gdInfo(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .array = arr }; } -// ---------------- registration ---------------- pub const entries = .{ .{ "image_type_to_mime_type", imageTypeToMimeType }, @@ -733,7 +723,6 @@ pub fn register(vm: *VM, a: Allocator) !void { const def = ClassDef{ .name = "GdImage", .native_cleanup = cleanupImage }; try vm.classes.put(a, "GdImage", def); - // font size constants try vm.php_constants.put(a, "IMG_PNG", .{ .int = 3 }); try vm.php_constants.put(a, "IMG_JPG", .{ .int = 2 }); try vm.php_constants.put(a, "IMG_JPEG", .{ .int = 2 }); diff --git a/src/stdlib/gmp.zig b/src/stdlib/gmp.zig index 4b6c3fc2..7436149c 100644 --- a/src/stdlib/gmp.zig +++ b/src/stdlib/gmp.zig @@ -57,7 +57,6 @@ extern fn zphp_mpz_legendre(a: *const ZphpMpz, p: *const ZphpMpz) c_int; extern fn zphp_mpz_jacobi(a: *const ZphpMpz, b: *const ZphpMpz) c_int; extern fn zphp_mpz_perfect_square_p(a: *const ZphpMpz) c_int; -// ---------------- helpers ---------------- fn dupString(ctx: *NativeContext, s: []const u8) ![]const u8 { const owned = try ctx.allocator.dupe(u8, s); @@ -137,7 +136,6 @@ fn coerceArgToMpz(ctx: *NativeContext, v: Value) !?*ZphpMpz { } } -// ---------------- top-level functions ---------------- fn gmpInit(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return .{ .bool = false }; @@ -433,7 +431,6 @@ fn gmpRoot(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .object = obj }; } -// ---------------- registration ---------------- pub const entries = .{ .{ "gmp_init", gmpInit }, diff --git a/src/stdlib/ini.zig b/src/stdlib/ini.zig index f1738962..b28108b9 100644 --- a/src/stdlib/ini.zig +++ b/src/stdlib/ini.zig @@ -49,13 +49,11 @@ fn parseIni(ctx: *NativeContext, input: []const u8, process_sections: bool, mode var line = input[line_start..pos]; if (pos < input.len) pos += 1; - // strip \r if (line.len > 0 and line[line.len - 1] == '\r') line = line[0 .. line.len - 1]; line = trimSpaces(line); if (line.len == 0 or line[0] == ';' or line[0] == '#') continue; - // section header if (line[0] == '[') { if (std.mem.indexOfScalar(u8, line, ']')) |end| { if (process_sections) { @@ -70,7 +68,6 @@ fn parseIni(ctx: *NativeContext, input: []const u8, process_sections: bool, mode continue; } - // key = value if (std.mem.indexOfScalar(u8, line, '=')) |eq_pos| { const raw_key = trimSpaces(line[0..eq_pos]); const raw_val = trimSpaces(if (eq_pos + 1 < line.len) line[eq_pos + 1 ..] else ""); @@ -92,7 +89,6 @@ fn parseIni(ctx: *NativeContext, input: []const u8, process_sections: bool, mode fn processValue(ctx: *NativeContext, raw: []const u8, mode: ScannerMode) Value { if (raw.len == 0) return .{ .string = "" }; - // strip quotes if (raw.len >= 2 and ((raw[0] == '"' and raw[raw.len - 1] == '"') or (raw[0] == '\'' and raw[raw.len - 1] == '\''))) { return .{ .string = raw[1 .. raw.len - 1] }; } @@ -133,7 +129,6 @@ fn processValue(ctx: *NativeContext, raw: []const u8, mode: ScannerMode) Value { return .{ .string = val }; } - // normal mode: booleans become "1"/"" if (isBoolTrue(val)) return .{ .string = "1" }; if (isBoolFalse(val)) return .{ .string = "" }; return .{ .string = val }; diff --git a/src/stdlib/intl.zig b/src/stdlib/intl.zig index 153a5588..ce4e63c7 100644 --- a/src/stdlib/intl.zig +++ b/src/stdlib/intl.zig @@ -151,7 +151,6 @@ extern fn zphp_uidna_nameToUnicode(idna: *const UIDNA, name: [*]const UChar, nam extern fn zphp_uidna_info_size() usize; extern fn zphp_uidna_info_init(info: *anyopaque) void; -// ---------------- helpers ---------------- fn dupString(ctx: *NativeContext, s: []const u8) ![]const u8 { const owned = try ctx.allocator.dupe(u8, s); @@ -208,7 +207,6 @@ fn u16ToUtf8(ctx: *NativeContext, s: []const u16) ![]const u8 { return buf; } -// ---------------- Normalizer ---------------- fn getNormalizer(form: i64) ?*const UNormalizer2 { var status: UErrorCode = U_ZERO_ERROR; @@ -258,7 +256,6 @@ fn normalizerIsNormalized(ctx: *NativeContext, args: []const Value) RuntimeError return .{ .bool = ok != 0 }; } -// ---------------- Locale ---------------- fn localeGetDefault(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const def = zphp_uloc_getDefault(); @@ -327,7 +324,6 @@ fn localeGetDisplayLanguage(ctx: *NativeContext, args: []const Value) RuntimeErr fn localeGetDisplayRegion(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return displayCall(ctx, args, zphp_uloc_getDisplayCountry); } fn localeGetDisplayScript(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return displayCall(ctx, args, zphp_uloc_getDisplayScript); } -// ---------------- Collator ---------------- fn getCollator(obj: *const PhpObject) ?*UCollator { const v = obj.get("__coll"); @@ -422,7 +418,6 @@ fn collSort(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = true }; } -// ---------------- NumberFormatter ---------------- fn getNumFmt(obj: *const PhpObject) ?*UNumberFormat { const v = obj.get("__nfmt"); @@ -544,7 +539,6 @@ fn nfGetAttribute(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .int = @intCast(zphp_unum_getAttribute(f, @intCast(args[0].int))) }; } -// ---------------- Transliterator ---------------- fn getTranslit(obj: *const PhpObject) ?*UTransliterator { const v = obj.get("__trans"); @@ -587,7 +581,6 @@ fn transTransliterate(ctx: *NativeContext, args: []const Value) RuntimeError!Val return .{ .string = out }; } -// ---------------- IntlDateFormatter ---------------- fn getDateFmt(obj: *const PhpObject) ?*UDateFormat { const v = obj.get("__dfmt"); @@ -628,7 +621,6 @@ fn dfConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const date_style: i32 = if (args[1] == .int) @intCast(args[1].int) else 0; const time_style: i32 = if (args[2] == .int) @intCast(args[2].int) else 0; const tz_opt: ?[]const u8 = if (args.len > 3 and args[3] == .string and args[3].string.len > 0) args[3].string else defaultTzName(ctx); - // skip args[4] (calendar) for now const pat_opt: ?[]const u8 = if (args.len > 5 and args[5] == .string and args[5].string.len > 0) args[5].string else null; const f = (try openDateFmt(ctx, args[0].string, date_style, time_style, tz_opt, pat_opt)) orelse return .null; @@ -741,7 +733,6 @@ fn idnToUtf8(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return idnConvert(ctx, args, false); } -// ---------------- MessageFormatter ---------------- fn buildArgEntries(ctx: *NativeContext, arr: *PhpArray, owned_u16: *std.ArrayListUnmanaged([]u16)) ![]ArgEntry { const out = try ctx.allocator.alloc(ArgEntry, arr.entries.items.len); @@ -871,7 +862,6 @@ fn mfGetLocale(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .string = try dupString(ctx, "") }; } -// ---------------- IntlCalendar ---------------- fn getCal(obj: *const PhpObject) ?*UCalendar { const v = obj.get("__cal"); @@ -915,7 +905,6 @@ fn calConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { // (int y, int m, int d, int h, int mi, int s) const obj = getThis(ctx) orelse return .null; - // tz/locale form if (args.len <= 2 and (args.len == 0 or args[0] == .string or args[0] == .null)) { var tz_opt: ?[]const u8 = null; if (args.len > 0 and args[0] == .string) tz_opt = args[0].string; @@ -970,7 +959,6 @@ fn calSet(ctx: *NativeContext, args: []const Value) RuntimeError!Value { zphp_ucal_set(cal, @intCast(args[0].int), @intCast(args[1].int)); return .{ .bool = true }; } - // year/month/day positional form const UCAL_YEAR: i32 = 1; const UCAL_MONTH: i32 = 2; const UCAL_DATE: i32 = 5; @@ -1143,7 +1131,6 @@ fn calGetActualMinimum(ctx: *NativeContext, args: []const Value) RuntimeError!Va const obj = getThis(ctx) orelse return .{ .bool = false }; const cal = getCal(obj) orelse return .{ .bool = false }; var status: UErrorCode = U_ZERO_ERROR; - // UCAL_ACTUAL_MINIMUM = 4 return .{ .int = @intCast(zphp_ucal_getLimit(cal, @intCast(args[0].int), 4, &status)) }; } @@ -1174,7 +1161,6 @@ fn calEquals(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = zphp_ucal_getMillis(a, &s1) == zphp_ucal_getMillis(b, &s2) }; } -// ---------------- IntlBreakIterator ---------------- fn getBrk(obj: *const PhpObject) ?*ZphpBrk { const v = obj.get("__brk"); @@ -1336,7 +1322,6 @@ fn brkGetLocale(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .string = try dupString(ctx, buf[0..@intCast(n)]) }; } -// ---------------- registration ---------------- const NativeFn = *const fn (*NativeContext, []const Value) RuntimeError!Value; @@ -1479,7 +1464,6 @@ fn graphemeSubstr(ctx: *NativeContext, args: []const Value) RuntimeError!Value { zphp_ubrk_setText(w, s.ptr, @intCast(s.len), &status); if (intlRecord(ctx.vm, status)) return .{ .bool = false }; - // collect grapheme byte offsets var offsets = std.ArrayListUnmanaged(i32){}; defer offsets.deinit(ctx.allocator); var pos = zphp_ubrk_first(w); @@ -2269,7 +2253,6 @@ fn localeParseLocale(ctx: *NativeContext, args: []const Value) RuntimeError!Valu try out.set(ctx.allocator, .{ .string = "region" }, .{ .string = parts[idx] }); idx += 1; } - // remaining are variants var vi: usize = 0; while (idx < n) : ({ idx += 1; vi += 1; }) { var key_buf: [16]u8 = undefined; diff --git a/src/stdlib/json.zig b/src/stdlib/json.zig index 57901f52..1ec8fbc1 100644 --- a/src/stdlib/json.zig +++ b/src/stdlib/json.zig @@ -402,10 +402,8 @@ fn encodeValue(buf: *std.ArrayListUnmanaged(u8), a: std.mem.Allocator, val: Valu } } - // dynamic properties (always public) var dyn_iter = obj.properties.iterator(); while (dyn_iter.next()) |entry| { - // skip if already in slots var in_slots = false; if (obj.slot_layout) |layout| { for (layout.names) |sn| { @@ -465,7 +463,6 @@ fn formatJsonScientific(buf: *[64]u8, f: f64) []const u8 { if (!has_dot) { return std.fmt.bufPrint(buf, "{s}.0e{s}", .{ mant, exp }) catch "0"; } - // has_dot, no sign on exp return std.fmt.bufPrint(buf, "{s}e+{s}", .{ mant, exp }) catch "0"; } diff --git a/src/stdlib/mysqli.zig b/src/stdlib/mysqli.zig index bc994c33..739b2db6 100644 --- a/src/stdlib/mysqli.zig +++ b/src/stdlib/mysqli.zig @@ -550,7 +550,6 @@ fn mysqliReport(_: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = true }; } -// ---------- class constructor ---------- fn mysqliConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const v = ctx.vm.currentFrame().vars.get("$this") orelse return .null; @@ -576,7 +575,6 @@ fn mysqliConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value return .null; } -// ---------- registration ---------- pub const entries = .{ .{ "mysqli_init", mysqliInit }, diff --git a/src/stdlib/native_params.zig b/src/stdlib/native_params.zig index 2ddcc793..ef9d513b 100644 --- a/src/stdlib/native_params.zig +++ b/src/stdlib/native_params.zig @@ -5,9 +5,9 @@ const std = @import("std"); // names include the $ prefix to match php convention and user-defined function param format. pub const map = std.StaticStringMap([]const []const u8).initComptime(.{ - // strings .{ "substr", &.{ "$string", "$offset", "$length" } }, .{ "str_replace", &.{ "$search", "$replace", "$subject", "$count" } }, + .{ "str_ireplace", &.{ "$search", "$replace", "$subject", "$count" } }, .{ "explode", &.{ "$separator", "$string", "$limit" } }, .{ "implode", &.{ "$separator", "$array" } }, .{ "join", &.{ "$separator", "$array" } }, @@ -22,6 +22,23 @@ pub const map = std.StaticStringMap([]const []const u8).initComptime(.{ .{ "substr_count", &.{ "$haystack", "$needle", "$offset", "$length" } }, .{ "substr_replace", &.{ "$string", "$replace", "$offset", "$length" } }, .{ "str_split", &.{ "$string", "$length" } }, + .{ "str_shuffle", &.{ "$string" } }, + .{ "str_word_count", &.{ "$string", "$format", "$characters" } }, + .{ "strcasecmp", &.{ "$string1", "$string2" } }, + .{ "strcmp", &.{ "$string1", "$string2" } }, + .{ "strncasecmp", &.{ "$string1", "$string2", "$length" } }, + .{ "strncmp", &.{ "$string1", "$string2", "$length" } }, + .{ "strtr", &.{ "$string", "$from", "$to" } }, + .{ "ucfirst", &.{ "$string" } }, + .{ "lcfirst", &.{ "$string" } }, + .{ "ucwords", &.{ "$string", "$separators" } }, + .{ "strip_tags", &.{ "$string", "$allowed_tags" } }, + .{ "addslashes", &.{ "$string" } }, + .{ "stripslashes", &.{ "$string" } }, + .{ "bin2hex", &.{ "$string" } }, + .{ "hex2bin", &.{ "$string" } }, + .{ "base64_encode", &.{ "$string" } }, + .{ "base64_decode", &.{ "$string", "$strict" } }, .{ "wordwrap", &.{ "$string", "$width", "$break", "$cut_long_words" } }, .{ "number_format", &.{ "$num", "$decimals", "$decimal_separator", "$thousands_separator" } }, .{ "sprintf", &.{ "$format" } }, @@ -30,16 +47,42 @@ pub const map = std.StaticStringMap([]const []const u8).initComptime(.{ .{ "rtrim", &.{ "$string", "$characters" } }, .{ "htmlspecialchars", &.{ "$string", "$flags", "$encoding", "$double_encode" } }, .{ "nl2br", &.{ "$string", "$use_xhtml" } }, + .{ "parse_url", &.{ "$url", "$component" } }, + .{ "parse_str", &.{ "$string", "$result" } }, + .{ "http_build_query", &.{ "$data", "$numeric_prefix", "$arg_separator", "$encoding_type" } }, + .{ "urlencode", &.{ "$string" } }, + .{ "urldecode", &.{ "$string" } }, + .{ "rawurlencode", &.{ "$string" } }, + .{ "rawurldecode", &.{ "$string" } }, + + .{ "mb_strlen", &.{ "$string", "$encoding" } }, + .{ "mb_substr", &.{ "$string", "$start", "$length", "$encoding" } }, + .{ "mb_strpos", &.{ "$haystack", "$needle", "$offset", "$encoding" } }, + .{ "mb_stripos", &.{ "$haystack", "$needle", "$offset", "$encoding" } }, + .{ "mb_strrpos", &.{ "$haystack", "$needle", "$offset", "$encoding" } }, + .{ "mb_strtolower", &.{ "$string", "$encoding" } }, + .{ "mb_strtoupper", &.{ "$string", "$encoding" } }, - // arrays .{ "in_array", &.{ "$needle", "$haystack", "$strict" } }, .{ "array_search", &.{ "$needle", "$haystack", "$strict" } }, .{ "array_key_exists", &.{ "$key", "$array" } }, + .{ "array_keys", &.{ "$array", "$filter_value", "$strict" } }, + .{ "array_values", &.{ "$array" } }, + .{ "array_flip", &.{ "$array" } }, + .{ "array_replace", &.{ "$array" } }, + .{ "array_count_values", &.{ "$array" } }, + .{ "array_key_first", &.{ "$array" } }, + .{ "array_key_last", &.{ "$array" } }, + .{ "array_is_list", &.{ "$array" } }, + .{ "array_sum", &.{ "$array" } }, + .{ "array_product", &.{ "$array" } }, + .{ "array_shift", &.{ "$array" } }, + .{ "array_pop", &.{ "$array" } }, + .{ "array_rand", &.{ "$array", "$num" } }, .{ "array_map", &.{ "$callback", "$array" } }, .{ "array_filter", &.{ "$array", "$callback", "$mode" } }, .{ "array_slice", &.{ "$array", "$offset", "$length", "$preserve_keys" } }, .{ "array_splice", &.{ "$array", "$offset", "$length", "$replacement" } }, - .{ "array_merge", &.{ "$array" } }, .{ "array_combine", &.{ "$keys", "$values" } }, .{ "array_chunk", &.{ "$array", "$length", "$preserve_keys" } }, .{ "array_column", &.{ "$array", "$column_key", "$index_key" } }, @@ -50,59 +93,71 @@ pub const map = std.StaticStringMap([]const []const u8).initComptime(.{ .{ "array_pad", &.{ "$array", "$length", "$value" } }, .{ "array_reduce", &.{ "$array", "$callback", "$initial" } }, .{ "array_walk", &.{ "$array", "$callback", "$arg" } }, + .{ "array_walk_recursive", &.{ "$array", "$callback", "$arg" } }, .{ "usort", &.{ "$array", "$callback" } }, .{ "uasort", &.{ "$array", "$callback" } }, .{ "uksort", &.{ "$array", "$callback" } }, - .{ "compact", &.{ "$var_names" } }, + .{ "compact", &.{ "$var_name" } }, .{ "range", &.{ "$start", "$end", "$step" } }, - // types .{ "intval", &.{ "$value", "$base" } }, .{ "settype", &.{ "$var", "$type" } }, .{ "call_user_func", &.{ "$callback" } }, .{ "version_compare", &.{ "$version1", "$version2", "$operator" } }, - // json .{ "json_encode", &.{ "$value", "$flags", "$depth" } }, .{ "json_decode", &.{ "$json", "$associative", "$depth", "$flags" } }, - // math .{ "round", &.{ "$num", "$precision", "$mode" } }, .{ "rand", &.{ "$min", "$max" } }, .{ "mt_rand", &.{ "$min", "$max" } }, .{ "base_convert", &.{ "$num", "$from_base", "$to_base" } }, + .{ "abs", &.{ "$num" } }, + .{ "floor", &.{ "$num" } }, + .{ "ceil", &.{ "$num" } }, - // io/filesystem - .{ "file_get_contents", &.{ "$filename" } }, - .{ "file_put_contents", &.{ "$filename", "$data", "$flags" } }, - .{ "fopen", &.{ "$filename", "$mode" } }, + .{ "file_get_contents", &.{ "$filename", "$use_include_path", "$context", "$offset", "$length" } }, + .{ "file_put_contents", &.{ "$filename", "$data", "$flags", "$context" } }, + .{ "fopen", &.{ "$filename", "$mode", "$use_include_path", "$context" } }, .{ "fread", &.{ "$stream", "$length" } }, .{ "fwrite", &.{ "$stream", "$data", "$length" } }, - .{ "mkdir", &.{ "$directory", "$permissions", "$recursive" } }, + .{ "mkdir", &.{ "$directory", "$permissions", "$recursive", "$context" } }, + .{ "scandir", &.{ "$directory", "$sorting_order", "$context" } }, + .{ "glob", &.{ "$pattern", "$flags" } }, + .{ "file_exists", &.{ "$filename" } }, + .{ "is_file", &.{ "$filename" } }, + .{ "is_dir", &.{ "$filename" } }, + .{ "is_readable", &.{ "$filename" } }, + .{ "is_writable", &.{ "$filename" } }, + .{ "copy", &.{ "$from", "$to", "$context" } }, + .{ "rename", &.{ "$from", "$to", "$context" } }, + .{ "unlink", &.{ "$filename", "$context" } }, + .{ "chmod", &.{ "$filename", "$permissions" } }, + .{ "touch", &.{ "$filename", "$mtime", "$atime" } }, + .{ "pathinfo", &.{ "$path", "$flags" } }, + .{ "basename", &.{ "$path", "$suffix" } }, + .{ "dirname", &.{ "$path", "$levels" } }, - // regex .{ "preg_match", &.{ "$pattern", "$subject", "$matches", "$flags", "$offset" } }, .{ "preg_match_all", &.{ "$pattern", "$subject", "$matches", "$flags", "$offset" } }, .{ "preg_replace", &.{ "$pattern", "$replacement", "$subject", "$limit", "$count" } }, .{ "preg_split", &.{ "$pattern", "$subject", "$limit", "$flags" } }, - // datetime .{ "date", &.{ "$format", "$timestamp" } }, .{ "mktime", &.{ "$hour", "$minute", "$second", "$month", "$day", "$year" } }, .{ "strtotime", &.{ "$datetime", "$baseTimestamp" } }, - // crypto .{ "password_hash", &.{ "$password", "$algo", "$options" } }, .{ "password_verify", &.{ "$password", "$hash" } }, - .{ "hash", &.{ "$algo", "$data", "$binary" } }, + .{ "hash", &.{ "$algo", "$data", "$binary", "$options" } }, .{ "hash_hmac", &.{ "$algo", "$data", "$key", "$binary" } }, + .{ "hash_equals", &.{ "$known_string", "$user_string" } }, + .{ "hash_pbkdf2", &.{ "$algo", "$password", "$salt", "$iterations", "$length", "$binary", "$options" } }, .{ "random_int", &.{ "$min", "$max" } }, - // output .{ "var_dump", &.{ "$value" } }, .{ "print_r", &.{ "$value", "$return" } }, .{ "var_export", &.{ "$value", "$return" } }, - // session .{ "setcookie", &.{ "$name", "$value", "$expires_or_options", "$path", "$domain", "$secure", "$httponly" } }, }); diff --git a/src/stdlib/network.zig b/src/stdlib/network.zig index 402228e1..bfc5ba73 100644 --- a/src/stdlib/network.zig +++ b/src/stdlib/network.zig @@ -275,12 +275,10 @@ fn native_gethostname(ctx: *NativeContext, _: []const Value) RuntimeError!Value fn native_inet_pton(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len == 0 or args[0] != .string) return .{ .bool = false }; const s = args[0].string; - // try IPv4 if (std.net.Address.parseIp4(s, 0)) |addr| { const bytes = std.mem.toBytes(addr.in.sa.addr); return .{ .string = try createString(ctx, &bytes) }; } else |_| {} - // try IPv6 if (std.net.Address.parseIp6(s, 0)) |addr| { return .{ .string = try createString(ctx, &addr.in6.sa.addr) }; } else |_| {} @@ -301,7 +299,6 @@ fn native_inet_ntop(ctx: *NativeContext, args: []const Value) RuntimeError!Value const ip = std.net.Address.initIp6(addr, 0, 0, 0); var buf: [64]u8 = undefined; const out = std.fmt.bufPrint(&buf, "{f}", .{ip}) catch return .{ .bool = false }; - // strip [...]:port wrapping var s = out; if (s.len > 0 and s[0] == '[') { const close = std.mem.indexOfScalar(u8, s, ']') orelse return .{ .bool = false }; diff --git a/src/stdlib/output.zig b/src/stdlib/output.zig index f6867a85..a42f563d 100644 --- a/src/stdlib/output.zig +++ b/src/stdlib/output.zig @@ -132,7 +132,6 @@ fn varDumpValue(ctx: *NativeContext, val: Value, depth: usize) !void { } } - // honor __debugInfo if defined var debug_arr: ?*@import("../runtime/value.zig").PhpArray = null; if (ctx.vm.hasMethod(obj.class_name, "__debugInfo")) { const result = try ctx.vm.callMethod(obj, "__debugInfo", &.{}); diff --git a/src/stdlib/pcre.zig b/src/stdlib/pcre.zig index 742e3c90..7f712079 100644 --- a/src/stdlib/pcre.zig +++ b/src/stdlib/pcre.zig @@ -290,7 +290,6 @@ test "normalizeOpenBoundQuantifier" { try t.expectEqual(@as(?[]u8, null), normalizeOpenBoundQuantifier("[{,3}]a")); try t.expectEqual(@as(?[]u8, null), normalizeOpenBoundQuantifier("[]{,3}]a")); try t.expectEqual(@as(?[]u8, null), normalizeOpenBoundQuantifier("[^]{,3}]a")); - // \Q...\E quotes everything try t.expectEqual(@as(?[]u8, null), normalizeOpenBoundQuantifier("\\Qa{,3}\\Eb")); { const r = normalizeOpenBoundQuantifier("\\Qa{,3}\\Eb{,2}").?; @@ -1008,7 +1007,6 @@ fn pregReplaceLimited(ctx: *NativeContext, code: *pcre2.Code, match_data: *pcre2 try parts.appendSlice(ctx.allocator, subject[offset..ms]); - // handle backreferences in replacement var ri: usize = 0; while (ri < replacement.len) { if (replacement[ri] == '$' and ri + 1 < replacement.len and replacement[ri + 1] >= '0' and replacement[ri + 1] <= '9') { @@ -1106,7 +1104,6 @@ fn preg_replace_callback(ctx: *NativeContext, args: []const Value) RuntimeError! _ = pcre2.pcre2_pattern_info_8(code, pcre2.INFO_CAPTURECOUNT, @ptrCast(&capture_count)); const group_count: usize = capture_count + 1; - // collect named-group info once var name_count: u32 = 0; _ = pcre2.pcre2_pattern_info_8(code, pcre2.INFO_NAMECOUNT, @ptrCast(&name_count)); var name_entry_size: u32 = 0; diff --git a/src/stdlib/pdo.zig b/src/stdlib/pdo.zig index cbc2946a..2bb23c58 100644 --- a/src/stdlib/pdo.zig +++ b/src/stdlib/pdo.zig @@ -272,7 +272,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try pdo_def.methods.put(a, "sqliteCreateAggregate", .{ .name = "sqliteCreateAggregate", .arity = 3 }); try pdo_def.methods.put(a, "sqliteCreateCollation", .{ .name = "sqliteCreateCollation", .arity = 2 }); - // PDO constants as static properties try pdo_def.static_props.put(a, "FETCH_BOTH", .{ .int = 4 }); try pdo_def.static_props.put(a, "FETCH_ASSOC", .{ .int = 2 }); try pdo_def.static_props.put(a, "FETCH_NUM", .{ .int = 3 }); @@ -455,7 +454,6 @@ pub fn register(vm: *VM, a: Allocator) !void { fn stmtIterRewind(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; try obj.set(ctx.allocator, "__iter_key", .{ .int = 0 }); - // fetch the first row const row = try stmtFetch(ctx, &.{}); try obj.set(ctx.allocator, "__iter_current", row); return .null; @@ -564,7 +562,6 @@ pub fn cleanupResources(objects: std.ArrayListUnmanaged(*PhpObject)) void { } } -// PDO methods const pdo_mysql = @import("pdo_mysql.zig"); const pdo_pgsql = @import("pdo_pgsql.zig"); @@ -903,7 +900,6 @@ fn pdoGetAttribute(ctx: *NativeContext, args: []const Value) RuntimeError!Value return .null; } -// PDOStatement methods fn stmtExecute(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -914,7 +910,6 @@ fn stmtExecute(ctx: *NativeContext, args: []const Value) RuntimeError!Value { _ = sqlite.sqlite3_reset(stmt); - // bind parameters if provided if (args.len >= 1 and args[0] == .array) { try bindParams(ctx, stmt, args[0].array); } @@ -933,7 +928,6 @@ fn stmtExecute(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = false }; } - // store affected rows const db_val = obj.get("__db_ptr"); if (db_val == .int and db_val.int != 0) { const db: *sqlite.Db = @ptrFromInt(@as(usize, @intCast(db_val.int))); @@ -996,7 +990,6 @@ fn stmtFetch(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const row = try fetchRow(ctx, stmt, mode); - // advance to next row const next_rc = sqlite.sqlite3_step(stmt); try obj.set(ctx.allocator, "__has_row", .{ .bool = next_rc == sqlite.ROW }); @@ -1492,7 +1485,6 @@ fn getDefaultFetchMode(obj: *PhpObject) i64 { return 4; // FETCH_BOTH } -// helpers fn fetchRowAsObject(ctx: *NativeContext, stmt: *sqlite.Stmt) !Value { const obj = try ctx.vm.allocator.create(PhpObject); diff --git a/src/stdlib/pdo_mysql.zig b/src/stdlib/pdo_mysql.zig index 0808a09f..2b5a320f 100644 --- a/src/stdlib/pdo_mysql.zig +++ b/src/stdlib/pdo_mysql.zig @@ -183,7 +183,6 @@ pub fn stmtExecute(ctx: *NativeContext, obj: *PhpObject, args: []const Value) Ru sql = try interpolateParams(ctx, conn, sql, params, if (param_map_val == .array) param_map_val.array else null); } - // free previous result if any if (getRes(obj)) |old_res| { mysql.mysql_free_result(old_res); try obj.set(ctx.allocator, "__res_ptr", .{ .int = 0 }); @@ -232,7 +231,6 @@ fn interpolateParams(ctx: *NativeContext, conn: *mysql.MYSQL, sql: []const u8, p } } } else { - // positional params for (params.entries.items) |entry| { try positional.append(ctx.allocator, try valueToSqlString(ctx, conn, entry.value)); } @@ -269,7 +267,6 @@ fn valueToSqlString(ctx: *NativeContext, conn: *mysql.MYSQL, val: Value) ![]cons return try ctx.createString(s); }, .string => |s| { - // escape and quote const escaped = try ctx.allocator.alloc(u8, s.len * 2 + 3); escaped[0] = '\''; const elen = mysql.mysql_real_escape_string(conn, escaped[1..].ptr, s.ptr, @intCast(s.len)); diff --git a/src/stdlib/pdo_pgsql.zig b/src/stdlib/pdo_pgsql.zig index 5712df6d..a112e791 100644 --- a/src/stdlib/pdo_pgsql.zig +++ b/src/stdlib/pdo_pgsql.zig @@ -188,7 +188,6 @@ pub fn stmtExecute(ctx: *NativeContext, obj: *PhpObject, args: []const Value) Ru if (sql_val != .string) return .{ .bool = false }; const sql_z = try pdo.dupeZ(ctx, sql_val.string); - // free previous result if (getRes(obj)) |old_res| { pg.PQclear(old_res); try obj.set(ctx.allocator, "__res_ptr", .{ .int = 0 }); @@ -202,13 +201,11 @@ pub fn stmtExecute(ctx: *NativeContext, obj: *PhpObject, args: []const Value) Ru const param_map_val = obj.get("__param_map"); const param_map = if (param_map_val == .array) param_map_val.array else null; - // build param values array var param_values = try ctx.allocator.alloc(?[*:0]const u8, param_count); defer ctx.allocator.free(param_values); @memset(param_values, null); if (param_map) |pm| { - // named params for (params.entries.items) |entry| { var name = if (entry.key == .string) entry.key.string else continue; if (name.len > 0 and name[0] == ':') name = name[1..]; @@ -225,7 +222,6 @@ pub fn stmtExecute(ctx: *NativeContext, obj: *PhpObject, args: []const Value) Ru } } } else { - // positional params for (params.entries.items, 0..) |entry, idx| { if (idx >= param_count) break; if (entry.value == .null) { diff --git a/src/stdlib/phar.zig b/src/stdlib/phar.zig index 4ce11f6b..0f955bcc 100644 --- a/src/stdlib/phar.zig +++ b/src/stdlib/phar.zig @@ -46,7 +46,6 @@ pub const Phar = struct { } }; -// compression flag bits in entry.flags pub const COMPRESSION_MASK: u32 = 0x0000F000; pub const COMPRESSED_GZ: u32 = 0x00001000; pub const COMPRESSED_BZ2: u32 = 0x00002000; @@ -162,7 +161,6 @@ pub fn parse(a: Allocator, raw: []const u8) ParseError!Phar { .data_offset = data_pos, }); data_pos += er.compressed_size; - // synthesize parent directories var slash_search: usize = name_copy.len; while (slash_search > 0) { slash_search -= 1; @@ -256,11 +254,9 @@ pub fn write(a: Allocator, stub: []const u8, alias: []const u8, entries: []const try writeU32LE(a, &mbody, 0); // entry metadata length } - // emit manifest length + body try writeU32LE(a, &buf, @intCast(mbody.items.len)); try buf.appendSlice(a, mbody.items); - // emit file data for (blobs) |blob| try buf.appendSlice(a, blob); // signature: SHA1 over everything written so far (stub + manifest + data) diff --git a/src/stdlib/random.zig b/src/stdlib/random.zig index e0f455a8..bdcd2728 100644 --- a/src/stdlib/random.zig +++ b/src/stdlib/random.zig @@ -115,7 +115,6 @@ fn loadState(obj: *PhpObject, comptime T: type) ?T { return out; } -// ---- Mt19937 ---- fn mtConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; @@ -180,7 +179,6 @@ fn pcgGenerate(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .string = owned }; } -// ---- Xoshiro256** ---- fn xoshConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; @@ -222,7 +220,6 @@ fn secureGenerate(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .string = owned }; } -// ---- Randomizer dispatch ---- // returns the next u32 from whatever engine the Randomizer was constructed with fn engineNextU32(ctx: *NativeContext, eng: *PhpObject) u32 { @@ -244,7 +241,6 @@ fn engineNextU32(ctx: *NativeContext, eng: *PhpObject) u32 { storeState(ctx, eng, std.mem.asBytes(&p)) catch {}; return @as(u32, @truncate(v)); } - // Secure or unknown: csprng return @as(u32, @truncate(freshU64FromCrypto())); } @@ -298,7 +294,6 @@ fn rangeU32(ctx: *NativeContext, eng_opt: ?*PhpObject, umax: u32) u32 { return if (eng_opt) |e| engineNextU32(ctx, e) else @as(u32, @truncate(freshU64FromCrypto())); } const span: u64 = @as(u64, umax) + 1; - // power-of-two: simple mask if ((span & (span - 1)) == 0) { const r = if (eng_opt) |e| engineNextU32(ctx, e) else @as(u32, @truncate(freshU64FromCrypto())); return @as(u32, @intCast(@as(u64, r) & (span - 1))); diff --git a/src/stdlib/reflection.zig b/src/stdlib/reflection.zig index b94f76a6..589c8223 100644 --- a/src/stdlib/reflection.zig +++ b/src/stdlib/reflection.zig @@ -27,7 +27,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try rt_def.methods.put(a, "__toString", .{ .name = "__toString", .arity = 0 }); try vm.classes.put(a, "ReflectionType", rt_def); - // Attribute class with target constants var attr_def = ClassDef{ .name = "Attribute" }; try attr_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 1 }); try attr_def.static_props.put(a, "TARGET_CLASS", .{ .int = 1 }); @@ -62,18 +61,15 @@ pub fn register(vm: *VM, a: Allocator) !void { try spv_def.methods.put(a, "getValue", .{ .name = "getValue", .arity = 0 }); try vm.classes.put(a, "SensitiveParameterValue", spv_def); - // ReflectionException var exc_def = ClassDef{ .name = "ReflectionException" }; exc_def.parent = "Exception"; try vm.classes.put(a, "ReflectionException", exc_def); - // Reflection (utility class) var refl_def = ClassDef{ .name = "Reflection" }; try refl_def.methods.put(a, "getModifierNames", .{ .name = "getModifierNames", .arity = 1, .is_static = true }); try vm.classes.put(a, "Reflection", refl_def); try vm.native_fns.put(a, "Reflection::getModifierNames", reflectionGetModifierNames); - // ReflectionClass var rc_def = ClassDef{ .name = "ReflectionClass" }; try rc_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); try rc_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 1 }); @@ -203,7 +199,6 @@ pub fn register(vm: *VM, a: Allocator) !void { const rfa_def = ClassDef{ .name = "ReflectionFunctionAbstract", .is_abstract = true }; try vm.classes.put(a, "ReflectionFunctionAbstract", rfa_def); - // ReflectionMethod var rm_def = ClassDef{ .name = "ReflectionMethod", .parent = "ReflectionFunctionAbstract" }; try rm_def.static_props.put(a, "IS_STATIC", .{ .int = 16 }); try rm_def.static_props.put(a, "IS_PUBLIC", .{ .int = 1 }); @@ -296,7 +291,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionMethod::isUserDefined", rmIsUserDefined); try vm.native_fns.put(a, "ReflectionMethod::isDeprecated", reflectionFalse); - // ReflectionParameter var rp_def = ClassDef{ .name = "ReflectionParameter" }; try rp_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); try rp_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 2 }); @@ -340,7 +334,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionParameter::isDefaultValueConstant", rpIsDefaultValueConstant); try vm.native_fns.put(a, "ReflectionParameter::getDefaultValueConstantName", rpGetDefaultValueConstantName); - // ReflectionNamedType var rnt_def = ClassDef{ .name = "ReflectionNamedType", .parent = "ReflectionType" }; try rnt_def.properties.append(a, .{ .name = "type_name", .default = .{ .string = "" } }); try rnt_def.properties.append(a, .{ .name = "nullable", .default = .{ .bool = false } }); @@ -355,7 +348,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionNamedType::allowsNull", rntAllowsNull); try vm.native_fns.put(a, "ReflectionNamedType::__toString", rntToString); - // ReflectionUnionType var rut_def = ClassDef{ .name = "ReflectionUnionType", .parent = "ReflectionType" }; try rut_def.properties.append(a, .{ .name = "type_str", .default = .{ .string = "" } }); try rut_def.properties.append(a, .{ .name = "nullable", .default = .{ .bool = false } }); @@ -367,7 +359,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionUnionType::allowsNull", rutAllowsNull); try vm.native_fns.put(a, "ReflectionUnionType::__toString", rutToString); - // ReflectionIntersectionType var rit_def = ClassDef{ .name = "ReflectionIntersectionType", .parent = "ReflectionType" }; try rit_def.properties.append(a, .{ .name = "type_str", .default = .{ .string = "" } }); try rit_def.methods.put(a, "getTypes", .{ .name = "getTypes", .arity = 0 }); @@ -378,7 +369,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionIntersectionType::allowsNull", ritAllowsNull); try vm.native_fns.put(a, "ReflectionIntersectionType::__toString", ritToString); - // ReflectionEnum (extends ReflectionClass) var re_def = ClassDef{ .name = "ReflectionEnum" }; re_def.parent = "ReflectionClass"; try re_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); @@ -396,7 +386,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionEnum::getCase", reGetCase); try vm.native_fns.put(a, "ReflectionEnum::hasCase", reHasCase); - // ReflectionEnumUnitCase var reuc_def = ClassDef{ .name = "ReflectionEnumUnitCase" }; try reuc_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); try reuc_def.properties.append(a, .{ .name = "class", .default = .{ .string = "" } }); @@ -408,7 +397,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionEnumUnitCase::getName", reucGetName); try vm.native_fns.put(a, "ReflectionEnumUnitCase::getValue", reucGetValue); - // ReflectionEnumBackedCase (extends ReflectionEnumUnitCase) var rebc_def = ClassDef{ .name = "ReflectionEnumBackedCase" }; rebc_def.parent = "ReflectionEnumUnitCase"; try rebc_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); @@ -423,7 +411,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionEnumBackedCase::getValue", reucGetValue); try vm.native_fns.put(a, "ReflectionEnumBackedCase::getBackingValue", rebcGetBackingValue); - // ReflectionFunction var rf_def = ClassDef{ .name = "ReflectionFunction", .parent = "ReflectionFunctionAbstract" }; try rf_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); try rf_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 1 }); @@ -495,7 +482,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionFunction::isDeprecated", reflectionFalse); try vm.native_fns.put(a, "ReflectionFunction::isDisabled", reflectionFalse); - // ReflectionProperty var rprop_def = ClassDef{ .name = "ReflectionProperty" }; try rprop_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); try rprop_def.properties.append(a, .{ .name = "class", .default = .{ .string = "" } }); @@ -526,13 +512,9 @@ pub fn register(vm: *VM, a: Allocator) !void { try rprop_def.methods.put(a, "getAttributes", .{ .name = "getAttributes", .arity = 0 }); try rprop_def.methods.put(a, "getDocComment", .{ .name = "getDocComment", .arity = 0 }); try rprop_def.methods.put(a, "isVirtual", .{ .name = "isVirtual", .arity = 0 }); - // PHP 8.4 asymmetric visibility methods. zphp doesn't yet model - // separate set visibility but the symfony/property-access component - // probes for these unconditionally; return false until we implement - // `public(set)` / `protected(set)` / `private(set)` modifiers + // php8.4 asymmetric visibility methods try rprop_def.methods.put(a, "isPrivateSet", .{ .name = "isPrivateSet", .arity = 0 }); try rprop_def.methods.put(a, "isProtectedSet", .{ .name = "isProtectedSet", .arity = 0 }); - try rprop_def.methods.put(a, "isPublicSet", .{ .name = "isPublicSet", .arity = 0 }); try rprop_def.methods.put(a, "isFinal", .{ .name = "isFinal", .arity = 0 }); try rprop_def.methods.put(a, "isAbstract", .{ .name = "isAbstract", .arity = 0 }); try rprop_def.methods.put(a, "hasHooks", .{ .name = "hasHooks", .arity = 0 }); @@ -542,17 +524,25 @@ pub fn register(vm: *VM, a: Allocator) !void { try rprop_def.methods.put(a, "isLazy", .{ .name = "isLazy", .arity = 1 }); try rprop_def.methods.put(a, "skipLazyInitialization", .{ .name = "skipLazyInitialization", .arity = 1 }); try rprop_def.static_props.put(a, "IS_STATIC", .{ .int = 16 }); + try rprop_def.static_props.put(a, "IS_READONLY", .{ .int = 128 }); try rprop_def.static_props.put(a, "IS_PUBLIC", .{ .int = 1 }); try rprop_def.static_props.put(a, "IS_PROTECTED", .{ .int = 2 }); try rprop_def.static_props.put(a, "IS_PRIVATE", .{ .int = 4 }); - try rprop_def.static_props.put(a, "IS_READONLY", .{ .int = 128 }); + try rprop_def.static_props.put(a, "IS_ABSTRACT", .{ .int = 64 }); + try rprop_def.static_props.put(a, "IS_PROTECTED_SET", .{ .int = 2048 }); + try rprop_def.static_props.put(a, "IS_PRIVATE_SET", .{ .int = 4096 }); try rprop_def.static_props.put(a, "IS_VIRTUAL", .{ .int = 512 }); + try rprop_def.static_props.put(a, "IS_FINAL", .{ .int = 32 }); try rprop_def.constant_names.put(a, "IS_STATIC", {}); + try rprop_def.constant_names.put(a, "IS_READONLY", {}); try rprop_def.constant_names.put(a, "IS_PUBLIC", {}); try rprop_def.constant_names.put(a, "IS_PROTECTED", {}); try rprop_def.constant_names.put(a, "IS_PRIVATE", {}); - try rprop_def.constant_names.put(a, "IS_READONLY", {}); + try rprop_def.constant_names.put(a, "IS_ABSTRACT", {}); + try rprop_def.constant_names.put(a, "IS_PROTECTED_SET", {}); + try rprop_def.constant_names.put(a, "IS_PRIVATE_SET", {}); try rprop_def.constant_names.put(a, "IS_VIRTUAL", {}); + try rprop_def.constant_names.put(a, "IS_FINAL", {}); try vm.classes.put(a, "ReflectionProperty", rprop_def); try vm.native_fns.put(a, "ReflectionProperty::__construct", rpConstruct); @@ -567,11 +557,10 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionProperty::isPublic", rpropIsPublic); try vm.native_fns.put(a, "ReflectionProperty::isProtected", rpropIsProtected); try vm.native_fns.put(a, "ReflectionProperty::isPrivate", rpropIsPrivate); - try vm.native_fns.put(a, "ReflectionProperty::isPrivateSet", reflectionFalse); - try vm.native_fns.put(a, "ReflectionProperty::isProtectedSet", reflectionFalse); - try vm.native_fns.put(a, "ReflectionProperty::isPublicSet", reflectionFalse); - try vm.native_fns.put(a, "ReflectionProperty::isFinal", reflectionFalse); - try vm.native_fns.put(a, "ReflectionProperty::isAbstract", reflectionFalse); + try vm.native_fns.put(a, "ReflectionProperty::isPrivateSet", rpropIsPrivateSet); + try vm.native_fns.put(a, "ReflectionProperty::isProtectedSet", rpropIsProtectedSet); + try vm.native_fns.put(a, "ReflectionProperty::isFinal", rpropIsFinal); + try vm.native_fns.put(a, "ReflectionProperty::isAbstract", rpropIsAbstract); try vm.native_fns.put(a, "ReflectionProperty::hasHooks", reflectionFalse); try vm.native_fns.put(a, "ReflectionProperty::hasHook", reflectionFalse); try vm.native_fns.put(a, "ReflectionProperty::getHooks", rpropGetHooks); @@ -593,7 +582,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionProperty::getDocComment", rpropGetDocComment); try vm.native_fns.put(a, "ReflectionProperty::isVirtual", rpropIsVirtual); - // ReflectionAttribute var ra_def = ClassDef{ .name = "ReflectionAttribute" }; try ra_def.static_props.put(a, "IS_INSTANCEOF", .{ .int = 2 }); try ra_def.methods.put(a, "getName", .{ .name = "getName", .arity = 0 }); @@ -609,7 +597,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ReflectionAttribute::getTarget", raGetTarget); try vm.native_fns.put(a, "ReflectionAttribute::isRepeated", raIsRepeated); - // ReflectionClassConstant var rcc_def = ClassDef{ .name = "ReflectionClassConstant" }; try rcc_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); try rcc_def.properties.append(a, .{ .name = "class", .default = .{ .string = "" } }); @@ -654,9 +641,9 @@ pub fn register(vm: *VM, a: Allocator) !void { var rext_def = ClassDef{ .name = "ReflectionExtension" }; try rext_def.properties.append(a, .{ .name = "name", .default = .{ .string = "" } }); for ([_][]const u8{ - "__construct", "getName", "getVersion", "getFunctions", "getConstants", - "getINIEntries", "getClasses", "getClassNames", "getDependencies", - "info", "isPersistent", "isTemporary", "__toString", + "__construct", "getName", "getVersion", "getFunctions", "getConstants", + "getINIEntries", "getClasses", "getClassNames", "getDependencies", "info", + "isPersistent", "isTemporary", "__toString", }) |method| try rext_def.methods.put(a, method, .{ .name = method, .arity = if (std.mem.eql(u8, method, "__construct")) 1 else 0 }); try vm.classes.put(a, "ReflectionExtension", rext_def); try vm.native_fns.put(a, "ReflectionExtension::__construct", rextConstruct); @@ -779,8 +766,9 @@ fn throwReflection(ctx: *NativeContext, msg: []const u8) RuntimeError { fn isBuiltinType(name: []const u8) bool { const builtins = [_][]const u8{ - "int", "string", "bool", "float", "array", "callable", - "null", "void", "never", "mixed", "object", "iterable", "false", "true", + "int", "string", "bool", "float", "array", "callable", + "null", "void", "never", "mixed", "object", "iterable", + "false", "true", }; for (builtins) |b| { if (std.mem.eql(u8, name, b)) return true; @@ -925,20 +913,48 @@ fn buildPropertyObj(ctx: *NativeContext, class_name: []const u8, prop: ClassDef. return try buildPropertyObjStatic(ctx, class_name, prop, declaring_class, false); } -fn buildPropertyObjStatic(ctx: *NativeContext, class_name: []const u8, prop: ClassDef.PropertyDef, declaring_class: []const u8, is_static: bool) !*PhpObject { +fn buildPropertyObjStatic( + ctx: *NativeContext, + class_name: []const u8, + prop: ClassDef.PropertyDef, + declaring_class: []const u8, + is_static: bool, +) !*PhpObject { const obj = try ctx.createObject("ReflectionProperty"); + try obj.set(ctx.allocator, "name", .{ .string = prop.name }); try obj.set(ctx.allocator, "class", .{ .string = class_name }); try obj.set(ctx.allocator, "_visibility", .{ .int = @intFromEnum(prop.visibility) }); + // PHP's ReflectionProperty::hasDefaultValue() always returns false for - // constructor-promoted properties, regardless of whether the promoted param - // has a default in the constructor signature + // constructor-promoted properties, even if the promoted parameter has + // a default in the constructor signature. const effective_has_default = prop.has_default and !prop.is_promoted; + try obj.set(ctx.allocator, "_has_default", .{ .bool = effective_has_default }); - try obj.set(ctx.allocator, "_default_value", if (effective_has_default) prop.default else .null); + try obj.set( + ctx.allocator, + "_default_value", + if (effective_has_default) prop.default else .null, + ); + try obj.set(ctx.allocator, "_declaring_class", .{ .string = declaring_class }); + + try obj.set( + ctx.allocator, + "_set_visibility", + .{ .int = @intFromEnum(prop.set_visibility) }, + ); + try obj.set( + ctx.allocator, + "_has_set_visibility", + .{ .bool = prop.has_set_visibility }, + ); + try obj.set(ctx.allocator, "_is_readonly", .{ .bool = prop.is_readonly }); + try obj.set(ctx.allocator, "_is_final", .{ .bool = prop.is_final }); try obj.set(ctx.allocator, "_is_static", .{ .bool = is_static }); + return obj; } @@ -947,7 +963,10 @@ fn hasAbstractMethodInChain(vm: *VM, class_name: []const u8, method_name: []cons while (true) { const cls = vm.classes.get(current) orelse return false; if (cls.methods.get(method_name)) |_| return true; - if (cls.parent) |p| { current = p; continue; } + if (cls.parent) |p| { + current = p; + continue; + } return false; } } @@ -963,8 +982,6 @@ fn hasInterfaceMethod(vm: *VM, iface_name: []const u8, method_name: []const u8) return false; } -// --- ReflectionClass --- - fn rcConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return throwReflection(ctx, "ReflectionClass::__construct() expects a class name"); const raw_class_name = if (args[0] == .string) @@ -2138,9 +2155,6 @@ fn rcSetStaticPropertyValue(ctx: *NativeContext, args: []const Value) RuntimeErr return throwReflection(ctx, "Static property does not exist"); } - -// --- ReflectionMethod --- - fn rmConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return throwReflection(ctx, "ReflectionMethod::__construct() expects parameters"); const this = getThis(ctx) orelse return .null; @@ -2158,7 +2172,6 @@ fn rmConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return throwReflection(ctx, "ReflectionMethod::__construct() expects a class name or object"); } } else if (args[0] == .string) { - // "Class::method" string form const s = args[0].string; if (std.mem.indexOf(u8, s, "::")) |sep| { class_name = s[0..sep]; @@ -2169,7 +2182,6 @@ fn rmConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } else { return throwReflection(ctx, "ReflectionMethod::__construct() expects a class name or object"); } - // accept FQN with leading backslash if (class_name.len > 0 and class_name[0] == '\\') class_name = class_name[1..]; if (!ctx.vm.hasMethod(class_name, method_name)) { @@ -2360,8 +2372,6 @@ fn rmGetNumberOfRequiredParameters(ctx: *NativeContext, _: []const Value) Runtim return if (req == .int) req else .{ .int = 0 }; } -// --- ReflectionParameter --- - fn rpGetName(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; return this.get("name"); @@ -2403,7 +2413,6 @@ fn rpIsOptional(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .{ .bool = false }; const has_default = this.get("_has_default"); if (has_default == .bool and has_default.bool) return .{ .bool = true }; - // variadic params are always optional const is_var = this.get("_is_variadic"); return .{ .bool = is_var == .bool and is_var.bool }; } @@ -2522,8 +2531,6 @@ fn rpGetDeclaringFunction(ctx: *NativeContext, _: []const Value) RuntimeError!Va return .{ .object = obj }; } -// --- ReflectionNamedType --- - fn rntGetName(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; return this.get("type_name"); @@ -2562,8 +2569,6 @@ fn rntAllowsNull(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = false }; } -// --- ReflectionUnionType --- - fn rutGetTypes(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .{ .array = try ctx.createArray() }; const ts_v = this.get("type_str"); @@ -2597,8 +2602,6 @@ fn rutToString(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return ts_v; } -// --- ReflectionIntersectionType --- - fn ritGetTypes(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .{ .array = try ctx.createArray() }; const ts_v = this.get("type_str"); @@ -2623,8 +2626,6 @@ fn ritToString(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return ts_v; } -// --- ReflectionFunction --- - fn rfConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return throwReflection(ctx, "ReflectionFunction::__construct() expects a function name"); const this = getThis(ctx) orelse return .null; @@ -2911,8 +2912,6 @@ fn rfIsStatic(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = false }; } -// --- shared helpers --- - /// populate fields on a ReflectionParameter `this` object from a function + /// param index. shared between buildParamArray (constructing the full set for /// getParameters) and the public ReflectionParameter::__construct @@ -3040,12 +3039,10 @@ fn buildParamArray(ctx: *NativeContext, func: *const ObjFunction, type_key: []co for (func.params, 0..) |param_name, i| { const obj = try ctx.createObject("ReflectionParameter"); - // strip $ prefix const clean_name = if (param_name.len > 0 and param_name[0] == '$') param_name[1..] else param_name; try obj.set(ctx.allocator, "name", .{ .string = clean_name }); try obj.set(ctx.allocator, "_position", .{ .int = @intCast(i) }); - // type info if (type_info) |ti| { if (i < ti.param_types.len and ti.param_types[i].len > 0) { const raw_type = ti.param_types[i]; @@ -3065,7 +3062,6 @@ fn buildParamArray(ctx: *NativeContext, func: *const ObjFunction, type_key: []co try obj.set(ctx.allocator, "_nullable", .{ .bool = false }); } - // variadic const is_variadic = func.is_variadic and i == func.arity - 1; try obj.set(ctx.allocator, "_is_variadic", .{ .bool = is_variadic }); @@ -3081,7 +3077,6 @@ fn buildParamArray(ctx: *NativeContext, func: *const ObjFunction, type_key: []co } } - // by-reference const by_ref = if (i < func.ref_params.len) func.ref_params[i] else false; try obj.set(ctx.allocator, "_by_reference", .{ .bool = by_ref }); @@ -3105,8 +3100,6 @@ fn buildParamArray(ctx: *NativeContext, func: *const ObjFunction, type_key: []co return .{ .array = arr }; } -// --- Closure --- - fn closureBind(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return .null; const closure = args[0]; @@ -3205,34 +3198,111 @@ fn rrefGetId(_: *NativeContext, _: []const Value) RuntimeError!Value { return .null; } -// --- ReflectionProperty --- - fn rpConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { - if (args.len < 2) return throwReflection(ctx, "ReflectionProperty::__construct() expects class and property name"); + if (args.len < 2) { + return throwReflection( + ctx, + "ReflectionProperty::__construct() expects class and property name", + ); + } + const this = getThis(ctx) orelse return .null; - const raw_class = if (args[0] == .string) args[0].string else if (args[0] == .object) args[0].object.class_name else return throwReflection(ctx, "ReflectionProperty::__construct() expects a class name"); - const class_name = if (raw_class.len > 0 and raw_class[0] == '\\') raw_class[1..] else raw_class; - const prop_name = if (args[1] == .string) args[1].string else return throwReflection(ctx, "ReflectionProperty::__construct() expects a property name"); + const raw_class = if (args[0] == .string) + args[0].string + else if (args[0] == .object) + args[0].object.class_name + else + return throwReflection( + ctx, + "ReflectionProperty::__construct() expects a class name", + ); + + const class_name = if (raw_class.len > 0 and raw_class[0] == '\\') + raw_class[1..] + else + raw_class; + + const prop_name = if (args[1] == .string) + args[1].string + else + return throwReflection( + ctx, + "ReflectionProperty::__construct() expects a property name", + ); try this.set(ctx.allocator, "name", .{ .string = prop_name }); try this.set(ctx.allocator, "class", .{ .string = class_name }); if (findPropertyDef(ctx.vm, class_name, prop_name)) |result| { - try this.set(ctx.allocator, "_visibility", .{ .int = @intFromEnum(result.prop.visibility) }); - const effective_has_default = result.prop.has_default and !result.prop.is_promoted; - try this.set(ctx.allocator, "_has_default", .{ .bool = effective_has_default }); - try this.set(ctx.allocator, "_default_value", if (effective_has_default) result.prop.default else .null); - try this.set(ctx.allocator, "_declaring_class", .{ .string = result.declaring_class }); - try this.set(ctx.allocator, "_is_readonly", .{ .bool = result.prop.is_readonly }); + try this.set( + ctx.allocator, + "_visibility", + .{ .int = @intFromEnum(result.prop.visibility) }, + ); + + const effective_has_default = + result.prop.has_default and !result.prop.is_promoted; + + try this.set( + ctx.allocator, + "_has_default", + .{ .bool = effective_has_default }, + ); + + try this.set( + ctx.allocator, + "_default_value", + if (effective_has_default) result.prop.default else .null, + ); + + try this.set( + ctx.allocator, + "_declaring_class", + .{ .string = result.declaring_class }, + ); + + try this.set( + ctx.allocator, + "_set_visibility", + .{ .int = @intFromEnum(result.prop.set_visibility) }, + ); + + try this.set( + ctx.allocator, + "_has_set_visibility", + .{ .bool = result.prop.has_set_visibility }, + ); + + try this.set( + ctx.allocator, + "_is_readonly", + .{ .bool = result.prop.is_readonly }, + ); + + try this.set( + ctx.allocator, + "_is_final", + .{ .bool = result.prop.is_final }, + ); + + try this.set( + ctx.allocator, + "_is_static", + .{ .bool = result.is_static }, + ); } else { - // PHP throws when the property is not declared on the class (or any - // ancestor) - a non-existent / dynamic-only property has no - // ReflectionProperty. matches "Property C::$x does not exist" - const msg = try std.fmt.allocPrint(ctx.allocator, "Property {s}::${s} does not exist", .{ class_name, prop_name }); + const msg = try std.fmt.allocPrint( + ctx.allocator, + "Property {s}::${s} does not exist", + .{ class_name, prop_name }, + ); + try ctx.vm.strings.append(ctx.allocator, msg); + return throwReflection(ctx, msg); } + return .null; } @@ -3364,6 +3434,57 @@ fn rpropIsPrivate(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = vis == .int and vis.int == 2 }; } +fn rpropIsPrivateSet(ctx: *NativeContext, _: []const Value) RuntimeError!Value { + const this = getThis(ctx) orelse return .{ .bool = false }; + const vis = this.get("_visibility"); + const set_vis = this.get("_set_visibility"); + const vis_val = if (vis == .int) vis.int else 0; + const set_vis_val = if (set_vis == .int) set_vis.int else vis_val; + return .{ .bool = set_vis_val == 2 and vis_val != 2 }; +} + +fn rpropIsProtectedSet(ctx: *NativeContext, _: []const Value) RuntimeError!Value { + const this = getThis(ctx) orelse return .{ .bool = false }; + const vis = this.get("_visibility"); + const set_vis = this.get("_set_visibility"); + const has_set_vis = this.get("_has_set_visibility"); + const ro = this.get("_is_readonly"); + const is_ro = ro == .bool and ro.bool; + const has_explicit_set = has_set_vis == .bool and has_set_vis.bool; + const vis_val = if (vis == .int) vis.int else 0; + const set_vis_val = if (set_vis == .int) set_vis.int else vis_val; + + return .{ + .bool = (set_vis_val == 1 and vis_val == 0) or + (is_ro and vis_val == 0 and !has_explicit_set), + }; +} +fn rpropIsFinal(ctx: *NativeContext, _: []const Value) RuntimeError!Value { + const this = getThis(ctx) orelse return .{ .bool = false }; + + const final_v = this.get("_is_final"); + const is_explicit_final = final_v == .bool and final_v.bool; + if (is_explicit_final) return .{ .bool = true }; + + const vis = this.get("_visibility"); + const set_vis = this.get("_set_visibility"); + const has_set_vis = this.get("_has_set_visibility"); + const vis_val = if (vis == .int) vis.int else 0; + const set_vis_val = if (set_vis == .int) set_vis.int else vis_val; + const has_explicit_set = has_set_vis == .bool and has_set_vis.bool; + + // PHP 8.4 only treats asymmetric private(set) as implicitly final. + // Symmetric `private private(set)` and private readonly remain non-final. + const implicit_private_set_final = + has_explicit_set and set_vis_val == 2 and vis_val != 2; + + return .{ .bool = implicit_private_set_final }; +} + +fn rpropIsAbstract(_: *NativeContext, _: []const Value) RuntimeError!Value { + return .{ .bool = false }; +} + fn rpropGetDefaultValue(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; const has_default = this.get("_has_default"); @@ -3443,15 +3564,42 @@ fn rpropHasType(ctx: *NativeContext, _: []const Value) RuntimeError!Value { fn rpropGetModifiers(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .{ .int = 0 }; const vis = this.get("_visibility"); - var mods: i64 = switch (if (vis == .int) vis.int else -1) { - 1 => 2, // protected - 2 => 4, // private - else => 1, // public (0) / fallback + const vis_val = if (vis == .int) vis.int else 0; + + var mods: i64 = switch (vis_val) { + 1 => 2, // IS_PROTECTED + 2 => 4, // IS_PRIVATE + else => 1, // IS_PUBLIC }; - // a readonly property carries IS_READONLY (128) plus an internal readonly - // flag (2048); php 8.4 and 8.5 both report 2177 for a public readonly prop + + const st = this.get("_is_static"); + if (st == .bool and st.bool) mods |= 16; + const ro = this.get("_is_readonly"); - if (ro == .bool and ro.bool) mods |= 128 | 2048; + const is_ro = ro == .bool and ro.bool; + if (is_ro) mods |= 128; + + const set_vis = this.get("_set_visibility"); + const has_set_vis = this.get("_has_set_visibility"); + const set_vis_val = if (set_vis == .int) set_vis.int else vis_val; + const has_explicit_set = has_set_vis == .bool and has_set_vis.bool; + + const final_v = this.get("_is_final"); + const is_explicit_final = final_v == .bool and final_v.bool; + // Match PHP 8.4: only asymmetric private(set) contributes IS_FINAL. + const implicit_private_set_final = + has_explicit_set and set_vis_val == 2 and vis_val != 2; + if (is_explicit_final or implicit_private_set_final) mods |= 32; + + const is_asym_private_set = + has_explicit_set and set_vis_val == 2 and vis_val != 2; + if (is_asym_private_set) mods |= 4096; + + const is_asym_prot_set = + (has_explicit_set and set_vis_val == 1 and vis_val == 0) or + (is_ro and vis_val == 0 and !has_explicit_set); + if (is_asym_prot_set) mods |= 2048; + return .{ .int = mods }; } @@ -3491,8 +3639,6 @@ fn rpropIsVirtual(_: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = false }; } -// --- ReflectionMethod::invoke --- - fn rmInvoke(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; const method_name = if (this.get("name") == .string) this.get("name").string else return .null; @@ -3524,7 +3670,6 @@ fn rmInvokeArgs(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (target == .object) { return ctx.callMethod(target.object, method_name, call_args[0..count]) catch .null; } - // static call: null target const declaring = if (this.get("_declaring_class") == .string) this.get("_declaring_class").string else return .null; var buf: [256]u8 = undefined; const full = std.fmt.bufPrint(&buf, "{s}::{s}", .{ declaring, method_name }) catch return .null; @@ -3661,8 +3806,6 @@ fn rpGetClass(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .null; } -// --- ReflectionAttribute --- - fn attributeConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const this = getThis(ctx) orelse return .null; const flags = if (args.len > 0 and args[0] == .int) args[0] else Value{ .int = 127 }; @@ -3738,7 +3881,6 @@ fn raNewInstance(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return error.RuntimeError; } - // target enforcement const target_val = this.get("_target"); if (target_val == .int) { const target = target_val.int; @@ -3761,7 +3903,6 @@ fn raNewInstance(ctx: *NativeContext, _: []const Value) RuntimeError!Value { } } - // repeatability enforcement const is_repeated_val = this.get("_is_repeated"); if (is_repeated_val == .bool and is_repeated_val.bool) { const flags = getAttributeFlags(ctx.vm, attr_name); @@ -3787,7 +3928,10 @@ fn raNewInstance(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const arr = args_val.array; var has_named = false; for (arr.entries.items) |entry| { - if (entry.key == .string) { has_named = true; break; } + if (entry.key == .string) { + has_named = true; + break; + } } if (has_named) { @@ -3846,8 +3990,6 @@ fn raIsRepeated(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return if (repeated == .bool) repeated else .{ .bool = false }; } -// --- ReflectionEnum --- - fn reConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return throwReflection(ctx, "ReflectionEnum::__construct() expects an enum name"); const raw = if (args[0] == .string) @@ -3978,8 +4120,6 @@ fn rebcGetBackingValue(ctx: *NativeContext, _: []const Value) RuntimeError!Value return case_obj_v.object.get("value"); } -// ---------------- ReflectionGenerator ---------------- - fn rgConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .generator) return throwReflection(ctx, "ReflectionGenerator::__construct expects a Generator"); const obj = getThis(ctx) orelse return .null; @@ -4052,8 +4192,6 @@ fn rgGetTrace(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .array = arr }; } -// ---------------- ReflectionFiber ---------------- - fn rfibConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .fiber) return throwReflection(ctx, "ReflectionFiber::__construct expects a Fiber"); const obj = getThis(ctx) orelse return .null; diff --git a/src/stdlib/serialize.zig b/src/stdlib/serialize.zig index 4ab0e0e2..945353be 100644 --- a/src/stdlib/serialize.zig +++ b/src/stdlib/serialize.zig @@ -754,7 +754,6 @@ fn unserializeValue(ctx: *NativeContext, uctx: *UnserCtx, s: []const u8, pos: us return .{ .value = .{ .object = obj }, .pos = p }; }, 'C' => { - // Serializable interface payload: C::""::{} if (pos + 2 >= s.len or s[pos + 1] != ':') return error.RuntimeError; const colon1 = std.mem.indexOfPos(u8, s, pos + 2, ":") orelse return error.RuntimeError; const name_len = std.fmt.parseInt(usize, s[pos + 2 .. colon1], 10) catch return error.RuntimeError; @@ -802,7 +801,6 @@ fn unserializeValue(ctx: *NativeContext, uctx: *UnserCtx, s: []const u8, pos: us const StringResult = struct { str: []const u8, pos: usize }; fn parseString(s: []const u8, pos: usize) !StringResult { - // s:LEN:"..."; if (pos + 2 >= s.len or s[pos + 1] != ':') return error.RuntimeError; const len_start = pos + 2; const colon = std.mem.indexOfPos(u8, s, len_start, ":") orelse return error.RuntimeError; @@ -815,4 +813,3 @@ fn parseString(s: []const u8, pos: usize) !StringResult { if (end + 1 >= s.len or s[end] != '"' or s[end + 1] != ';') return error.RuntimeError; return .{ .str = str, .pos = end + 2 }; } - diff --git a/src/stdlib/simplexml.zig b/src/stdlib/simplexml.zig index 7fe2bf47..2293cb68 100644 --- a/src/stdlib/simplexml.zig +++ b/src/stdlib/simplexml.zig @@ -255,7 +255,6 @@ fn isSequentialList(arr: *const PhpArray) bool { return true; } -// ---------------- top-level functions ---------------- fn sxmlLoadString(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .string) return .{ .bool = false }; @@ -304,7 +303,6 @@ fn sxmlImportDom(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .object = wrapper }; } -// ---------------- SimpleXMLElement::__construct ---------------- fn sxmlConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .string) return .null; @@ -333,7 +331,6 @@ fn sxmlConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// ---------------- methods ---------------- fn sxmlGetName(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -773,7 +770,6 @@ fn sxmlOffsetGet(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } return .null; } - // string offset: attribute access if (args[0] == .string) { const attr_z = try dupZ(ctx, args[0].string); if (c.xmlHasProp(node, @ptrCast(attr_z.ptr)) == null) return .null; @@ -966,7 +962,6 @@ fn sxmlGetIterator(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .object = iter_obj }; } -// ---------------- registration ---------------- pub fn register(vm: *VM, a: Allocator) !void { var def = ClassDef{ .name = "SimpleXMLElement" }; diff --git a/src/stdlib/soap.zig b/src/stdlib/soap.zig index 1f09e680..4cde8c6b 100644 --- a/src/stdlib/soap.zig +++ b/src/stdlib/soap.zig @@ -16,7 +16,6 @@ const c = @cImport({ }); pub fn register(vm: *VM, a: Allocator) !void { - // SoapClient { var def = ClassDef{ .name = "SoapClient" }; try def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 2 }); @@ -48,7 +47,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SoapClient::__getCookies", soapClientGetCookies); } - // SoapServer { var def = ClassDef{ .name = "SoapServer" }; try def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 2 }); @@ -72,7 +70,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SoapServer::setPersistence", soapServerSetPersistence); } - // SoapHeader { var def = ClassDef{ .name = "SoapHeader" }; try def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 5 }); @@ -80,7 +77,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SoapHeader::__construct", soapHeaderConstruct); } - // SoapVar { var def = ClassDef{ .name = "SoapVar" }; try def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 5 }); @@ -88,7 +84,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SoapVar::__construct", soapVarConstruct); } - // SoapParam { var def = ClassDef{ .name = "SoapParam" }; try def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 2 }); @@ -96,7 +91,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SoapParam::__construct", soapParamConstruct); } - // SoapFault - extends Exception { var def = ClassDef{ .name = "SoapFault" }; def.parent = "Exception"; @@ -511,7 +505,6 @@ fn parseSoapResponse(ctx: *NativeContext, xml: []const u8) RuntimeError!Value { const body = xml[body_start.?..(body_end orelse xml.len)]; - // check for Fault if (std.mem.indexOf(u8, body, "Fault") != null) { const code = extractBetween(body, "", "") orelse "Server"; const str = extractBetween(body, "", "") orelse "SOAP fault"; @@ -532,7 +525,6 @@ fn parseSoapResponse(ctx: *NativeContext, xml: []const u8) RuntimeError!Value { try ctx.strings.append(ctx.allocator, owned); return .{ .string = owned }; } - // skip past response element tag const tag_close = std.mem.indexOfScalarPos(u8, body, p, '>') orelse return .null; const inner_start = tag_close + 1; // find matching close - approximate by looking for @@ -565,7 +557,6 @@ fn parseSoapResponse(ctx: *NativeContext, xml: []const u8) RuntimeError!Value { const child_inner_end = std.mem.indexOfPos(u8, inner, child_inner_start, child_close_tag) orelse return .null; const text = inner[child_inner_start..child_inner_end]; - // try to coerce numeric if (text.len > 0) { if (std.fmt.parseInt(i64, text, 10)) |n| return .{ .int = n } else |_| {} if (std.fmt.parseFloat(f64, text)) |f| return .{ .float = f } else |_| {} diff --git a/src/stdlib/sodium.zig b/src/stdlib/sodium.zig index cd54a7b6..a9df8218 100644 --- a/src/stdlib/sodium.zig +++ b/src/stdlib/sodium.zig @@ -127,7 +127,6 @@ fn native_bin2base64(ctx: *NativeContext, args: []const Value) RuntimeError!Valu const out = try ctx.allocator.alloc(u8, out_max); defer ctx.allocator.free(out); _ = c.sodium_bin2base64(out.ptr, out.len, bin.ptr, bin.len, variant); - // result is NUL-terminated; trim var end = out.len; while (end > 0 and out[end - 1] == 0) end -= 1; return try allocStr(ctx, out[0..end]); diff --git a/src/stdlib/spl.zig b/src/stdlib/spl.zig index 629cfd5a..f3fd8720 100644 --- a/src/stdlib/spl.zig +++ b/src/stdlib/spl.zig @@ -28,12 +28,10 @@ pub fn register(vm: *VM, a: Allocator) !void { const std_def = ClassDef{ .name = "stdClass" }; try vm.classes.put(a, "stdClass", std_def); - // Countable interface var countable = vm_mod.InterfaceDef{ .name = "Countable" }; try countable.methods.append(a, "count"); try vm.interfaces.put(a, "Countable", countable); - // ArrayAccess interface var array_access = vm_mod.InterfaceDef{ .name = "ArrayAccess" }; try array_access.methods.append(a, "offsetGet"); try array_access.methods.append(a, "offsetSet"); @@ -55,7 +53,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try backed_enum.methods.append(a, "tryFrom"); try vm.interfaces.put(a, "BackedEnum", backed_enum); - // Iterator interface var iterator = vm_mod.InterfaceDef{ .name = "Iterator" }; iterator.parent = "Traversable"; try iterator.methods.append(a, "current"); @@ -65,18 +62,15 @@ pub fn register(vm: *VM, a: Allocator) !void { try iterator.methods.append(a, "valid"); try vm.interfaces.put(a, "Iterator", iterator); - // IteratorAggregate interface var iter_agg = vm_mod.InterfaceDef{ .name = "IteratorAggregate" }; iter_agg.parent = "Traversable"; try iter_agg.methods.append(a, "getIterator"); try vm.interfaces.put(a, "IteratorAggregate", iter_agg); - // JsonSerializable interface var json_ser = vm_mod.InterfaceDef{ .name = "JsonSerializable" }; try json_ser.methods.append(a, "jsonSerialize"); try vm.interfaces.put(a, "JsonSerializable", json_ser); - // Stringable interface var stringable = vm_mod.InterfaceDef{ .name = "Stringable" }; try stringable.methods.append(a, "__toString"); try vm.interfaces.put(a, "Stringable", stringable); @@ -109,7 +103,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try serializable.methods.append(a, "unserialize"); try vm.interfaces.put(a, "Serializable", serializable); - // SplStack var stack_def = ClassDef{ .name = "SplStack", .parent = "SplDoublyLinkedList" }; try stack_def.interfaces.append(a, "Countable"); try stack_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 0 }); @@ -154,7 +147,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplStack::valid", stackValid); try vm.native_fns.put(a, "SplStack::toArray", stackToArray); - // ArrayObject var ao_def = ClassDef{ .name = "ArrayObject" }; // matches PHP's interface order on ArrayObject so getInterfaceNames() lists // them the way callers expect @@ -217,7 +209,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "ArrayObject::__isset", aoMagicIsset); try vm.native_fns.put(a, "ArrayObject::__unset", aoMagicUnset); - // ArrayIterator var ai_def = ClassDef{ .name = "ArrayIterator" }; try ai_def.interfaces.append(a, "Iterator"); try ai_def.interfaces.append(a, "Countable"); @@ -288,7 +279,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "WeakMap::offsetUnset", wmOffsetUnset); try vm.native_fns.put(a, "WeakMap::count", wmCount); - // SplPriorityQueue var pq_def = ClassDef{ .name = "SplPriorityQueue" }; try pq_def.interfaces.append(a, "Countable"); try pq_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 0 }); @@ -349,7 +339,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplHeap::rewind", heapRewind); try vm.native_fns.put(a, "SplHeap::valid", heapValid); - // SplMinHeap var minh_def = ClassDef{ .name = "SplMinHeap", .parent = "SplHeap" }; try minh_def.interfaces.append(a, "Countable"); try minh_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 0 }); @@ -377,7 +366,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplMinHeap::rewind", heapRewind); try vm.native_fns.put(a, "SplMinHeap::valid", heapValid); - // SplMaxHeap var maxh_def = ClassDef{ .name = "SplMaxHeap", .parent = "SplHeap" }; try maxh_def.interfaces.append(a, "Countable"); try maxh_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 0 }); @@ -405,7 +393,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplMaxHeap::rewind", heapRewind); try vm.native_fns.put(a, "SplMaxHeap::valid", heapValid); - // SplFixedArray var fa_def = ClassDef{ .name = "SplFixedArray" }; try fa_def.interfaces.append(a, "Traversable"); try fa_def.interfaces.append(a, "Countable"); @@ -443,7 +430,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplFixedArray::rewind", faRewind); try vm.native_fns.put(a, "SplFixedArray::valid", faValid); - // SplQueue var sq_def = ClassDef{ .name = "SplQueue", .parent = "SplDoublyLinkedList" }; try sq_def.interfaces.append(a, "Countable"); try sq_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 0 }); @@ -473,7 +459,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplQueue::rewind", sqRewind); try vm.native_fns.put(a, "SplQueue::valid", sqValid); - // SplDoublyLinkedList var dll_def = ClassDef{ .name = "SplDoublyLinkedList" }; try dll_def.interfaces.append(a, "Iterator"); try dll_def.interfaces.append(a, "Countable"); @@ -531,7 +516,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplDoublyLinkedList::add", dllAdd); try vm.native_fns.put(a, "SplDoublyLinkedList::toArray", dllToArray); - // SplObjectStorage var sos_def = ClassDef{ .name = "SplObjectStorage" }; try sos_def.interfaces.append(a, "Countable"); try sos_def.interfaces.append(a, "Iterator"); @@ -658,9 +642,7 @@ fn wmiValid(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = cursor >= 0 and cursor < @as(i64, @intCast(objs_v.array.entries.items.len)) }; } -// ========================================================= // WeakReference -// ========================================================= fn weakRefCreate(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .object) return .null; @@ -681,9 +663,7 @@ fn weakRefConstruct(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return error.RuntimeError; } -// ========================================================= // WeakMap -// ========================================================= fn weakMapGetIterator(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -719,7 +699,6 @@ fn ensureData(ctx: *NativeContext, obj: *PhpObject) !*PhpArray { return arr; } -// --- SplStack --- fn stackConstruct(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -869,7 +848,6 @@ fn stackToArray(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .array = copy }; } -// --- ArrayObject --- fn aoConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1135,7 +1113,6 @@ fn aoGetFlags(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .int = Value.toInt(obj.get("__flags")) }; } -// --- ArrayIterator --- fn aiConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1283,7 +1260,6 @@ fn aiSetFlags(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// --- WeakMap --- fn wmObjKey(arg: Value) ?i64 { if (arg == .object) return @intCast(@intFromPtr(arg.object)); @@ -1719,7 +1695,6 @@ fn heapValid(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = arr.entries.items.len > 0 }; } -// --- SplFixedArray --- fn faConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1943,7 +1918,6 @@ fn faValid(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = cursor >= 0 and cursor < @as(i64, @intCast(arr.entries.items.len)) }; } -// --- SplQueue --- fn sqConstruct(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -2041,7 +2015,6 @@ fn sqValid(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = cursor >= 0 and cursor < @as(i64, @intCast(arr.entries.items.len)) }; } -// --- SplDoublyLinkedList --- const DLL_IT_MODE_LIFO: i64 = 2; const DLL_IT_MODE_FIFO: i64 = 0; diff --git a/src/stdlib/spl_iterators.zig b/src/stdlib/spl_iterators.zig index f41d5027..376eadde 100644 --- a/src/stdlib/spl_iterators.zig +++ b/src/stdlib/spl_iterators.zig @@ -11,7 +11,6 @@ const Allocator = std.mem.Allocator; const RuntimeError = error{ RuntimeError, OutOfMemory }; pub fn register(vm: *VM, a: Allocator) !void { - // RecursiveIterator interface var rec_iter = vm_mod.InterfaceDef{ .name = "RecursiveIterator" }; rec_iter.parent = "Iterator"; try rec_iter.methods.append(a, "hasChildren"); @@ -35,7 +34,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "GeneratorWrapper::next", gwNext); try vm.native_fns.put(a, "GeneratorWrapper::valid", gwValid); - // SplFileInfo var fi_def = ClassDef{ .name = "SplFileInfo" }; for ([_][]const u8{ "__construct", "getFilename", "getExtension", "getBasename", @@ -69,7 +67,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "SplFileInfo::__toString", fiToString); try vm.native_fns.put(a, "SplFileInfo::openFile", fiOpenFile); - // DirectoryIterator var di_def = ClassDef{ .name = "DirectoryIterator" }; di_def.parent = "SplFileInfo"; try di_def.interfaces.append(a, "Iterator"); @@ -115,7 +112,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "FilesystemIterator::next", diNext); try vm.native_fns.put(a, "FilesystemIterator::valid", diValid); - // RecursiveDirectoryIterator var rdi_def = ClassDef{ .name = "RecursiveDirectoryIterator" }; rdi_def.parent = "DirectoryIterator"; try rdi_def.interfaces.append(a, "RecursiveIterator"); @@ -166,7 +162,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "FilterIterator::getFilename", filterGetFilename); try vm.native_fns.put(a, "FilterIterator::isDir", filterIsDir); - // RecursiveIteratorIterator var rii_def = ClassDef{ .name = "RecursiveIteratorIterator" }; try rii_def.interfaces.append(a, "Iterator"); for ([_][]const u8{ @@ -211,7 +206,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "IteratorIterator::next", iiNext); try vm.native_fns.put(a, "IteratorIterator::getInnerIterator", iiGetInner); - // EmptyIterator var empty_def = ClassDef{ .name = "EmptyIterator" }; try empty_def.interfaces.append(a, "Iterator"); for ([_][]const u8{ "rewind", "valid", "current", "key", "next" }) |m| { @@ -224,7 +218,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "EmptyIterator::key", emptyCurrent); try vm.native_fns.put(a, "EmptyIterator::next", emptyNoop); - // LimitIterator (extends IteratorIterator behaviorally) var limit_def = ClassDef{ .name = "LimitIterator" }; limit_def.parent = "IteratorIterator"; for ([_][]const u8{ "__construct", "rewind", "valid", "current", "key", "next", "getPosition", "seek" }) |m| { @@ -241,7 +234,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "LimitIterator::getPosition", limitGetPosition); try vm.native_fns.put(a, "LimitIterator::seek", limitSeek); - // NoRewindIterator var nri_def = ClassDef{ .name = "NoRewindIterator" }; nri_def.parent = "IteratorIterator"; for ([_][]const u8{ "__construct", "rewind", "valid", "current", "key", "next" }) |m| { @@ -251,7 +243,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.classes.put(a, "NoRewindIterator", nri_def); try vm.native_fns.put(a, "NoRewindIterator::rewind", emptyNoop); - // InfiniteIterator var inf_def = ClassDef{ .name = "InfiniteIterator" }; inf_def.parent = "IteratorIterator"; for ([_][]const u8{ "__construct", "rewind", "valid", "current", "key", "next" }) |m| { @@ -261,7 +252,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.classes.put(a, "InfiniteIterator", inf_def); try vm.native_fns.put(a, "InfiniteIterator::next", infiniteNext); - // AppendIterator var app_def = ClassDef{ .name = "AppendIterator" }; try app_def.interfaces.append(a, "Iterator"); for ([_][]const u8{ "__construct", "append", "rewind", "valid", "current", "key", "next", "getInnerIterator", "getIteratorIndex", "getArrayIterator" }) |m| { @@ -280,7 +270,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "AppendIterator::getIteratorIndex", appGetIndex); try vm.native_fns.put(a, "AppendIterator::getArrayIterator", appGetArrayIterator); - // CallbackFilterIterator var cbf_def = ClassDef{ .name = "CallbackFilterIterator" }; cbf_def.parent = "FilterIterator"; try cbf_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 2 }); @@ -289,7 +278,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "CallbackFilterIterator::__construct", cbfConstruct); try vm.native_fns.put(a, "CallbackFilterIterator::accept", cbfAccept); - // RegexIterator var rx_def = ClassDef{ .name = "RegexIterator" }; rx_def.parent = "FilterIterator"; for ([_][]const u8{ "__construct", "accept", "current", "getRegex", "getMode", "setMode", "getFlags", "setFlags", "getPregFlags", "setPregFlags" }) |m| { @@ -315,7 +303,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "RegexIterator::getPregFlags", rxGetPregFlags); try vm.native_fns.put(a, "RegexIterator::setPregFlags", rxSetPregFlags); - // CachingIterator var ci_def = ClassDef{ .name = "CachingIterator" }; ci_def.parent = "IteratorIterator"; for ([_][]const u8{ "__construct", "rewind", "valid", "current", "key", "next", "hasNext", "__toString", "getCache", "getFlags", "setFlags" }) |m| { @@ -341,7 +328,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "CachingIterator::getFlags", ciGetFlags); try vm.native_fns.put(a, "CachingIterator::setFlags", ciSetFlags); - // MultipleIterator var mi_def = ClassDef{ .name = "MultipleIterator" }; try mi_def.interfaces.append(a, "Iterator"); for ([_][]const u8{ "__construct", "attachIterator", "detachIterator", "containsIterator", "countIterators", "rewind", "valid", "current", "key", "next", "getFlags", "setFlags" }) |m| { @@ -377,7 +363,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "RecursiveFilterIterator::hasChildren", rfiHasChildren); try vm.native_fns.put(a, "RecursiveFilterIterator::getChildren", rfiGetChildren); - // RecursiveCallbackFilterIterator var rcbf_def = ClassDef{ .name = "RecursiveCallbackFilterIterator" }; rcbf_def.parent = "RecursiveFilterIterator"; try rcbf_def.methods.put(a, "__construct", .{ .name = "__construct", .arity = 2 }); @@ -389,7 +374,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "RecursiveCallbackFilterIterator::accept", cbfAccept); try vm.native_fns.put(a, "RecursiveCallbackFilterIterator::getChildren", rcbfGetChildren); - // RecursiveRegexIterator var rrx_def = ClassDef{ .name = "RecursiveRegexIterator" }; rrx_def.parent = "RegexIterator"; try rrx_def.interfaces.append(a, "RecursiveIterator"); @@ -400,7 +384,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "RecursiveRegexIterator::hasChildren", rfiHasChildren); try vm.native_fns.put(a, "RecursiveRegexIterator::getChildren", rrxGetChildren); - // RecursiveArrayIterator (extends ArrayIterator, adds hasChildren/getChildren) var rai_def = ClassDef{ .name = "RecursiveArrayIterator" }; rai_def.parent = "ArrayIterator"; try rai_def.interfaces.append(a, "RecursiveIterator"); @@ -410,7 +393,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "RecursiveArrayIterator::hasChildren", raiHasChildren); try vm.native_fns.put(a, "RecursiveArrayIterator::getChildren", raiGetChildren); - // RecursiveTreeIterator var rti_def = ClassDef{ .name = "RecursiveTreeIterator" }; rti_def.parent = "RecursiveIteratorIterator"; for ([_][]const u8{ "__construct", "current", "key", "getPrefix", "setPrefixPart", "getEntry", "getPostfix" }) |m| { @@ -451,9 +433,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try vm.native_fns.put(a, "GlobIterator::count", giCount); } -// ========================================== -// helpers -// ========================================== fn getThis(ctx: *NativeContext) ?*PhpObject { const v = ctx.vm.currentFrame().vars.get("$this") orelse return null; @@ -513,9 +492,7 @@ fn statPath(path: []const u8) ?std.fs.File.Stat { return file.stat() catch null; } -// ========================================== // SplFileInfo -// ========================================== fn fiConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -651,9 +628,7 @@ fn fiOpenFile(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .object = obj }; } -// ========================================== // DirectoryIterator -// ========================================== fn diConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -731,7 +706,6 @@ fn fsiConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const path = args[0].string; try obj.set(ctx.allocator, "__di_path", .{ .string = try createString(ctx, path) }); try obj.set(ctx.allocator, "__di_idx", .{ .int = 0 }); - // FilesystemIterator skips dots by default const entries = try loadDirectoryEntries(ctx, path, SKIP_DOTS); try obj.set(ctx.allocator, "__di_entries", .{ .array = entries }); try syncCurrentEntry(ctx, obj); @@ -787,9 +761,7 @@ fn diIsDot(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = std.mem.eql(u8, name.string, ".") or std.mem.eql(u8, name.string, "..") }; } -// ========================================== // RecursiveDirectoryIterator -// ========================================== fn rdiConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -907,9 +879,7 @@ fn rdiValid(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return diValid(ctx, args); } -// ========================================== // FilterIterator -// ========================================== /// wrap a value into an iterator-protocol object. generators get wrapped in /// GeneratorWrapper so the rest of the iterator infra can call rewind/current @@ -987,7 +957,6 @@ fn filterRewind(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; const inner = filterGetInnerIterator(obj) orelse return .null; _ = try ctx.vm.callMethod(inner, "rewind", &.{}); - // advance to first accepted element try filterAdvanceToAccepted(ctx, obj, inner); return .null; } @@ -1050,9 +1019,7 @@ fn filterIsDir(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return if (current == .object) ctx.vm.callMethod(current.object, "isDir", &.{}) else .{ .bool = false }; } -// ========================================== // RecursiveIteratorIterator -// ========================================== // stores a stack of iterators to flatten recursive iteration // mode: 0=LEAVES_ONLY, 1=SELF_FIRST, 2=CHILD_FIRST @@ -1135,7 +1102,6 @@ fn riiDescend(ctx: *NativeContext, obj: *PhpObject) !void { const has_children = ctx.vm.callMethod(iter_obj, "hasChildren", &.{}) catch Value{ .bool = false }; if (!has_children.isTruthy()) { if (mode == 1) { - // SELF_FIRST: already yielding this item } return; } @@ -1286,9 +1252,7 @@ fn riiGetSubIterator(ctx: *NativeContext, args: []const Value) RuntimeError!Valu return stack.get(.{ .int = @intCast(depth) }); } -// ========================================== // IteratorIterator -// ========================================== fn iiConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1351,9 +1315,7 @@ fn iiGetInner(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .object = inner }; } -// ========================================== // EmptyIterator -// ========================================== fn emptyNoop(_: *NativeContext, _: []const Value) RuntimeError!Value { return .null; @@ -1367,9 +1329,7 @@ fn emptyCurrent(_: *NativeContext, _: []const Value) RuntimeError!Value { return .null; } -// ========================================== // LimitIterator -// ========================================== fn limitConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1444,9 +1404,7 @@ fn limitSeek(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// ========================================== // InfiniteIterator -// ========================================== fn infiniteNext(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1459,9 +1417,7 @@ fn infiniteNext(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .null; } -// ========================================== // AppendIterator -// ========================================== fn appConstruct(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1566,9 +1522,7 @@ fn appGetArrayIterator(ctx: *NativeContext, _: []const Value) RuntimeError!Value return .{ .array = iters }; } -// ========================================== // CallbackFilterIterator -// ========================================== fn cbfConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1592,9 +1546,7 @@ fn cbfAccept(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .bool = result.isTruthy() }; } -// ========================================== // RegexIterator -// ========================================== fn rxConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1729,9 +1681,7 @@ fn rxSetPregFlags(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// ========================================== // CachingIterator -// ========================================== fn ciConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -1839,9 +1789,7 @@ fn ciSetFlags(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// ========================================== // MultipleIterator -// ========================================== fn miConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; @@ -2044,9 +1992,7 @@ fn miSetFlags(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// ========================================== // RecursiveFilterIterator / RecursiveCallbackFilterIterator / RecursiveRegexIterator -// ========================================== fn rfiHasChildren(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .{ .bool = false }; @@ -2102,9 +2048,7 @@ fn rrxGetChildren(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .object = new_obj }; } -// ========================================== // RecursiveArrayIterator -// ========================================== fn raiHasChildren(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .{ .bool = false }; @@ -2124,9 +2068,7 @@ fn raiGetChildren(ctx: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .object = new_obj }; } -// ========================================== // RecursiveTreeIterator -// ========================================== fn rtiConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1) return .null; @@ -2168,7 +2110,6 @@ fn rtiGetPrefix(ctx: *NativeContext, _: []const Value) RuntimeError!Value { fn rtiGetEntry(ctx: *NativeContext, _: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .{ .string = "" }; - // delegate to inner current const inner = riiCurrentIterator(obj) orelse return .{ .string = "" }; const cur = try ctx.vm.callMethod(inner, "current", &.{}); if (cur == .string) return cur; @@ -2184,9 +2125,7 @@ fn rtiGetPostfix(_: *NativeContext, _: []const Value) RuntimeError!Value { return .{ .string = "" }; } -// ========================================== // GlobIterator -// ========================================== fn giConstruct(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const obj = getThis(ctx) orelse return .null; diff --git a/src/stdlib/strings.zig b/src/stdlib/strings.zig index 183fa102..59426303 100644 --- a/src/stdlib/strings.zig +++ b/src/stdlib/strings.zig @@ -456,7 +456,6 @@ fn str_shuffle(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (s.len <= 1) return .{ .string = s }; const buf = try ctx.allocator.alloc(u8, s.len); @memcpy(buf, s); - // Fisher-Yates with the default PRNG var prng = std.Random.DefaultPrng.init(@bitCast(std.time.timestamp())); var i: usize = buf.len - 1; while (i > 0) : (i -= 1) { @@ -583,7 +582,6 @@ fn natCompare(a: []const u8, b: []const u8, fold_case: bool) i64 { const ca = a[ai]; const cb = b[bi]; if (std.ascii.isDigit(ca) and std.ascii.isDigit(cb)) { - // skip leading zeros var as = ai; while (as < a.len and a[as] == '0') as += 1; var ae = as; @@ -598,7 +596,6 @@ fn natCompare(a: []const u8, b: []const u8, fold_case: bool) i64 { for (a[as..ae], b[bs..be]) |x, y| { if (x != y) return @as(i64, x) - @as(i64, y); } - // skip past digits ai = ae; bi = be; continue; @@ -1007,7 +1004,6 @@ fn native_number_format(ctx: *NativeContext, args: []const Value) RuntimeError!V const power = std.math.pow(f64, 10.0, @floatFromInt(-decimals_signed)); var rounded = @round(num / power) * power; if (rounded == 0) rounded = 0; // normalize -0 - // recurse with decimals=0 var combined: [4]Value = undefined; combined[0] = .{ .float = rounded }; combined[1] = .{ .int = 0 }; @@ -1144,7 +1140,6 @@ fn roundFloatToDecimals(num: f64, decimals: usize, int_buf: []u8, frac_buf: []u8 } else { combined[k] = '0'; if (k == 0) { - // need to prepend '1' if (c_len >= combined.len) c_len = combined.len - 1; var j: usize = c_len; while (j > 0) : (j -= 1) combined[j] = combined[j - 1]; @@ -1173,7 +1168,6 @@ fn roundFloatToDecimals(num: f64, decimals: usize, int_buf: []u8, frac_buf: []u8 frac_buf[fp_len] = if (idx < c_len) combined[idx] else '0'; } - // suppress -0 const all_zero = blk: { for (int_buf[0..ip_len]) |b| if (b != '0') break :blk false; for (frac_buf[0..fp_len]) |b| if (b != '0') break :blk false; @@ -1313,7 +1307,6 @@ fn sprintfImpl(ctx: *NativeContext, fmt_str: []const u8, args: []const Value) ![ continue; } - // check for argument swapping: %N$ var explicit_arg: ?usize = null; { var j = i; @@ -1372,7 +1365,6 @@ fn sprintfImpl(ctx: *NativeContext, fmt_str: []const u8, args: []const Value) ![ if (i < fmt_str.len and fmt_str[i] == '.') { i += 1; if (i < fmt_str.len and fmt_str[i] == '*') { - // dynamic precision from next arg const prec_arg = if (arg_idx < args.len) args[arg_idx] else Value.null; arg_idx += 1; precision = @intCast(@max(0, Value.toInt(prec_arg))); @@ -1790,7 +1782,6 @@ fn roundHalfToEven(x: f64) f64 { const diff = x - fl; if (diff < 0.5) return fl; if (diff > 0.5) return fl + 1; - // exact half: round to even const fl_i: i64 = @intFromFloat(fl); if (@mod(fl_i, 2) == 0) return fl; return fl + 1; @@ -2550,7 +2541,6 @@ fn native_mb_check_encoding(_: *NativeContext, args: []const Value) RuntimeError var is_ascii = false; if (args.len >= 2 and args[1] == .string) { const enc = args[1].string; - // case-insensitive ASCII check if (enc.len >= 5) { var lo: [16]u8 = undefined; const cap = @min(enc.len, lo.len); @@ -2646,9 +2636,7 @@ fn caseExpansionUpper(cp: u21) ?[]const u8 { } fn unicodeToUpper(cp: u21) u21 { - // latin-1 supplement: a-with-grave through o-with-diaeresis if (cp >= 0x00E0 and cp <= 0x00F6) return cp - 0x20; - // latin-1 supplement: o-with-slash through thorn if (cp >= 0x00F8 and cp <= 0x00FE) return cp - 0x20; // latin extended-a: pairing alternates per subrange. 0100-0137 and // 014A-0177 pair even-upper/odd-lower; 0139-0148 and 0179-017E pair @@ -2686,9 +2674,7 @@ fn unicodeToUpper(cp: u21) u21 { } pub fn unicodeToLower(cp: u21) u21 { - // latin-1 supplement: A-with-grave through O-with-diaeresis if (cp >= 0x00C0 and cp <= 0x00D6) return cp + 0x20; - // latin-1 supplement: O-with-slash through Thorn if (cp >= 0x00D8 and cp <= 0x00DE) return cp + 0x20; // latin extended-a: pairing alternates per subrange (see unicodeToUpper) if (cp >= 0x0100 and cp <= 0x0137 and cp % 2 == 0) return cp + 1; @@ -3076,7 +3062,6 @@ fn native_mb_convert_encoding(ctx: *NativeContext, args: []const Value) RuntimeE return args[0]; // ascii is a subset of both } if ((isUtf8Encoding(from) or isLatin1Encoding(from)) and isAsciiEncoding(to)) { - // strip non-ascii bytes var out = std.ArrayListUnmanaged(u8){}; errdefer out.deinit(ctx.allocator); var i: usize = 0; @@ -3591,7 +3576,6 @@ fn native_mb_strimwidth(ctx: *NativeContext, args: []const Value) RuntimeError!V if (max_width <= 0) return .{ .string = try ctx.createString("") }; const marker = if (args.len >= 4 and args[3] == .string) args[3].string else ""; - // marker width var marker_w: i64 = 0; { var mi: usize = 0; @@ -3651,7 +3635,6 @@ fn native_mb_strcut(ctx: *NativeContext, args: []const Value) RuntimeError!Value var start: i64 = if (args.len >= 2) Value.toInt(args[1]) else 0; if (start < 0) start = @max(0, @as(i64, @intCast(s.len)) + start); var ustart: usize = @intCast(@min(start, @as(i64, @intCast(s.len)))); - // align to leading byte while (ustart < s.len and (s[ustart] & 0xC0) == 0x80) ustart += 1; var end: usize = s.len; if (args.len >= 3 and args[2] != .null) { @@ -3963,13 +3946,11 @@ fn native_convert_uudecode(ctx: *NativeContext, args: []const Value) RuntimeErro var buf = std.ArrayListUnmanaged(u8){}; var p: usize = 0; while (p < s.len) { - // read length byte const lb = s[p]; p += 1; // backtick or space means end of stream const decoded_len: usize = if (lb == 0x60 or lb < 0x20) 0 else @intCast(lb - 0x20); if (decoded_len == 0) { - // skip optional newline and stop if (p < s.len and (s[p] == '\n' or s[p] == '\r')) p += 1; break; } @@ -4702,7 +4683,6 @@ fn native_http_build_query(ctx: *NativeContext, args: []const Value) RuntimeErro if (args.len == 0 or args[0] != .array) return .{ .string = "" }; const arr = args[0].array; const prefix_str: []const u8 = if (args.len >= 2 and args[1] == .string) args[1].string else ""; - // arg_separator default "&" const arg_sep: []const u8 = if (args.len >= 3 and args[2] == .string and args[2].string.len > 0) args[2].string else "&"; // encoding: PHP_QUERY_RFC1738 = 1 (default, space → +), PHP_QUERY_RFC3986 = 2 (space → %20) const enc_type: i64 = if (args.len >= 4) Value.toInt(args[3]) else 1; @@ -4938,21 +4918,18 @@ fn native_parse_url(ctx: *NativeContext, args: []const Value) RuntimeError!Value var rest = url[authority_start..]; - // split off fragment var fragment: ?[]const u8 = null; if (std.mem.indexOf(u8, rest, "#")) |pos| { fragment = rest[pos + 1 ..]; rest = rest[0..pos]; } - // split off query var query: ?[]const u8 = null; if (std.mem.indexOf(u8, rest, "?")) |pos| { query = rest[pos + 1 ..]; rest = rest[0..pos]; } - // split authority from path var host: ?[]const u8 = null; var port: ?i64 = null; var user: ?[]const u8 = null; @@ -5164,7 +5141,6 @@ fn insertParsedKey(ctx: *NativeContext, root: *PhpArray, key: []const u8, value: if (seg.len == 0) { // append: next_arr.append decides the int key current_arr = next_arr; - // pre-compute next int key var max_int: i64 = -1; for (next_arr.entries.items) |e| { if (e.key == .int and e.key.int > max_int) max_int = e.key.int; @@ -5186,7 +5162,6 @@ fn native_addcslashes(ctx: *NativeContext, args: []const Value) RuntimeError!Val if (args.len < 2 or args[0] != .string or args[1] != .string) return if (args.len >= 1) args[0] else Value.null; const s = args[0].string; const charset = args[1].string; - // expand "a..z" ranges var mask = [_]bool{false} ** 256; var i: usize = 0; while (i < charset.len) : (i += 1) { @@ -5346,7 +5321,6 @@ fn native_strtok(ctx: *NativeContext, args: []const Value) RuntimeError!Value { }; const s = ctx.vm.strtok_state orelse return .{ .bool = false }; var p = ctx.vm.strtok_pos; - // skip leading delimiters while (p < s.len and std.mem.indexOfScalar(u8, tokens, s[p]) != null) : (p += 1) {} if (p >= s.len) { ctx.vm.strtok_pos = s.len; @@ -5371,7 +5345,6 @@ fn native_stristr(ctx: *NativeContext, args: []const Value) RuntimeError!Value { const lower_n = try toLowerBuf(ctx.allocator, needle); defer ctx.allocator.free(lower_n); if (std.mem.indexOf(u8, lower_h, lower_n)) |pos| { - // return the original-case substring if (before_needle) return .{ .string = try ctx.createString(haystack[0..pos]) }; return .{ .string = try ctx.createString(haystack[pos..]) }; } @@ -5505,7 +5478,6 @@ fn vsprintfArgsTooFew(ctx: *NativeContext, fmt: []const u8, argc: usize) Runtime i += 1; if (i >= fmt.len) break; if (fmt[i] == '%') continue; - // positional %N$ var j = i; var num: usize = 0; while (j < fmt.len and fmt[j] >= '0' and fmt[j] <= '9') : (j += 1) { @@ -5538,7 +5510,6 @@ fn vsprintfArgsTooFew(ctx: *NativeContext, fmt: []const u8, argc: usize) Runtime fn native_fscanf(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 2 or args[0] != .object) return .null; - // read one line via fgets const line = try ctx.vm.callByName("fgets", &.{args[0]}); if (line == .bool and !line.bool) return .{ .bool = false }; if (line != .string) return .null; @@ -5576,7 +5547,6 @@ fn native_sscanf(ctx: *NativeContext, args: []const Value) RuntimeError!Value { // % spec fp += 1; if (fp >= fmt.len) break; - // skip optional width var width: usize = 0; var has_width = false; while (fp < fmt.len and fmt[fp] >= '0' and fmt[fp] <= '9') { @@ -5638,7 +5608,6 @@ fn native_sscanf(ctx: *NativeContext, args: []const Value) RuntimeError!Value { try captures.append(ctx.allocator, .{ .string = s }); }, 'x', 'X' => { - // optional 0x / 0X prefix if (ip + 1 < input.len and input[ip] == '0' and (input[ip + 1] == 'x' or input[ip + 1] == 'X')) ip += 2; const start = ip; while (ip < input.len and std.ascii.isHex(input[ip])) { @@ -5679,7 +5648,6 @@ fn native_sscanf(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } }, 'n' => { - // consumed-byte-count. doesn't read from input try captures.append(ctx.allocator, .{ .int = @intCast(ip) }); }, '%' => { @@ -5925,7 +5893,6 @@ fn native_metaphone(ctx: *NativeContext, args: []const Value) RuntimeError!Value var i: usize = 0; - // skip initial silent consonant pairs if (upper.len >= 2) { const pair = upper[0..2]; if (std.mem.eql(u8, pair, "AE") or std.mem.eql(u8, pair, "GN") or @@ -5953,7 +5920,6 @@ fn native_metaphone(ctx: *NativeContext, args: []const Value) RuntimeError!Value continue; } - // skip doubled letters (except C) if (c == prev and c != 'C') { i += 1; continue; diff --git a/src/stdlib/system.zig b/src/stdlib/system.zig index c79c6e17..02ff3654 100644 --- a/src/stdlib/system.zig +++ b/src/stdlib/system.zig @@ -424,7 +424,6 @@ fn native_getopt(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } if (arg.len < 2 or arg[0] != '-') break; if (arg[1] == '-') { - // long const eq_idx = std.mem.indexOfScalar(u8, arg[2..], '='); const name = if (eq_idx) |e| arg[2 .. 2 + e] else arg[2..]; const inline_val: ?[]const u8 = if (eq_idx) |e| arg[2 + e + 1 ..] else null; @@ -900,7 +899,6 @@ fn native_get_defined_vars(ctx: *NativeContext, _: []const Value) RuntimeError!V try result.set(alloc, .{ .string = name }, frame.locals[i]); } - // dynamic vars (extract'd, etc.) var iter = frame.vars.iterator(); while (iter.next()) |entry| { const raw = entry.key_ptr.*; @@ -1017,7 +1015,6 @@ fn native_exec(ctx: *NativeContext, args: []const Value) RuntimeError!Value { } if (args[1] != .array) ctx.setCallerVar(1, args.len, .{ .array = arr }); } - // optional result_code (param 3, by-ref) const exit_code: i64 = switch (result.term) { .Exited => |c| @intCast(c), .Signal => |c| @as(i64, @intCast(c)) + 128, @@ -1047,7 +1044,6 @@ fn native_system(ctx: *NativeContext, args: []const Value) RuntimeError!Value { }; if (args.len >= 2) ctx.setCallerVar(1, args.len, .{ .int = exit_code }); - // last line of output var last_line: []const u8 = ""; if (result.stdout.len > 0) { var end = result.stdout.len; diff --git a/src/stdlib/testing.zig b/src/stdlib/testing.zig index df7ab98f..12b8c345 100644 --- a/src/stdlib/testing.zig +++ b/src/stdlib/testing.zig @@ -22,7 +22,6 @@ fn assertEq(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 2) return failAssertion(ctx, "assert_eq requires 2 arguments"); if (Value.identical(args[0], args[1])) return .null; - // build error message var buf1 = std.ArrayListUnmanaged(u8){}; try args[0].format(&buf1, ctx.allocator); const s1 = try buf1.toOwnedSlice(ctx.allocator); diff --git a/src/stdlib/types.zig b/src/stdlib/types.zig index 3c36dc15..daae5d64 100644 --- a/src/stdlib/types.zig +++ b/src/stdlib/types.zig @@ -672,7 +672,6 @@ fn native_setlocale(ctx: *NativeContext, args: []const Value) RuntimeError!Value else => continue, } } - // no acceptable locale provided return .{ .bool = false }; } @@ -932,7 +931,6 @@ fn native_is_callable(ctx: *NativeContext, args: []const Value) RuntimeError!Val fillName(ctx, args, name); if (ctx.vm.native_fns.contains(name)) return .{ .bool = true }; if (ctx.vm.functions.contains(name)) return .{ .bool = true }; - // Class::method string form if (std.mem.indexOf(u8, name, "::")) |sep| { const class_part = name[0..sep]; const method_part = name[sep + 2 ..]; @@ -2089,7 +2087,6 @@ fn native_iconv_mime_decode_headers(ctx: *NativeContext, args: []const Value) Ru } fn native_iconv_mime_encode(_: *NativeContext, args: []const Value) RuntimeError!Value { - // best-effort: emit "Header: value" unencoded if (args.len < 2 or args[0] != .string or args[1] != .string) return .{ .bool = false }; return args[1]; } @@ -2283,7 +2280,6 @@ fn native_class_alias(ctx: *NativeContext, args: []const Value) RuntimeError!Val } if (ctx.vm.classes.get(original)) |cls| { var alias_def = ClassDef{ .name = alias, .parent = original }; - // copy interfaces for (cls.interfaces.items) |iface| { try alias_def.interfaces.append(ctx.allocator, iface); } @@ -2735,7 +2731,6 @@ fn native_iterator_apply(ctx: *NativeContext, args: []const Value) RuntimeError! if (args[0] == .object) { const obj = args[0].object; - // try IteratorAggregate first if (ctx.vm.hasMethod(obj.class_name, "getIterator")) { const inner = try ctx.vm.callMethod(obj, "getIterator", &.{}); if (inner == .object) { @@ -2915,7 +2910,6 @@ fn native_filter_var(_ctx: *NativeContext, args: []const Value) RuntimeError!Val if (require_array or force_array) { if (value != .array) { if (require_array) return fail_default; - // force_array wraps scalar const out = try _ctx.createArray(); const single_flags = eff_flags & ~@as(i64, 0x05000000); const sub_args = [_]Value{ value, args[1], .{ .int = single_flags } }; @@ -2982,7 +2976,6 @@ fn native_filter_var(_ctx: *NativeContext, args: []const Value) RuntimeError!Val }; const trimmed = std.mem.trim(u8, s, " \t\n\r"); if (trimmed.len == 0) return fail_default; - // optional sign var rest = trimmed; var sign: i64 = 1; if (rest[0] == '+') { @@ -2992,17 +2985,14 @@ fn native_filter_var(_ctx: *NativeContext, args: []const Value) RuntimeError!Val rest = rest[1..]; } if (rest.len == 0) return fail_default; - // hex with ALLOW_HEX if (allow_hex and rest.len > 2 and rest[0] == '0' and (rest[1] == 'x' or rest[1] == 'X')) { const v = std.fmt.parseInt(i64, rest[2..], 16) catch return fail_default; break :blk sign * v; } - // octal with ALLOW_OCTAL, "0o" prefix if (allow_octal and rest.len > 2 and rest[0] == '0' and (rest[1] == 'o' or rest[1] == 'O')) { const v = std.fmt.parseInt(i64, rest[2..], 8) catch return fail_default; break :blk sign * v; } - // octal with ALLOW_OCTAL ("0" prefix) if (allow_octal and rest.len > 1 and rest[0] == '0' and std.ascii.isDigit(rest[1])) { const v = std.fmt.parseInt(i64, rest[1..], 8) catch return fail_default; break :blk sign * v; diff --git a/src/stdlib/xml_parser.zig b/src/stdlib/xml_parser.zig index 87d406ad..9d7adc34 100644 --- a/src/stdlib/xml_parser.zig +++ b/src/stdlib/xml_parser.zig @@ -251,7 +251,6 @@ fn xmlParse(ctx: *NativeContext, args: []const Value) RuntimeError!Value { try invokeEnd(ctx, &st, folded); continue; } - // start tag _ = st.advance(); // < const name_start = st.pos; while (st.pos < st.src.len) { diff --git a/src/stdlib/xmlreader.zig b/src/stdlib/xmlreader.zig index 406012af..5d2c052b 100644 --- a/src/stdlib/xmlreader.zig +++ b/src/stdlib/xmlreader.zig @@ -57,7 +57,6 @@ pub fn cleanupObject(obj: *PhpObject) void { if (!obj.pooled and std.mem.eql(u8, obj.class_name, "XMLReader")) closeExisting(obj); } -// ---------------- methods ---------------- fn xrOpen(ctx: *NativeContext, args: []const Value) RuntimeError!Value { if (args.len < 1 or args[0] != .string) return .{ .bool = false }; @@ -368,7 +367,6 @@ fn xrGet(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .null; } -// ---------------- registration ---------------- fn cleanupPoolable(obj: *PhpObject) bool { cleanupObject(obj); @@ -390,7 +388,6 @@ pub fn register(vm: *VM, a: Allocator) !void { try def.methods.put(a, m, .{ .name = m, .arity = 0 }); } - // class constants const xr_consts = .{ .{ "NONE", 0 }, .{ "ELEMENT", 1 }, .{ "ATTRIBUTE", 2 }, .{ "TEXT", 3 }, .{ "CDATA", 4 }, .{ "ENTITY_REF", 5 }, .{ "ENTITY", 6 }, .{ "PI", 7 }, @@ -439,7 +436,6 @@ pub fn register(vm: *VM, a: Allocator) !void { .{ "COMMENT", 8 }, .{ "DOC", 9 }, .{ "DOC_TYPE", 10 }, .{ "DOC_FRAGMENT", 11 }, .{ "NOTATION", 12 }, .{ "WHITESPACE", 13 }, .{ "SIGNIFICANT_WHITESPACE", 14 }, .{ "END_ELEMENT", 15 }, .{ "END_ENTITY", 16 }, .{ "XML_DECLARATION", 17 }, - // load options (subset) .{ "LOADDTD", 1 }, .{ "DEFAULTATTRS", 2 }, .{ "VALIDATE", 3 }, .{ "SUBST_ENTITIES", 4 }, }; inline for (consts) |k| { diff --git a/src/stdlib/xmlwriter.zig b/src/stdlib/xmlwriter.zig index 0caee7d0..5fcafaf3 100644 --- a/src/stdlib/xmlwriter.zig +++ b/src/stdlib/xmlwriter.zig @@ -61,7 +61,6 @@ pub fn cleanupObject(obj: *PhpObject) void { if (!obj.pooled and std.mem.eql(u8, obj.class_name, "XMLWriter")) closeExisting(obj); } -// ---------------- methods ---------------- fn xwOpenMemory(ctx: *NativeContext, _: []const Value) RuntimeError!Value { // if called statically, create object. if instance method, set up on $this @@ -342,7 +341,6 @@ fn xwWritePi(ctx: *NativeContext, args: []const Value) RuntimeError!Value { return .{ .bool = c.xmlTextWriterWritePI(writer, @ptrCast(t_z.ptr), @ptrCast(c_z.ptr)) >= 0 }; } -// ---------------- registration ---------------- fn cleanupPoolable(obj: *PhpObject) bool { cleanupObject(obj); diff --git a/src/test_runner.zig b/src/test_runner.zig index 4486a578..c88a0f02 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -3,6 +3,7 @@ const parser = @import("pipeline/parser.zig"); const compiler = @import("pipeline/compiler.zig"); const CompileResult = compiler.CompileResult; const VM = @import("runtime/vm.zig").VM; +const Value = @import("runtime/value.zig").Value; const ObjFunction = @import("pipeline/bytecode.zig").ObjFunction; const tui = @import("tui.zig"); @@ -97,7 +98,6 @@ fn runTestFile(allocator: Allocator, path: []const u8) TestResult { }; defer compile_result.deinit(); - // find test_ functions var test_fns = std.ArrayListUnmanaged(*const ObjFunction){}; defer test_fns.deinit(allocator); for (compile_result.functions.items) |*func| { @@ -113,7 +113,10 @@ fn runTestFile(allocator: Allocator, path: []const u8) TestResult { result.failed = 1; return result; }; - defer { vm.deinit(); allocator.destroy(vm); } + defer { + vm.deinit(); + allocator.destroy(vm); + } vm.interpret(&compile_result) catch { printFail(path, if (vm.error_msg) |m| m else "runtime error"); @@ -125,22 +128,28 @@ fn runTestFile(allocator: Allocator, path: []const u8) TestResult { return result; } - // run each test_ function individually tui.step("file", path); for (test_fns.items) |func| { const vm = VM.initOnHeap(allocator) catch continue; - defer { vm.deinit(); allocator.destroy(vm); } + defer { + vm.deinit(); + allocator.destroy(vm); + } + + vm.registerResultFunctions(&compile_result) catch continue; - // register all functions from the compile result (without executing top-level code) - for (compile_result.functions.items) |*f| { - vm.registerFunction(f) catch continue; + var locals: []Value = &.{}; + if (func.local_count > 0) { + locals = allocator.alloc(Value, func.local_count) catch continue; + @memset(locals, .null); } - // execute the test function vm.frames[0] = .{ .chunk = &func.chunk, .ip = 0, .vars = .{}, + .locals = locals, + .func = func, }; vm.frame_count = 1; @@ -148,8 +157,10 @@ fn runTestFile(allocator: Allocator, path: []const u8) TestResult { const err_msg = if (vm.error_msg) |m| m else "assertion failed"; printFail(func.name, err_msg); result.failed += 1; + vm.runShutdownDestructors(); continue; }; + vm.runShutdownDestructors(); printPass(func.name); result.passed += 1; diff --git a/src/tui.zig b/src/tui.zig index 4720f249..d1691ac9 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -3,7 +3,6 @@ const posix = std.posix; const STDERR = posix.STDERR_FILENO; -// colors const reset = "\x1b[0m"; const bold = "\x1b[1m"; const dim = "\x1b[2m"; @@ -94,7 +93,6 @@ pub fn progress(current: usize, total: usize, name: []const u8) void { write(reset); write(" "); write(name); - // clear rest of line write("\x1b[K"); } @@ -144,7 +142,6 @@ pub fn tableRow(col1: []const u8, col2: []const u8, col3: []const u8) void { write(cyan); write(col1); write(reset); - // pad to 40 chars var pad: usize = if (col1.len < 38) 38 - col1.len else 2; while (pad > 0) : (pad -= 1) write(" "); write(dim); diff --git a/src/websocket.zig b/src/websocket.zig index f0a45632..c7394283 100644 --- a/src/websocket.zig +++ b/src/websocket.zig @@ -212,7 +212,6 @@ pub fn writeCloseFrame(writer: anytype, code: u16) !void { try writeFrame(writer, .close, &payload); } -// tests test "accept key computation" { var buf: [28]u8 = undefined; @@ -241,12 +240,10 @@ fn writeMaskedFrame(stream: std.net.Stream, opcode: Opcode, payload: []const u8) std.mem.writeInt(u16, hdr[2..4], @intCast(payload.len), .big); hdr_len = 4; } - // mask key const mask = [4]u8{ 0x12, 0x34, 0x56, 0x78 }; @memcpy(hdr[hdr_len .. hdr_len + 4], &mask); hdr_len += 4; _ = try stream.write(hdr[0..hdr_len]); - // write masked payload var masked: [256]u8 = undefined; for (0..payload.len) |i| masked[i] = payload[i] ^ mask[i % 4]; if (payload.len > 0) _ = try stream.write(masked[0..payload.len]); @@ -298,7 +295,6 @@ test "tryParseFrame from buffer" { // incomplete buffer returns null (no error) try std.testing.expect((try tryParseFrame(buf[0..3], 256)) == null); - // complete buffer returns frame const result = (try tryParseFrame(buf[0..total], 256)).?; try std.testing.expect(result.frame.fin); try std.testing.expectEqual(Opcode.text, result.frame.opcode); @@ -311,7 +307,6 @@ test "unmasked frame rejected" { defer pair[0].close(); defer pair[1].close(); - // write an unmasked frame (server-style) try writeFrame(pair[0], .text, "bad"); var buf: [256]u8 = undefined; @@ -351,7 +346,6 @@ test "rsv bits rejected" { test "reserved opcodes rejected" { var buf: [16]u8 = undefined; - // opcode 0x3 is reserved buf[0] = 0x83; buf[1] = 0x80 | 0; @memset(buf[2..6], 0); @@ -361,7 +355,6 @@ test "reserved opcodes rejected" { test "fragmented control frame rejected" { var buf: [16]u8 = undefined; - // ping with FIN=0 buf[0] = 0x09; buf[1] = 0x80 | 0; @memset(buf[2..6], 0); diff --git a/tests/named_args_native.php b/tests/named_args_native.php index 8511430a..448d4395 100644 --- a/tests/named_args_native.php +++ b/tests/named_args_native.php @@ -1,19 +1,46 @@ 1]) . "\n"; +// string functions and reordered args +assert(substr(string: "hello world", offset: 6) === "world"); +assert(substr(offset: 0, length: 3, string: "hello") === "hel"); +assert(str_replace(replace: "Y", search: "X", subject: "aXbXc") === "aYbYc"); +assert(str_ireplace(replace: "Y", search: "x", subject: "aXbXc") === "aYbYc"); +assert(implode(array: [1, 2, 3], separator: "-") === "1-2-3"); +assert(str_pad(pad_string: ".", length: 5, string: "hi") === "hi..."); + +// array and search functions +assert(in_array(strict: true, haystack: ["a", "b", "c"], needle: "b") === true); +assert(in_array(needle: "d", haystack: ["a", "b", "c"]) === false); +assert(array_keys(array: ["a" => 1, "b" => 2]) === ["a", "b"]); +assert(array_keys(strict: true, filter_value: 1, array: [1, "1", 2]) === [0]); +assert(array_values(array: ["x" => 10, "y" => 20]) === [10, 20]); +assert(array_flip(array: ["a" => 1, "b" => 2]) === [1 => "a", 2 => "b"]); + +// encoding, json, and math +assert(json_encode(value: ["a" => 1]) === '{"a":1}'); +assert(base64_encode(string: "test") === "dGVzdA=="); +assert(base64_decode(strict: true, string: "dGVzdA==") === "test"); +assert(bin2hex(string: "ABC") === "414243"); +assert(hex2bin(string: "414243") === "ABC"); +assert(round(precision: 2, num: 3.14159) === 3.14); + +// url, multibyte, and path functions +assert(parse_url(component: PHP_URL_PATH, url: "https://example.com/api/v1") === "/api/v1"); +assert(http_build_query(numeric_prefix: "num_", data: ["a" => 1, 2 => "b"]) === "a=1&num_2=b"); +assert(mb_strlen(encoding: "UTF-8", string: "café") === 4); +assert(mb_substr(encoding: "UTF-8", length: 2, start: 1, string: "café") === "af"); +assert(pathinfo(flags: PATHINFO_EXTENSION, path: "/path/to/file.php") === "php"); +assert(basename(suffix: ".txt", path: "/path/to/note.txt") === "note"); + +// fixed named args with trailing variadics +assert(sprintf(format: "%s: %d", "count", 5) === "count: 5"); + +$threw = false; +try { + str_replace(invalid_name: "test", replace: "b", search: "a", subject: "a"); +} catch (Error $e) { + $threw = true; +} +assert($threw === true, "Passing unknown named argument must throw an Error"); + +echo "All named argument tests passed!\n"; diff --git a/tests/php84_asymmetric_visibility.php b/tests/php84_asymmetric_visibility.php new file mode 100644 index 00000000..14cc7a6f --- /dev/null +++ b/tests/php84_asymmetric_visibility.php @@ -0,0 +1,150 @@ +isPublic() === $public, "$name isPublic()"); + check($rp->isProtected() === $protected, "$name isProtected()"); + check($rp->isPrivate() === $private, "$name isPrivate()"); + check($rp->isProtectedSet() === $protectedSet, "$name isProtectedSet()"); + check($rp->isPrivateSet() === $privateSet, "$name isPrivateSet()"); + check($rp->isStatic() === $static, "$name isStatic()"); + check($rp->isReadOnly() === $readonly, "$name isReadOnly()"); + check($rp->isFinal() === $final, "$name isFinal()"); + check($rp->getModifiers() === $modifiers, "$name getModifiers()"); +} + +class AsymTest { + public string $a = "a"; + public private(set) string $b = "b"; + public protected(set) string $c = "c"; + protected private(set) string $d = "d"; + private string $e = "e"; + public static string $f = "f"; + public readonly string $g; + protected readonly string $h; + private readonly string $i; + public public(set) readonly string $j; + public protected(set) readonly string $k; + public private(set) readonly string $l; + final protected string $m; + private private(set) string $n; +} + +checkProperty(AsymTest::class, 'a', true, false, false, false, false, false, false, false, 1); +checkProperty(AsymTest::class, 'b', true, false, false, false, true, false, false, true, 4129); +checkProperty(AsymTest::class, 'c', true, false, false, true, false, false, false, false, 2049); +checkProperty(AsymTest::class, 'd', false, true, false, false, true, false, false, true, 4130); +checkProperty(AsymTest::class, 'e', false, false, true, false, false, false, false, false, 4); +checkProperty(AsymTest::class, 'f', true, false, false, false, false, true, false, false, 17); +checkProperty(AsymTest::class, 'g', true, false, false, true, false, false, true, false, 2177); +checkProperty(AsymTest::class, 'h', false, true, false, false, false, false, true, false, 130); +checkProperty(AsymTest::class, 'i', false, false, true, false, false, false, true, false, 132); +checkProperty(AsymTest::class, 'j', true, false, false, false, false, false, true, false, 129); +checkProperty(AsymTest::class, 'k', true, false, false, true, false, false, true, false, 2177); +checkProperty(AsymTest::class, 'l', true, false, false, false, true, false, true, true, 4257); +checkProperty(AsymTest::class, 'm', false, true, false, false, false, false, false, true, 34); +checkProperty(AsymTest::class, 'n', false, false, true, false, false, false, false, false, 4); + +check(!method_exists(ReflectionProperty::class, 'isPublicSet'), 'ReflectionProperty::isPublicSet() must not exist'); + +// Symmetric private visibility is not final and may be redeclared in a child. +class SymmetricPrivateParent { + private private(set) string $value; +} +class SymmetricPrivateChild extends SymmetricPrivateParent { + private string $value; +} +check(true, 'symmetric private(set) redeclaration is legal'); + +// private readonly is also not a final property. +class PrivateReadonlyParent { + private readonly int $value; +} +class PrivateReadonlyChild extends PrivateReadonlyParent { + private int $value; +} +check(true, 'private readonly redeclaration is legal'); + + +readonly class ReadonlyPrivateClass { + private string $value; +} +checkProperty(ReadonlyPrivateClass::class, 'value', false, false, true, false, false, false, true, false, 132); + +trait AsymTrait { + public private(set) string $traitPrivateSet = "trait"; + public readonly string $traitReadonly; + private function traitPrivateMethod(): void {} + final protected function traitFinalMethod(): void {} +} + +class UsesAsymTrait { + use AsymTrait; +} + +checkProperty(UsesAsymTrait::class, 'traitPrivateSet', true, false, false, false, true, false, false, true, 4129); +checkProperty(UsesAsymTrait::class, 'traitReadonly', true, false, false, true, false, false, true, false, 2177); + +$traitPrivate = new ReflectionMethod(UsesAsymTrait::class, 'traitPrivateMethod'); +check($traitPrivate->isPrivate(), 'trait private method visibility'); + +$traitFinal = new ReflectionMethod(UsesAsymTrait::class, 'traitFinalMethod'); +check($traitFinal->isProtected(), 'trait final method protected visibility'); +check($traitFinal->isFinal(), 'trait final method isFinal()'); + +class PromotedAsym { + public function __construct( + public private(set) string $promoted, + ) {} +} + +$promoted = new ReflectionProperty(PromotedAsym::class, 'promoted'); +check($promoted->isPromoted(), 'promoted property isPromoted()'); +check($promoted->isPrivateSet(), 'promoted property isPrivateSet()'); +check($promoted->isFinal(), 'promoted private(set) property isFinal()'); +check($promoted->getModifiers() === 4129, 'promoted property getModifiers()'); + +class PromotedAbbreviatedAsym { + public function __construct( + private(set) string $promoted, + ) {} +} + +$promotedAbbreviated = new ReflectionProperty(PromotedAbbreviatedAsym::class, 'promoted'); +check($promotedAbbreviated->isPromoted(), 'abbreviated promoted property isPromoted()'); +check($promotedAbbreviated->isPublic(), 'abbreviated promoted property isPublic()'); +check($promotedAbbreviated->isPrivateSet(), 'abbreviated promoted property isPrivateSet()'); +check($promotedAbbreviated->isFinal(), 'abbreviated promoted private(set) property isFinal()'); +check($promotedAbbreviated->getModifiers() === 4129, 'abbreviated promoted property getModifiers()'); + +$anon = new class { + public protected(set) string $anonymous = "anonymous"; +}; +checkProperty($anon, 'anonymous', true, false, false, true, false, false, false, false, 2049); + +check(ReflectionProperty::IS_PROTECTED_SET === 2048, 'IS_PROTECTED_SET'); +check(ReflectionProperty::IS_PRIVATE_SET === 4096, 'IS_PRIVATE_SET'); +check(ReflectionProperty::IS_FINAL === 32, 'IS_FINAL'); +check(ReflectionProperty::IS_ABSTRACT === 64, 'IS_ABSTRACT'); +check(ReflectionProperty::IS_VIRTUAL === 512, 'IS_VIRTUAL'); + +echo "OK\n";