From 48e335eaf8205fa573d5ffd32181436d94c55872 Mon Sep 17 00:00:00 2001 From: PurHur Date: Wed, 20 May 2026 17:14:14 +0000 Subject: [PATCH] Add json_encode stdlib and 004-ApiJson example (issues #61, #270) Ship VM delegation via VmJson, LLVM JIT/AOT encoding for string-key assoc arrays (bool/int/string/null), string-key bool storage in hashtables, and a lint-clean JSON API example wired into ExamplesCompileTest. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 69 ++-- docs/bootstrap-profile.json | 8 +- docs/capabilities.md | 1 + examples/004-ApiJson/README.md | 32 ++ examples/004-ApiJson/example.php | 17 + examples/004-ApiJson/phpc.json | 4 + examples/README.md | 6 +- ext/standard/JitJsonEncode.php | 36 ++ ext/standard/Module.php | 1 + ext/standard/VmJson.php | 47 +++ ext/standard/json_encode.php | 55 +++ lib/JIT/Builtin/StringJsonEncode.php | 327 ++++++++++++++++++ lib/JIT/Builtin/Type.php | 7 + lib/JIT/Builtin/Type/HashTable.php | 98 ++++++ lib/JIT/Builtin/Type/String_.php | 1 + lib/JIT/HashTableHelper.php | 16 + test/compliance/cases/stdlib/json_encode.phpt | 7 + .../cases/stdlib/json_encode_jit.phpt | 7 + test/real/cases/json_encode_api.phpt | 9 + test/unit/ExamplesCompileTest.php | 2 + 20 files changed, 725 insertions(+), 25 deletions(-) create mode 100644 examples/004-ApiJson/README.md create mode 100644 examples/004-ApiJson/example.php create mode 100644 examples/004-ApiJson/phpc.json create mode 100644 ext/standard/JitJsonEncode.php create mode 100644 ext/standard/VmJson.php create mode 100644 ext/standard/json_encode.php create mode 100644 lib/JIT/Builtin/StringJsonEncode.php create mode 100644 test/compliance/cases/stdlib/json_encode.phpt create mode 100644 test/compliance/cases/stdlib/json_encode_jit.phpt create mode 100644 test/real/cases/json_encode_api.phpt diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 36348c2b..2a907a28 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,9 +8,9 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 240 | +| PHP files on vm.php path | 244 | | Source constructs flagged (blockers) | 10 | -| Source constructs flagged (warnings) | 629 | +| Source constructs flagged (warnings) | 634 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -43,6 +43,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/JitHtmlspecialchars.php` | 0 | 1 | | `ext/standard/JitHttpResponseCode.php` | 0 | 1 | | `ext/standard/JitImplode.php` | 0 | 1 | +| `ext/standard/JitJsonEncode.php` | 0 | 1 | | `ext/standard/JitNl2br.php` | 0 | 1 | | `ext/standard/JitNumberFormat.php` | 0 | 1 | | `ext/standard/JitParseUrl.php` | 0 | 1 | @@ -59,10 +60,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/JitStripTags.php` | 0 | 1 | | `ext/standard/JitStrpos.php` | 0 | 1 | | `ext/standard/JitUrlencode.php` | 0 | 1 | -| `ext/standard/Module.php` | 0 | 110 | +| `ext/standard/Module.php` | 0 | 111 | | `ext/standard/VmDate.php` | 0 | 1 | | `ext/standard/VmExit.php` | 0 | 2 | | `ext/standard/VmFs.php` | 0 | 3 | +| `ext/standard/VmJson.php` | 0 | 1 | | `ext/standard/VmNumberFormat.php` | 0 | 1 | | `ext/standard/VmScope.php` | 0 | 3 | | `ext/standard/VmString.php` | 0 | 4 | @@ -127,6 +129,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/is_nan.php` | 0 | 1 | | `ext/standard/is_numeric.php` | 0 | 1 | | `ext/standard/is_scalar.php` | 0 | 1 | +| `ext/standard/json_encode.php` | 0 | 1 | | `ext/standard/lcfirst.php` | 0 | 1 | | `ext/standard/log.php` | 0 | 1 | | `ext/standard/nl2br.php` | 0 | 1 | @@ -206,6 +209,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/JIT/Builtin/StringDateTime.php` | 0 | 1 | | `lib/JIT/Builtin/StringGetenv.php` | 0 | 1 | | `lib/JIT/Builtin/StringHtmlspecialchars.php` | 0 | 1 | +| `lib/JIT/Builtin/StringJsonEncode.php` | 0 | 1 | | `lib/JIT/Builtin/StringNl2br.php` | 0 | 1 | | `lib/JIT/Builtin/StringRandomBytes.php` | 0 | 1 | | `lib/JIT/Builtin/StringUcwords.php` | 0 | 1 | @@ -323,6 +327,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `ext/standard/JitJsonEncode.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/JitNl2br.php` **Warnings** (review for bootstrap subset): @@ -497,24 +506,25 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new header_list (line 110) - new getallheaders_ (line 111) - new http_response_code (line 112) -- new urlencode (line 113) -- new rawurlencode (line 114) -- new urldecode (line 115) -- new rawurldecode (line 116) -- new parse_url (line 117) -- new dirname (line 118) -- new basename (line 119) -- new realpath (line 120) -- new file_get_contents (line 121) -- new getenv_ (line 122) -- new putenv_ (line 123) -- new extract_ (line 124) -- new compact_ (line 125) -- new scandir (line 126) -- new glob_ (line 127) -- new time (line 128) -- new date (line 129) -- new gmdate (line 130) +- new json_encode (line 113) +- new urlencode (line 114) +- new rawurlencode (line 115) +- new urldecode (line 116) +- new rawurldecode (line 117) +- new parse_url (line 118) +- new dirname (line 119) +- new basename (line 120) +- new realpath (line 121) +- new file_get_contents (line 122) +- new getenv_ (line 123) +- new putenv_ (line 124) +- new extract_ (line 125) +- new compact_ (line 126) +- new scandir (line 127) +- new glob_ (line 128) +- new time (line 129) +- new date (line 130) +- new gmdate (line 131) - 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `ext/standard/VmDate.php` @@ -535,6 +545,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new Variable (line 20) - 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `ext/standard/VmJson.php` + +**Warnings** (review for bootstrap subset): +- 1 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/VmNumberFormat.php` **Warnings** (review for bootstrap subset): @@ -875,6 +890,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - 4 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `ext/standard/json_encode.php` + +**Warnings** (review for bootstrap subset): +- 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/lcfirst.php` **Warnings** (review for bootstrap subset): @@ -1403,6 +1423,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - 5 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `lib/JIT/Builtin/StringJsonEncode.php` + +**Warnings** (review for bootstrap subset): +- 4 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `lib/JIT/Builtin/StringNl2br.php` **Warnings** (review for bootstrap subset): @@ -1439,7 +1464,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: ### `lib/JIT/Builtin/Type/HashTable.php` **Warnings** (review for bootstrap subset): -- 29 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- 31 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler - 1 closure(s) ### `lib/JIT/Builtin/Type/MaskedArray.php` diff --git a/docs/bootstrap-profile.json b/docs/bootstrap-profile.json index 791ec03a..1d28d8f6 100644 --- a/docs/bootstrap-profile.json +++ b/docs/bootstrap-profile.json @@ -41,6 +41,7 @@ "ext/standard/JitHtmlspecialchars.php", "ext/standard/JitHttpResponseCode.php", "ext/standard/JitImplode.php", + "ext/standard/JitJsonEncode.php", "ext/standard/JitNl2br.php", "ext/standard/JitNumberFormat.php", "ext/standard/JitParseUrl.php", @@ -61,6 +62,7 @@ "ext/standard/VmDate.php", "ext/standard/VmExit.php", "ext/standard/VmFs.php", + "ext/standard/VmJson.php", "ext/standard/VmNumberFormat.php", "ext/standard/VmScope.php", "ext/standard/VmString.php", @@ -125,6 +127,7 @@ "ext/standard/is_nan.php", "ext/standard/is_numeric.php", "ext/standard/is_scalar.php", + "ext/standard/json_encode.php", "ext/standard/lcfirst.php", "ext/standard/log.php", "ext/standard/nl2br.php", @@ -204,6 +207,7 @@ "lib/JIT/Builtin/StringDateTime.php", "lib/JIT/Builtin/StringGetenv.php", "lib/JIT/Builtin/StringHtmlspecialchars.php", + "lib/JIT/Builtin/StringJsonEncode.php", "lib/JIT/Builtin/StringNl2br.php", "lib/JIT/Builtin/StringRandomBytes.php", "lib/JIT/Builtin/StringUcwords.php", @@ -274,9 +278,9 @@ "test/bootstrap-aot/echo_hello.php" ], "totals": { - "inventory_files": 240, + "inventory_files": 244, "excluded": 2, - "eligible": 238, + "eligible": 242, "aot_lint_targets": 2 } } diff --git a/docs/capabilities.md b/docs/capabilities.md index 782c0597..b7e58ffe 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -73,6 +73,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `is_object` | yes | yes | yes | types | | | `is_scalar` | yes | yes | yes | standard | | | `is_string` | yes | yes | yes | types | JIT PHPT | +| `json_encode` | yes | yes | yes | standard | JIT PHPT | | `lcfirst` | yes | yes | yes | standard | | | `log` | yes | yes | yes | standard | | | `ltrim` | yes | yes | yes | standard | AOT PHPT | diff --git a/examples/004-ApiJson/README.md b/examples/004-ApiJson/README.md new file mode 100644 index 00000000..fc2b1052 --- /dev/null +++ b/examples/004-ApiJson/README.md @@ -0,0 +1,32 @@ +# 004-ApiJson + +Minimal JSON API endpoint for `phpc lint` / `ExamplesCompileTest` ([#270](https://github.com/PurHur/php-compiler/issues/270)). + +## Run + +```console +./phpc lint examples/004-ApiJson/example.php +./phpc run examples/004-ApiJson/example.php +./phpc serve 127.0.0.1:8080 examples/004-ApiJson +curl -s -D - 'http://127.0.0.1:8080/example.php' +``` + +Expected body: + +```json +{"ok":true,"service":"php-compiler"} +``` + +## Blockers (closed) + +| Feature | Issue | Status | +|---------|-------|--------| +| `json_encode()` | [#61](https://github.com/PurHur/php-compiler/issues/61) | VM + JIT | +| `http_response_code()` | [#252](https://github.com/PurHur/php-compiler/issues/252) | done | +| `header()` | [#55](https://github.com/PurHur/php-compiler/issues/55) / stdlib | done | + +## Related + +- [#67](https://github.com/PurHur/php-compiler/issues/67) reference app `/api/status` +- [#210](https://github.com/PurHur/php-compiler/issues/210) routing +- [#90](https://github.com/PurHur/php-compiler/issues/90) routing guards diff --git a/examples/004-ApiJson/example.php b/examples/004-ApiJson/example.php new file mode 100644 index 00000000..2a1d0711 --- /dev/null +++ b/examples/004-ApiJson/example.php @@ -0,0 +1,17 @@ + true, 'service' => 'php-compiler']); diff --git a/examples/004-ApiJson/phpc.json b/examples/004-ApiJson/phpc.json new file mode 100644 index 00000000..d012338d --- /dev/null +++ b/examples/004-ApiJson/phpc.json @@ -0,0 +1,4 @@ +{ + "entry": "example.php", + "binary": ".phpc/bin/app" +} diff --git a/examples/README.md b/examples/README.md index ba4f9688..fd19ee87 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,6 +14,9 @@ Shipped demos live under `examples/00x-*/` with an `example.php` entry script. U ./phpc lint examples/002-StaticWeb/example.php ./phpc run examples/002-StaticWeb/example.php + +./phpc lint examples/004-ApiJson/example.php +./phpc run examples/004-ApiJson/example.php ``` AOT (needs LLVM 9 — see `script/install-llvm9.sh` or the `php-compiler:22.04-dev` Docker image): @@ -33,6 +36,7 @@ Legacy entrypoints still work: `php bin/vm.php`, `php bin/jit.php`, `php bin/com | [000-HelloWorld](000-HelloWorld/) | ✅ `./phpc run` | ✅ `bin/jit.php` | optional | no superglobals | | [001-SimpleWeb](001-SimpleWeb/) | ✅ `-q` / `-p` / env / `phpc serve` | ✅ `bin/jit.php` | ✅ `phpc build` | runtime `QUERY_STRING` / POST ([#201](https://github.com/PurHur/php-compiler/issues/201), [#257](https://github.com/PurHur/php-compiler/issues/257), [#259](https://github.com/PurHur/php-compiler/issues/259)) | | [002-StaticWeb](002-StaticWeb/) | ✅ `./phpc run` | ✅ `bin/jit.php` | ✅ recommended | no superglobals — [#247](https://github.com/PurHur/php-compiler/issues/247) execute smoke | +| [004-ApiJson](004-ApiJson/) | ✅ `./phpc run` | ✅ `bin/jit.php` | ✅ `phpc build` | JSON + `http_response_code` — [#270](https://github.com/PurHur/php-compiler/issues/270), [#61](https://github.com/PurHur/php-compiler/issues/61) | ### 000-HelloWorld @@ -78,7 +82,7 @@ cd examples/002-StaticWeb ## `phpc.json` (web examples) -**001-SimpleWeb** and **002-StaticWeb** ship a minimal manifest beside `example.php` ([#274](https://github.com/PurHur/php-compiler/issues/274)): +**001-SimpleWeb**, **002-StaticWeb**, and **004-ApiJson** ship a minimal manifest beside `example.php` ([#274](https://github.com/PurHur/php-compiler/issues/274)): ```json { diff --git a/ext/standard/JitJsonEncode.php b/ext/standard/JitJsonEncode.php new file mode 100644 index 00000000..b05a472a --- /dev/null +++ b/ext/standard/JitJsonEncode.php @@ -0,0 +1,36 @@ +type) { + return $context->builder->call( + $context->lookupFunction('__compiler_json_encode_hashtable'), + $context->helper->loadValue($arg) + ); + } + if (JITVariable::TYPE_VALUE === $arg->type) { + $boxed = $context->helper->loadValue($arg); + $ht = $context->builder->call( + $context->lookupFunction('__value__readHashtable'), + $boxed + ); + + return $context->builder->call( + $context->lookupFunction('__compiler_json_encode_hashtable'), + $ht + ); + } + + throw new \LogicException('json_encode() only supports arrays in this compiler build'); + } +} diff --git a/ext/standard/Module.php b/ext/standard/Module.php index fbd47170..d09f68c4 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -110,6 +110,7 @@ public function getFunctions(): array new header_list(), new getallheaders_(), new http_response_code(), + new json_encode(), new urlencode(), new rawurlencode(), new urldecode(), diff --git a/ext/standard/VmJson.php b/ext/standard/VmJson.php new file mode 100644 index 00000000..a7ceb339 --- /dev/null +++ b/ext/standard/VmJson.php @@ -0,0 +1,47 @@ +resolveIndirect(); + switch ($v->type) { + case Variable::TYPE_NULL: + return null; + case Variable::TYPE_INTEGER: + return $v->toInt(); + case Variable::TYPE_FLOAT: + return $v->toFloat(); + case Variable::TYPE_BOOLEAN: + return $v->toBool(); + case Variable::TYPE_STRING: + return $v->toString(); + case Variable::TYPE_ARRAY: + $out = []; + foreach ($v->toArray()->iterateKeyed(true) as [$key, $value]) { + $k = $key->resolveIndirect(); + if (Variable::TYPE_STRING !== $k->type) { + throw new \LogicException( + 'json_encode() only supports string keys in this compiler build' + ); + } + $out[$k->toString()] = self::export($value); + } + + return $out; + default: + throw new \LogicException( + 'json_encode() value type not supported in this compiler build' + ); + } + } +} diff --git a/ext/standard/json_encode.php b/ext/standard/json_encode.php new file mode 100644 index 00000000..74a8acb8 --- /dev/null +++ b/ext/standard/json_encode.php @@ -0,0 +1,55 @@ +calledArgs); + if ($argc < 1) { + throw new \LogicException('json_encode() requires at least one argument'); + } + if (null === $frame->returnVar) { + return; + } + if ($argc > 1) { + throw new \LogicException('json_encode() flags not supported in this compiler build'); + } + $value = VmJson::export($frame->calledArgs[0]->resolveIndirect()); + $encoded = \json_encode($value); + if (false === $encoded) { + throw new \LogicException('json_encode() failed'); + } + $frame->returnVar->string($encoded); + } + + public function call(Context $context, JITVariable ...$args): Value + { + if (\count($args) < 1) { + throw new \LogicException('json_encode() requires at least one argument'); + } + if (\count($args) > 1) { + throw new \LogicException('json_encode() flags not supported in this compiler build'); + } + + return JitJsonEncode::encode($context, $args[0]); + } +} diff --git a/lib/JIT/Builtin/StringJsonEncode.php b/lib/JIT/Builtin/StringJsonEncode.php new file mode 100644 index 00000000..0d402c7d --- /dev/null +++ b/lib/JIT/Builtin/StringJsonEncode.php @@ -0,0 +1,327 @@ +lookupFunction('__compiler_json_encode_hashtable'); + $entry = $fn->appendBasicBlock('je_entry'); + $context->builder->positionAtEnd($entry); + + $ht = $fn->getParam(0); + $htMap = $context->structFieldMap['__hashtable__']; + $nodeMap = $context->structFieldMap['__strkey_node__']; + $valMap = $context->structFieldMap['__value__']; + $strMap = $context->structFieldMap['__string__']; + $nodePtrType = $context->getTypeFromString('__strkey_node__*'); + $i8 = $context->getTypeFromString('int8'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $i8p = $context->getTypeFromString('int8*'); + $strPtr = $context->getTypeFromString('__string__*'); + $zeroI64 = $i64->constInt(0, false); + $oneI64 = $i64->constInt(1, false); + $twoI64 = $i64->constInt(2, false); + + $sizeT = $context->getTypeFromString('size_t'); + $zeroSize = $sizeT->constInt(0, false); + $oneSize = $sizeT->constInt(1, false); + $ptrSize = $sizeT->constInt(8, false); + + $countSlot = $context->builder->alloca($sizeT, 1, 'je_count'); + $walkSlot = $context->builder->alloca($nodePtrType, 1, 'je_walk'); + $idxSlot = $context->builder->alloca($sizeT, 1, 'je_fill_idx'); + $emitIdxSlot = $context->builder->alloca($sizeT, 1, 'je_emit_idx'); + $firstSlot = $context->builder->alloca($i8, 1, 'je_first'); + $resultSlot = $context->builder->alloca($strPtr, 1, 'je_acc'); + $finalSlot = $context->builder->alloca($strPtr, 1, 'je_final'); + $nodesSlot = $context->builder->alloca($nodePtrType->pointerType(0), 1, 'je_nodes_buf'); + + $head = $context->builder->load($context->builder->structGep($ht, $htMap['strKeys'])); + $isEmpty = $context->builder->icmp(Builder::INT_EQ, $head, $nodePtrType->constNull()); + $bbEmpty = $fn->appendBasicBlock('je_empty'); + $bbWork = $fn->appendBasicBlock('je_work'); + $bbReturn = $fn->appendBasicBlock('je_return'); + $context->builder->branchIf($isEmpty, $bbEmpty, $bbWork); + + $context->builder->positionAtEnd($bbEmpty); + $context->builder->store(self::literalString($context, '{}'), $finalSlot); + $context->builder->branch($bbReturn); + + $context->builder->positionAtEnd($bbWork); + $context->builder->store($zeroSize, $countSlot); + $context->builder->store($head, $walkSlot); + $countHead = $fn->appendBasicBlock('je_count_head'); + $countBody = $fn->appendBasicBlock('je_count_body'); + $countDone = $fn->appendBasicBlock('je_count_done'); + $context->builder->branch($countHead); + $context->builder->positionAtEnd($countHead); + $walkNode = $context->builder->load($walkSlot); + $walkEnd = $context->builder->icmp(Builder::INT_EQ, $walkNode, $nodePtrType->constNull()); + $context->builder->branchIf($walkEnd, $countDone, $countBody); + $context->builder->positionAtEnd($countBody); + $count = $context->builder->load($countSlot); + $context->builder->store($context->builder->addNoSignedWrap($count, $oneSize), $countSlot); + $nextWalk = $context->builder->load($context->builder->structGep($walkNode, $nodeMap['next'])); + $context->builder->store($nextWalk, $walkSlot); + $context->builder->branch($countHead); + $context->builder->positionAtEnd($countDone); + $numKeys = $context->builder->load($countSlot); + $bytes = $context->builder->mulNoSignedWrap($numKeys, $ptrSize); + $nodesRaw = $context->builder->call($context->lookupFunction('malloc'), $bytes); + $nodesArray = $context->builder->pointerCast($nodesRaw, $nodePtrType->pointerType(0)); + $context->builder->store($nodesArray, $nodesSlot); + $context->builder->store($zeroSize, $idxSlot); + $context->builder->store($head, $walkSlot); + $fillHead = $fn->appendBasicBlock('je_fill_head'); + $fillBody = $fn->appendBasicBlock('je_fill_body'); + $fillDone = $fn->appendBasicBlock('je_fill_done'); + $context->builder->branch($fillHead); + $context->builder->positionAtEnd($fillHead); + $fillNode = $context->builder->load($walkSlot); + $fillEnd = $context->builder->icmp(Builder::INT_EQ, $fillNode, $nodePtrType->constNull()); + $context->builder->branchIf($fillEnd, $fillDone, $fillBody); + $context->builder->positionAtEnd($fillBody); + $idx = $context->builder->load($idxSlot); + $nodesArray = $context->builder->load($nodesSlot); + $slotPtr = $context->builder->inBoundsGEP($nodesArray, $idx); + $context->builder->store($fillNode, $slotPtr); + $context->builder->store($context->builder->addNoSignedWrap($idx, $oneSize), $idxSlot); + $nextFill = $context->builder->load($context->builder->structGep($fillNode, $nodeMap['next'])); + $context->builder->store($nextFill, $walkSlot); + $context->builder->branch($fillHead); + $context->builder->positionAtEnd($fillDone); + + $openBrace = self::literalString($context, '{'); + $context->builder->store($openBrace, $resultSlot); + $context->builder->store($i8->constInt(1, false), $firstSlot); + $context->builder->store($numKeys, $emitIdxSlot); + + $emitHead = $fn->appendBasicBlock('je_emit_head'); + $emitBody = $fn->appendBasicBlock('je_emit_body'); + $emitDone = $fn->appendBasicBlock('je_emit_done'); + $context->builder->branch($emitHead); + $context->builder->positionAtEnd($emitHead); + $emitIdx = $context->builder->load($emitIdxSlot); + $emitEnd = $context->builder->icmp(Builder::INT_EQ, $emitIdx, $zeroSize); + $context->builder->branchIf($emitEnd, $emitDone, $emitBody); + + $context->builder->positionAtEnd($emitBody); + $prevIdx = $context->builder->subNoSignedWrap($emitIdx, $oneSize); + $context->builder->store($prevIdx, $emitIdxSlot); + $nodesArray = $context->builder->load($nodesSlot); + $nodePtr = $context->builder->load($context->builder->inBoundsGEP($nodesArray, $prevIdx)); + + $acc = $context->builder->load($resultSlot); + $isFirst = $context->builder->load($firstSlot); + $notFirst = $context->builder->icmp(Builder::INT_EQ, $isFirst, $i8->constInt(0, false)); + $bbComma = $fn->appendBasicBlock('je_comma'); + $bbAfterComma = $fn->appendBasicBlock('je_after_comma'); + $context->builder->branchIf($notFirst, $bbComma, $bbAfterComma); + $context->builder->positionAtEnd($bbComma); + $comma = self::literalString($context, ','); + $acc = JitStringConcat::concat($context, $acc, $comma); + $context->builder->store($acc, $resultSlot); + $context->builder->branch($bbAfterComma); + $context->builder->positionAtEnd($bbAfterComma); + $context->builder->store($i8->constInt(0, false), $firstSlot); + + $nodeKey = $context->builder->load($context->builder->structGep($nodePtr, $nodeMap['key'])); + $quotedKey = self::quoteString($context, $nodeKey); + $acc = $context->builder->load($resultSlot); + $acc = JitStringConcat::concat($context, $acc, $quotedKey); + $colon = self::literalString($context, ':'); + $acc = JitStringConcat::concat($context, $acc, $colon); + $valPtr = $context->builder->structGep($nodePtr, $nodeMap['value']); + $afterVal = $fn->appendBasicBlock('je_after_val'); + $encodedVal = self::encodeValue( + $context, + $fn, + $valPtr, + $valMap, + $strMap, + $i8, + $i32, + $i64, + $i8p, + $zeroI64, + $afterVal + ); + $context->builder->positionAtEnd($afterVal); + $acc = JitStringConcat::concat($context, $acc, $encodedVal); + $context->builder->store($acc, $resultSlot); + $context->builder->branch($emitHead); + + $context->builder->positionAtEnd($emitDone); + $nodesArray = $context->builder->load($nodesSlot); + $nodesRaw = $context->builder->pointerCast($nodesArray, $context->getTypeFromString('int8*')); + $context->builder->call($context->lookupFunction('free'), $nodesRaw); + $closeBrace = self::literalString($context, '}'); + $acc = $context->builder->load($resultSlot); + $workResult = JitStringConcat::concat($context, $acc, $closeBrace); + $context->builder->store($workResult, $finalSlot); + $context->builder->branch($bbReturn); + + $context->builder->positionAtEnd($bbReturn); + $context->builder->returnValue($context->builder->load($finalSlot)); + $context->builder->clearInsertionPosition(); + } + + private static function literalString(Context $context, string $text): Value + { + $i8p = $context->getTypeFromString('int8*'); + $len = $context->getTypeFromString('int64')->constInt(strlen($text), false); + + return $context->builder->call( + $context->lookupFunction('__string__init'), + $len, + $context->builder->pointerCast($context->constantFromString($text), $i8p) + ); + } + + private static function quoteString(Context $context, Value $str): Value + { + $open = self::literalString($context, '"'); + $close = self::literalString($context, '"'); + $quoted = JitStringConcat::concat($context, $open, $str); + + return JitStringConcat::concat($context, $quoted, $close); + } + + /** + * @param array $valMap + * @param array $strMap + */ + private static function encodeValue( + Context $context, + \PHPLLVM\Value\Function_ $fn, + Value $valPtr, + array $valMap, + array $strMap, + \PHPLLVM\Type $i8, + \PHPLLVM\Type $i32, + \PHPLLVM\Type $i64, + \PHPLLVM\Type $i8p, + Value $zeroI64, + \PHPLLVM\BasicBlock $resumeBlock + ): Value { + $strPtr = $context->getTypeFromString('__string__*'); + $bbEntry = $fn->appendBasicBlock('je_val_entry'); + $context->builder->branch($bbEntry); + $context->builder->positionAtEnd($bbEntry); + + $resultSlot = $context->builder->alloca($strPtr, 1, 'je_val_out'); + $numBuf = $context->builder->alloca($i8, $i64->constInt(32, false), 'je_numbuf'); + $typeByte = $context->builder->load( + $context->builder->structGep($valPtr, $valMap['type']) + ); + $nullType = $i8->constInt(Variable::TYPE_NULL, false); + $longType = $i8->constInt(Variable::TYPE_NATIVE_LONG, false); + $boolType = $i8->constInt(Variable::TYPE_NATIVE_BOOL, false); + $stringType = $i8->constInt(Variable::TYPE_STRING & 0xff, false); + + $bbNull = $fn->appendBasicBlock('je_val_null'); + $bbCheckLong = $fn->appendBasicBlock('je_val_check_long'); + $bbLong = $fn->appendBasicBlock('je_val_long'); + $bbCheckBool = $fn->appendBasicBlock('je_val_check_bool'); + $bbBool = $fn->appendBasicBlock('je_val_bool'); + $bbCheckString = $fn->appendBasicBlock('je_val_check_string'); + $bbString = $fn->appendBasicBlock('je_val_string'); + $bbDefault = $fn->appendBasicBlock('je_val_default'); + $bbDone = $fn->appendBasicBlock('je_val_done'); + + $isNull = $context->builder->icmp(Builder::INT_EQ, $typeByte, $nullType); + $context->builder->branchIf($isNull, $bbNull, $bbCheckLong); + + $context->builder->positionAtEnd($bbNull); + $context->builder->store(self::literalString($context, 'null'), $resultSlot); + $context->builder->branch($bbDone); + + $context->builder->positionAtEnd($bbCheckLong); + $isLong = $context->builder->icmp(Builder::INT_EQ, $typeByte, $longType); + $context->builder->branchIf($isLong, $bbLong, $bbCheckBool); + + $context->builder->positionAtEnd($bbCheckBool); + $isBool = $context->builder->icmp(Builder::INT_EQ, $typeByte, $boolType); + $context->builder->branchIf($isBool, $bbBool, $bbCheckString); + + $context->builder->positionAtEnd($bbCheckString); + $isStr = $context->builder->icmp(Builder::INT_EQ, $typeByte, $stringType); + $context->builder->branchIf($isStr, $bbString, $bbDefault); + + $context->builder->positionAtEnd($bbDefault); + $context->builder->store(self::literalString($context, 'null'), $resultSlot); + $context->builder->branch($bbDone); + + $context->builder->positionAtEnd($bbLong); + $num = $context->builder->call( + $context->lookupFunction('__value__readLong'), + $valPtr + ); + $bufC = $context->builder->pointerCast($numBuf, $i8p); + $fmt = $context->builder->pointerCast( + $context->constantFromString('%lld'), + $i8p + ); + $context->builder->call($context->lookupFunction('sprintf'), $bufC, $fmt, $num); + $len = $context->builder->call($context->lookupFunction('strlen'), $bufC); + $lenI64 = $len->typeOf() === $i64 + ? $len + : $context->builder->zExt($len, $i64); + $context->builder->store( + $context->builder->call( + $context->lookupFunction('__string__init'), + $lenI64, + $bufC + ), + $resultSlot + ); + $context->builder->branch($bbDone); + + $context->builder->positionAtEnd($bbBool); + $valueField = $context->builder->structGep($valPtr, $valMap['value']); + $boolByte = $context->builder->load( + $context->builder->inBoundsGEP( + $valueField, + $i32->constInt(0, false), + $zeroI64 + ) + ); + $isTrue = $context->builder->icmp(Builder::INT_NE, $boolByte, $i8->constInt(0, false)); + $boolStr = $context->builder->select( + $isTrue, + self::literalString($context, 'true'), + self::literalString($context, 'false') + ); + $context->builder->store($boolStr, $resultSlot); + $context->builder->branch($bbDone); + + $context->builder->positionAtEnd($bbString); + $raw = $context->builder->call( + $context->lookupFunction('__value__readString'), + $valPtr + ); + $context->builder->store(self::quoteString($context, $raw), $resultSlot); + $context->builder->branch($bbDone); + + $context->builder->positionAtEnd($bbDone); + $result = $context->builder->load($resultSlot); + $context->builder->branch($resumeBlock); + + return $result; + } +} diff --git a/lib/JIT/Builtin/Type.php b/lib/JIT/Builtin/Type.php index 2d748e58..0b3f2e7e 100755 --- a/lib/JIT/Builtin/Type.php +++ b/lib/JIT/Builtin/Type.php @@ -123,6 +123,13 @@ public function register(): void { $this->context->registerFunction($libcName, $fn); } $this->hashtable->register(); + $fntypeJsonEncode = $this->context->context->functionType( + $this->context->getTypeFromString('__string__*'), + false, + $this->context->getTypeFromString('__hashtable__*') + ); + $fnJsonEncode = $this->context->module->addFunction('__compiler_json_encode_hashtable', $fntypeJsonEncode); + $this->context->registerFunction('__compiler_json_encode_hashtable', $fnJsonEncode); // $this->maskedarray->register(); // $this->nativearray->register(); } diff --git a/lib/JIT/Builtin/Type/HashTable.php b/lib/JIT/Builtin/Type/HashTable.php index 4dd4a0dd..6f89b2a6 100755 --- a/lib/JIT/Builtin/Type/HashTable.php +++ b/lib/JIT/Builtin/Type/HashTable.php @@ -71,6 +71,7 @@ public function register(): void $this->registerFn('__hashtable__setStringKeyString', 'void', ['__hashtable__*', '__string__*', '__string__*']); $this->registerFn('__hashtable__setStringKeyHashtable', 'void', ['__hashtable__*', '__string__*', '__hashtable__*']); $this->registerFn('__hashtable__setStringKeyLong', 'void', ['__hashtable__*', '__string__*', 'int64']); + $this->registerFn('__hashtable__setStringKeyBool', 'void', ['__hashtable__*', '__string__*', 'int1']); $this->registerFn('__hashtable__offsetIsSetStringKey', 'int1', ['__hashtable__*', '__string__*']); $this->registerFn('__hashtable__readStringKeyValue', '__value__*', ['__hashtable__*', '__string__*']); $this->registerFn('__hashtable__readStringKeyHashtable', '__hashtable__*', ['__hashtable__*', '__string__*']); @@ -109,6 +110,8 @@ public function implement(): void $this->implementGetNumElements(); $this->implementOffsetIsSet(); $this->implementSetStringKeyString(); + $this->implementSetStringKeyLong(); + $this->implementSetStringKeyBool(); $this->implementSetStringKeyHashtable(); $this->implementOffsetIsSetStringKey(); $this->implementReadStringKeyValue(); @@ -717,6 +720,101 @@ private function implementSetStringKeyLong(): void $this->context->builder->returnVoid(); } + private function implementSetStringKeyBool(): void + { + $fn = $this->context->lookupFunction('__hashtable__setStringKeyBool'); + $block = $fn->appendBasicBlock('main'); + $this->context->builder->positionAtEnd($block); + $ht = $fn->getParam(0); + $key = $fn->getParam(1); + $bool = $fn->getParam(2); + + $htMap = $this->context->structFieldMap['__hashtable__']; + $nodeMap = $this->context->structFieldMap['__strkey_node__']; + $valMap = $this->context->structFieldMap['__value__']; + $headSlot = $this->context->builder->structGep($ht, $htMap['strKeys']); + $head = $this->context->builder->load($headSlot); + + $done = $fn->appendBasicBlock('strkey_bool_done'); + $prepend = $fn->appendBasicBlock('strkey_bool_prepend'); + $loopHead = $fn->appendBasicBlock('strkey_bool_head'); + $loopBody = $fn->appendBasicBlock('strkey_bool_body'); + $this->context->builder->branch($loopHead); + + $this->context->builder->positionAtEnd($loopHead); + $node = $this->context->builder->phi($head->typeOf()); + $node->addIncoming($head, $block); + $isNull = $this->context->builder->icmp(Builder::INT_EQ, $node, $node->typeOf()->constNull()); + $this->context->builder->branchIf($isNull, $prepend, $loopBody); + + $this->context->builder->positionAtEnd($loopBody); + $nodeKey = $this->context->builder->load($this->context->builder->structGep($node, $nodeMap['key'])); + $cmp = $this->context->builder->call( + $this->context->lookupFunction('strcmp'), + $this->stringDataPtr($key), + $this->stringDataPtr($nodeKey) + ); + $isMatch = $this->context->builder->icmp(Builder::INT_EQ, $cmp, $cmp->typeOf()->constInt(0, false)); + $update = $fn->appendBasicBlock('strkey_bool_update'); + $next = $fn->appendBasicBlock('strkey_bool_next'); + $this->context->builder->branchIf($isMatch, $update, $next); + + $this->context->builder->positionAtEnd($update); + $valField = $this->context->builder->structGep($node, $nodeMap['value']); + $this->writeBoolToValueField($valField, $bool); + $this->context->builder->branch($done); + + $this->context->builder->positionAtEnd($next); + $nextNode = $this->context->builder->load($this->context->builder->structGep($node, $nodeMap['next'])); + $this->context->builder->branch($loopHead); + $node->addIncoming($nextNode, $next); + + $this->context->builder->positionAtEnd($prepend); + $nodeType = $this->context->getTypeFromString('__strkey_node__'); + $newNode = $this->context->memory->malloc($nodeType); + $typeinfo = $this->context->getTypeFromString('int32')->constInt( + Refcount::TYPE_INFO_TYPE_STRING | Refcount::TYPE_INFO_REFCOUNTED, + false + ); + $ref = $this->context->builder->pointerCast( + $newNode, + $this->context->getTypeFromString('__ref__virtual*') + ); + $this->context->builder->call($this->context->lookupFunction('__ref__init'), $typeinfo, $ref); + $storedKey = $this->context->builder->call($this->context->lookupFunction('__string__separate'), $key); + $this->context->builder->store($storedKey, $this->context->builder->structGep($newNode, $nodeMap['key'])); + $this->writeBoolToValueField( + $this->context->builder->structGep($newNode, $nodeMap['value']), + $bool + ); + $this->context->builder->store($head, $this->context->builder->structGep($newNode, $nodeMap['next'])); + $this->context->builder->store($newNode, $headSlot); + $this->context->builder->branch($done); + + $this->context->builder->positionAtEnd($done); + $this->context->builder->returnVoid(); + } + + private function writeBoolToValueField(\PHPLLVM\Value $valField, \PHPLLVM\Value $bool): void + { + $valMap = $this->context->structFieldMap['__value__']; + $i8 = $this->context->getTypeFromString('int8'); + $i32 = $this->context->getTypeFromString('int32'); + $i64 = $this->context->getTypeFromString('int64'); + $this->context->builder->store( + $i8->constInt(Variable::TYPE_NATIVE_BOOL, false), + $this->context->builder->structGep($valField, $valMap['type']) + ); + $boolByte = $this->context->builder->zExt($bool, $i8); + $valueField = $this->context->builder->structGep($valField, $valMap['value']); + $firstByte = $this->context->builder->inBoundsGEP( + $valueField, + $i32->constInt(0, false), + $i64->constInt(0, false) + ); + $this->context->builder->store($boolByte, $firstByte); + } + private function implementOffsetIsSetStringKey(): void { $fn = $this->context->lookupFunction('__hashtable__offsetIsSetStringKey'); diff --git a/lib/JIT/Builtin/Type/String_.php b/lib/JIT/Builtin/Type/String_.php index 3669542f..c31b247a 100644 --- a/lib/JIT/Builtin/Type/String_.php +++ b/lib/JIT/Builtin/Type/String_.php @@ -216,6 +216,7 @@ public function implement(): void { \PHPCompiler\JIT\Builtin\StringNl2br::implement($this->context); \PHPCompiler\JIT\Builtin\StringUcwords::implement($this->context); \PHPCompiler\JIT\Builtin\StringRandomBytes::implement($this->context); + \PHPCompiler\JIT\Builtin\StringJsonEncode::implement($this->context); \PHPCompiler\JIT\Builtin\StringGetenv::implement($this->context); \PHPCompiler\JIT\Builtin\StringDateTime::implement($this->context); } diff --git a/lib/JIT/HashTableHelper.php b/lib/JIT/HashTableHelper.php index 6af86dc6..43b3efdb 100644 --- a/lib/JIT/HashTableHelper.php +++ b/lib/JIT/HashTableHelper.php @@ -244,6 +244,22 @@ private static function setAtStringKey( $context->helper->loadValue($element) ); break; + case Variable::TYPE_NATIVE_LONG: + $context->builder->call( + $context->lookupFunction('__hashtable__setStringKeyLong'), + $ht, + $keyPtr, + $context->helper->loadValue($element) + ); + break; + case Variable::TYPE_NATIVE_BOOL: + $context->builder->call( + $context->lookupFunction('__hashtable__setStringKeyBool'), + $ht, + $keyPtr, + $context->helper->loadValue($element) + ); + break; default: throw new \LogicException( 'String-key array element type not supported for JIT: ' diff --git a/test/compliance/cases/stdlib/json_encode.phpt b/test/compliance/cases/stdlib/json_encode.phpt new file mode 100644 index 00000000..2519a9ce --- /dev/null +++ b/test/compliance/cases/stdlib/json_encode.phpt @@ -0,0 +1,7 @@ +--TEST-- +stdlib json_encode() for assoc array (issue #61) +--FILE-- + true, 'n' => 1, 'msg' => 'hi']); +--EXPECT-- +{"ok":true,"n":1,"msg":"hi"} diff --git a/test/compliance/cases/stdlib/json_encode_jit.phpt b/test/compliance/cases/stdlib/json_encode_jit.phpt new file mode 100644 index 00000000..9636fa4c --- /dev/null +++ b/test/compliance/cases/stdlib/json_encode_jit.phpt @@ -0,0 +1,7 @@ +--TEST-- +stdlib json_encode() JIT (issue #61) +--FILE-- + true, 'service' => 'php-compiler']); +--EXPECT-- +{"ok":true,"service":"php-compiler"} diff --git a/test/real/cases/json_encode_api.phpt b/test/real/cases/json_encode_api.phpt new file mode 100644 index 00000000..cf9ec911 --- /dev/null +++ b/test/real/cases/json_encode_api.phpt @@ -0,0 +1,9 @@ +--TEST-- +Web: json_encode API body (issue #61, #270) +--FILE-- + true, 'service' => 'php-compiler']); +--EXPECT-- +{"ok":true,"service":"php-compiler"} diff --git a/test/unit/ExamplesCompileTest.php b/test/unit/ExamplesCompileTest.php index f204dc8c..79e19596 100644 --- a/test/unit/ExamplesCompileTest.php +++ b/test/unit/ExamplesCompileTest.php @@ -84,6 +84,7 @@ public static function provideWebExampleManifestDirs(): array return [ '001-SimpleWeb' => [$root.'/001-SimpleWeb'], '002-StaticWeb' => [$root.'/002-StaticWeb'], + '004-ApiJson' => [$root.'/004-ApiJson'], ]; } @@ -485,6 +486,7 @@ private static function smokeNeedles(string $exampleName): array '000-HelloWorld' => ['Hello World'], '001-SimpleWeb' => ['Hello Example'], '002-StaticWeb' => ['Hello World'], + '004-ApiJson' => ['"ok":true', 'php-compiler'], default => ['Hello'], }; }