diff --git a/docs/capabilities.md b/docs/capabilities.md
index 704acb203..5d718cb85 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -44,7 +44,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand.
| `header` | yes | yes | yes | standard | AOT PHPT |
| `hexdec` | yes | yes | yes | standard | |
| `htmlspecialchars` | yes | yes | yes | standard | AOT PHPT |
-| `implode` | yes | yes | yes | standard | doc: VM only; JIT PHPT; AOT PHPT |
+| `implode` | yes | yes | yes | standard | doc: VM only; AOT PHPT |
| `in_array` | yes | yes | yes | standard | doc: VM only; JIT PHPT |
| `intdiv` | yes | yes | yes | standard | |
| `intval` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
@@ -86,7 +86,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand.
| `scandir` | yes | yes | yes | standard | |
| `sin` | yes | yes | yes | standard | |
| `sizeof` | yes | yes | yes | standard | |
-| `sort` | yes | yes | yes | standard | JIT PHPT; AOT PHPT |
+| `sort` | yes | yes | yes | standard | |
| `sqrt` | yes | yes | yes | standard | |
| `str_contains` | yes | yes | yes | standard | |
| `str_ends_with` | yes | yes | yes | standard | AOT PHPT |
diff --git a/lib/AOT/runtime/superglobals_refresh.c b/lib/AOT/runtime/superglobals_refresh.c
index aaa513229..654d4cb66 100644
--- a/lib/AOT/runtime/superglobals_refresh.c
+++ b/lib/AOT/runtime/superglobals_refresh.c
@@ -12,6 +12,10 @@ typedef struct __string__ __string__;
extern __hashtable__ *__hashtable__alloc(void);
extern void __hashtable__setStringKeyString(__hashtable__ *ht, __string__ *key, __string__ *val);
+extern void __hashtable__setStringKeyHashtable(__hashtable__ *ht, __string__ *key, __hashtable__ *child);
+extern void __hashtable__setStringAt(__hashtable__ *ht, size_t index, __string__ *val);
+extern size_t __hashtable__getNumElements(__hashtable__ *ht);
+extern __hashtable__ *__hashtable__readStringKeyHashtable(__hashtable__ *ht, __string__ *key);
extern __string__ *__string__init(long long size, const char *value);
extern __hashtable__ *sg_GET;
@@ -38,6 +42,161 @@ static void set_string_key(__hashtable__ *ht, const char *key, const char *value
__hashtable__setStringKeyString(ht, k, v);
}
+#define SG_MAX_KEY_PARTS 16
+
+typedef struct {
+ char *parts[SG_MAX_KEY_PARTS];
+ size_t count;
+ int append_list;
+} sg_parsed_key;
+
+static int sg_is_hex(char c)
+{
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+}
+
+static int sg_hex_value(char c)
+{
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+
+ return c - 'A' + 10;
+}
+
+static void sg_url_decode_inplace(char *s)
+{
+ char *w = s;
+
+ for (char *r = s; '\0' != *r; r++) {
+ if ('+' == *r) {
+ *w++ = ' ';
+ } else if ('%' == *r && sg_is_hex(r[1]) && sg_is_hex(r[2])) {
+ *w++ = (char) (sg_hex_value(r[1]) * 16 + sg_hex_value(r[2]));
+ r += 2;
+ } else {
+ *w++ = *r;
+ }
+ }
+ *w = '\0';
+}
+
+static void sg_free_parsed_key(sg_parsed_key *pk)
+{
+ size_t i;
+
+ for (i = 0; i < pk->count; i++) {
+ free(pk->parts[i]);
+ pk->parts[i] = NULL;
+ }
+ pk->count = 0;
+ pk->append_list = 0;
+}
+
+static int sg_parse_key_brackets(const char *raw, sg_parsed_key *out)
+{
+ const char *p = raw;
+ size_t base_len;
+
+ out->count = 0;
+ out->append_list = 0;
+ if ('\0' == raw[0]) {
+ return -1;
+ }
+
+ base_len = strcspn(p, "[");
+ if (base_len > 0) {
+ out->parts[out->count] = strndup(p, base_len);
+ if (NULL == out->parts[out->count]) {
+ return -1;
+ }
+ out->count++;
+ p += base_len;
+ }
+
+ while ('[' == *p) {
+ p++;
+ if (']' == *p) {
+ out->append_list = 1;
+ p++;
+ break;
+ }
+ {
+ const char *close = strchr(p, ']');
+ size_t len;
+
+ if (NULL == close) {
+ return -1;
+ }
+ len = (size_t) (close - p);
+ out->parts[out->count] = malloc(len + 1);
+ if (NULL == out->parts[out->count]) {
+ return -1;
+ }
+ memcpy(out->parts[out->count], p, len);
+ out->parts[out->count][len] = '\0';
+ out->count++;
+ p = close + 1;
+ }
+ if ('[' == *p && ']' == p[1]) {
+ out->append_list = 1;
+ p += 2;
+ }
+ }
+
+ if ('\0' != *p || 0 == out->count) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static __hashtable__ *sg_ensure_child(__hashtable__ *ht, const char *key)
+{
+ __string__ *k = cstr_to_string(key);
+ __hashtable__ *child = __hashtable__readStringKeyHashtable(ht, k);
+
+ if (NULL != child) {
+ return child;
+ }
+ child = __hashtable__alloc();
+ __hashtable__setStringKeyHashtable(ht, k, child);
+
+ return child;
+}
+
+static void sg_set_nested_value(__hashtable__ *root, sg_parsed_key *pk, const char *value)
+{
+ __hashtable__ *ht = root;
+ size_t last;
+ const char *leaf;
+
+ if (0 == pk->count) {
+ return;
+ }
+ last = pk->count;
+ {
+ size_t i;
+
+ for (i = 0; i + 1 < last; i++) {
+ ht = sg_ensure_child(ht, pk->parts[i]);
+ }
+ }
+ leaf = pk->parts[last - 1];
+ if (pk->append_list) {
+ __hashtable__ *list_ht = sg_ensure_child(ht, leaf);
+ size_t idx = __hashtable__getNumElements(list_ht);
+
+ __hashtable__setStringAt(list_ht, idx, cstr_to_string(value));
+
+ return;
+ }
+ set_string_key(ht, leaf, value);
+}
+
static void parse_form_encoded(__hashtable__ *ht, const char *body)
{
char *copy;
@@ -56,12 +215,30 @@ static void parse_form_encoded(__hashtable__ *ht, const char *body)
pair = strtok_r(copy, "&", &saveptr);
while (NULL != pair) {
char *eq = strchr(pair, '=');
+ char *raw_key;
+ char *raw_val;
+ sg_parsed_key pk;
+
if (NULL != eq) {
*eq = '\0';
- set_string_key(ht, pair, eq + 1);
- } else if ('\0' != pair[0]) {
- set_string_key(ht, pair, "");
+ raw_key = pair;
+ raw_val = eq + 1;
+ } else {
+ raw_key = pair;
+ raw_val = (char *) "";
+ }
+ if ('\0' == raw_key[0]) {
+ pair = strtok_r(NULL, "&", &saveptr);
+ continue;
+ }
+ sg_url_decode_inplace(raw_key);
+ sg_url_decode_inplace(raw_val);
+ if (0 == sg_parse_key_brackets(raw_key, &pk)) {
+ sg_set_nested_value(ht, &pk, raw_val);
+ } else {
+ set_string_key(ht, raw_key, raw_val);
}
+ sg_free_parsed_key(&pk);
pair = strtok_r(NULL, "&", &saveptr);
}
diff --git a/lib/JIT/Builtin/Type/HashTable.php b/lib/JIT/Builtin/Type/HashTable.php
index 2a61ad821..063bec7e8 100755
--- a/lib/JIT/Builtin/Type/HashTable.php
+++ b/lib/JIT/Builtin/Type/HashTable.php
@@ -69,9 +69,13 @@ public function register(): void
$this->registerFn('__hashtable__getNumElements', 'size_t', ['__hashtable__*']);
$this->registerFn('__hashtable__offsetIsSet', 'int1', ['__hashtable__*', 'size_t']);
$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__offsetIsSetStringKey', 'int1', ['__hashtable__*', '__string__*']);
$this->registerFn('__hashtable__readStringKeyValue', '__value__*', ['__hashtable__*', '__string__*']);
+ $this->registerFn('__hashtable__readStringKeyHashtable', '__hashtable__*', ['__hashtable__*', '__string__*']);
+ $this->registerFn('__value__readHashtable', '__hashtable__*', ['__value__*']);
+ $this->registerFn('__value__writeHashtable', 'void', ['__value__*', '__hashtable__*']);
$this->registerFn('__hashtable__sortPacked', 'void', ['__hashtable__*']);
$this->pointer = $this->context->getTypeFromString('__hashtable__*');
@@ -105,8 +109,12 @@ public function implement(): void
$this->implementGetNumElements();
$this->implementOffsetIsSet();
$this->implementSetStringKeyString();
+ $this->implementSetStringKeyHashtable();
$this->implementOffsetIsSetStringKey();
$this->implementReadStringKeyValue();
+ $this->implementReadStringKeyHashtable();
+ $this->implementValueReadHashtable();
+ $this->implementValueWriteHashtable();
$this->implementSortPacked();
}
@@ -550,6 +558,85 @@ private function implementSetStringKeyString(): void
$this->context->builder->returnVoid();
}
+ private function implementSetStringKeyHashtable(): void
+ {
+ $fn = $this->context->lookupFunction('__hashtable__setStringKeyHashtable');
+ $block = $fn->appendBasicBlock('main');
+ $this->context->builder->positionAtEnd($block);
+ $ht = $fn->getParam(0);
+ $key = $fn->getParam(1);
+ $child = $fn->getParam(2);
+
+ $htMap = $this->context->structFieldMap['__hashtable__'];
+ $nodeMap = $this->context->structFieldMap['__strkey_node__'];
+ $headSlot = $this->context->builder->structGep($ht, $htMap['strKeys']);
+ $head = $this->context->builder->load($headSlot);
+
+ $done = $fn->appendBasicBlock('strkey_ht_done');
+ $prepend = $fn->appendBasicBlock('strkey_ht_prepend');
+ $loopHead = $fn->appendBasicBlock('strkey_ht_head');
+ $loopBody = $fn->appendBasicBlock('strkey_ht_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_ht_update');
+ $next = $fn->appendBasicBlock('strkey_ht_next');
+ $this->context->builder->branchIf($isMatch, $update, $next);
+
+ $this->context->builder->positionAtEnd($update);
+ $valField = $this->context->builder->structGep($node, $nodeMap['value']);
+ $this->context->builder->call(
+ $this->context->lookupFunction('__value__writeHashtable'),
+ $valField,
+ $child
+ );
+ $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->context->builder->call(
+ $this->context->lookupFunction('__value__writeHashtable'),
+ $this->context->builder->structGep($newNode, $nodeMap['value']),
+ $child
+ );
+ $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 implementSetStringKeyLong(): void
{
$fn = $this->context->lookupFunction('__hashtable__setStringKeyLong');
@@ -669,6 +756,90 @@ private function implementReadStringKeyValue(): void
$this->context->builder->returnValue($valPtr);
}
+ private function implementReadStringKeyHashtable(): void
+ {
+ $fn = $this->context->lookupFunction('__hashtable__readStringKeyHashtable');
+ $block = $fn->appendBasicBlock('main');
+ $this->context->builder->positionAtEnd($block);
+ $ht = $fn->getParam(0);
+ $key = $fn->getParam(1);
+ $htPtr = $this->context->getTypeFromString('__hashtable__*');
+ $valPtr = $this->lookupStringKeyValue($fn, $block, $ht, $key);
+ $afterLookup = $fn->appendBasicBlock('strkey_read_ht_after_lookup');
+ $this->context->builder->branch($afterLookup);
+ $this->context->builder->positionAtEnd($afterLookup);
+ $isNull = $this->context->builder->icmp(Builder::INT_EQ, $valPtr, $valPtr->typeOf()->constNull());
+ $empty = $fn->appendBasicBlock('strkey_read_ht_empty');
+ $read = $fn->appendBasicBlock('strkey_read_ht_read');
+ $merge = $fn->appendBasicBlock('strkey_read_ht_merge');
+ $this->context->builder->branchIf($isNull, $empty, $read);
+ $this->context->builder->positionAtEnd($read);
+ $child = $this->context->builder->call(
+ $this->context->lookupFunction('__value__readHashtable'),
+ $valPtr
+ );
+ $this->context->builder->branch($merge);
+ $this->context->builder->positionAtEnd($empty);
+ $this->context->builder->branch($merge);
+ $this->context->builder->positionAtEnd($merge);
+ $result = $this->context->builder->phi($htPtr);
+ $result->addIncoming($child, $read);
+ $result->addIncoming($htPtr->constNull(), $empty);
+ $this->context->builder->returnValue($result);
+ }
+
+ private function implementValueReadHashtable(): void
+ {
+ $fn = $this->context->lookupFunction('__value__readHashtable');
+ $block = $fn->appendBasicBlock('main');
+ $this->context->builder->positionAtEnd($block);
+ $value = $fn->getParam(0);
+ $map = $this->context->structFieldMap['__value__'];
+ $htPtr = $this->context->getTypeFromString('__hashtable__*');
+ $typeByte = $this->context->builder->load($this->context->builder->structGep($value, $map['type']));
+ $expected = $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_HASHTABLE, false);
+ $isHt = $this->context->builder->icmp(Builder::INT_EQ, $typeByte, $expected);
+ $ok = $fn->appendBasicBlock('read_ht_ok');
+ $empty = $fn->appendBasicBlock('read_ht_empty');
+ $merge = $fn->appendBasicBlock('read_ht_merge');
+ $this->context->builder->branchIf($isHt, $ok, $empty);
+ $this->context->builder->positionAtEnd($ok);
+ $ptrField = $this->context->builder->structGep($value, $map['value']);
+ $htSlot = $this->context->builder->pointerCast($ptrField, $htPtr->pointerType(0));
+ $stored = $this->context->builder->load($htSlot);
+ $this->context->builder->branch($merge);
+ $this->context->builder->positionAtEnd($empty);
+ $this->context->builder->branch($merge);
+ $this->context->builder->positionAtEnd($merge);
+ $result = $this->context->builder->phi($htPtr);
+ $result->addIncoming($stored, $ok);
+ $result->addIncoming($htPtr->constNull(), $empty);
+ $this->context->builder->returnValue($result);
+ }
+
+ private function implementValueWriteHashtable(): void
+ {
+ $fn = $this->context->lookupFunction('__value__writeHashtable');
+ $block = $fn->appendBasicBlock('main');
+ $this->context->builder->positionAtEnd($block);
+ $value = $fn->getParam(0);
+ $hashtable = $fn->getParam(1);
+ $map = $this->context->structFieldMap['__value__'];
+ $htPtr = $this->context->getTypeFromString('__hashtable__*');
+ $this->context->builder->call(
+ $this->context->lookupFunction('__value__valueDelref'),
+ $value
+ );
+ $this->context->builder->store(
+ $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_HASHTABLE, false),
+ $this->context->builder->structGep($value, $map['type'])
+ );
+ $ptrField = $this->context->builder->structGep($value, $map['value']);
+ $htSlot = $this->context->builder->pointerCast($ptrField, $htPtr->pointerType(0));
+ $this->context->builder->store($hashtable, $htSlot);
+ $this->context->builder->returnVoid();
+ }
+
private function lookupStringKeyValue(
PHPLLVM\Value\Function_ $fn,
PHPLLVM\BasicBlock $block,
diff --git a/lib/JIT/SuperglobalInit.php b/lib/JIT/SuperglobalInit.php
index 39a3f3bac..e529f1f65 100644
--- a/lib/JIT/SuperglobalInit.php
+++ b/lib/JIT/SuperglobalInit.php
@@ -54,19 +54,52 @@ private static function populateFromVm(Context $context, \PHPLLVM\Value $ht, str
if (!$table instanceof HashTable) {
return;
}
+ self::populateHashTableFromVm($context, $ht, $table);
+ }
+
+ private static function populateHashTableFromVm(
+ Context $context,
+ \PHPLLVM\Value $ht,
+ HashTable $table
+ ): void {
$setString = $context->lookupFunction('__hashtable__setStringKeyString');
+ $setHashtable = $context->lookupFunction('__hashtable__setStringKeyHashtable');
foreach ($table->iterateKeyed(true) as [$keyVar, $valueVar]) {
+ $resolved = $valueVar->resolveIndirect();
+ if (VMVariable::TYPE_INTEGER === $keyVar->type) {
+ if (VMVariable::TYPE_STRING !== $resolved->type) {
+ continue;
+ }
+ $str = $context->builder->load(
+ $context->constantStringFromString($resolved->toString())
+ );
+ $context->builder->call(
+ $context->lookupFunction('__hashtable__setStringAt'),
+ $ht,
+ $context->getTypeFromString('size_t')->constInt($keyVar->toInt(), false),
+ $str
+ );
+
+ continue;
+ }
if (VMVariable::TYPE_STRING !== $keyVar->type) {
continue;
}
$key = $context->builder->load(
$context->constantStringFromString($keyVar->toString())
);
- if (VMVariable::TYPE_STRING === $valueVar->type) {
+ if (VMVariable::TYPE_STRING === $resolved->type) {
$str = $context->builder->load(
- $context->constantStringFromString($valueVar->toString())
+ $context->constantStringFromString($resolved->toString())
);
$context->builder->call($setString, $ht, $key, $str);
+ } elseif (VMVariable::TYPE_ARRAY === $resolved->type) {
+ $nested = $resolved->toArray();
+ if ($nested instanceof HashTable) {
+ $child = $context->builder->call($context->lookupFunction('__hashtable__alloc'));
+ self::populateHashTableFromVm($context, $child, $nested);
+ $context->builder->call($setHashtable, $ht, $key, $child);
+ }
}
}
}
diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php
index 2de266455..95047be1e 100755
--- a/lib/JIT/Variable.php
+++ b/lib/JIT/Variable.php
@@ -373,7 +373,11 @@ public function dimFetch(self $dim, ?Type $expectedType = null): Variable {
$ptr,
);
case self::TYPE_HASHTABLE:
- if (null !== $this->superglobalName && self::TYPE_STRING === $dim->type) {
+ if (
+ null !== $this->superglobalName
+ && self::TYPE_STRING === $dim->type
+ && (null === $expectedType || Type::TYPE_ARRAY !== $expectedType->type)
+ ) {
$key = $dim->compileTimeString;
if (null !== $key) {
$baked = SuperglobalInit::compileTimeReadString(
@@ -393,10 +397,25 @@ public function dimFetch(self $dim, ?Type $expectedType = null): Variable {
}
$ht = $this->context->helper->loadValue($this);
if (self::TYPE_STRING === $dim->type) {
+ $key = $this->context->helper->loadValue($dim);
+ if (null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) {
+ $childHt = $this->context->builder->call(
+ $this->context->lookupFunction('__hashtable__readStringKeyHashtable'),
+ $ht,
+ $key
+ );
+
+ return new Variable(
+ $this->context,
+ self::TYPE_HASHTABLE,
+ self::KIND_VALUE,
+ $childHt
+ );
+ }
$valPtr = $this->context->builder->call(
$this->context->lookupFunction('__hashtable__readStringKeyValue'),
$ht,
- $this->context->helper->loadValue($dim)
+ $key
);
$str = $this->context->builder->call(
$this->context->lookupFunction('__value__readString'),
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index aaae7f4f7..5ade03643 100755
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -39,6 +39,8 @@
./test/aot/ExampleWebAotTest.php
./test/aot/StdlibWebBuiltinsTest.php
./test/aot/RuntimeSuperglobalRefreshTest.php
+ ./test/aot/NestedSuperglobalsAotTest.php
+ ./test/aot/WebAppNumberFormatTest.php
diff --git a/script/ci-local.sh b/script/ci-local.sh
index 7a6a4f04a..7d75fda77 100755
--- a/script/ci-local.sh
+++ b/script/ci-local.sh
@@ -14,7 +14,7 @@ else
python3 -c "import urllib.request; urllib.request.urlretrieve('https://getcomposer.org/download/latest-stable/composer.phar','/tmp/composer.phar')"
COMPOSER=("$PHP_BIN" -d "extension=$EXT_DIR/phar.so" -d "extension=$EXT_DIR/mbstring.so" /tmp/composer.phar)
fi
-"${COMPOSER[@]}" install --no-interaction --ignore-platform-reqs --no-plugins 2>/dev/null || true
+"${COMPOSER[@]}" install --no-interaction --ignore-platform-reqs 2>/dev/null || true
chmod +x script/install-llvm9.sh script/apply-patches.sh 2>/dev/null || true
if [[ -x script/install-llvm9.sh ]]; then
diff --git a/script/docker-capability-matrix.sh b/script/docker-capability-matrix.sh
new file mode 100755
index 000000000..dd4b10a33
--- /dev/null
+++ b/script/docker-capability-matrix.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# Regenerate docs/capabilities.md inside php:8.2-cli-bookworm and copy back via docker cp.
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+IMAGE="${PHP_COMPILER_DOCKER_IMAGE:-php:8.2-cli-bookworm}"
+CID=$(tar -C "$ROOT" -cf - --exclude='.git' --exclude='.phpunit.result.cache' . | docker run -i -d "$IMAGE" bash -c 'mkdir -p /compiler && tar xf - -C /compiler && exec sleep 600')
+cleanup() { docker rm -f "$CID" >/dev/null 2>&1 || true; }
+trap cleanup EXIT
+docker exec "$CID" bash -lc '
+set -e
+cd /compiler
+apt-get update -qq
+DEBIAN_FRONTEND=noninteractive apt-get install -y -qq pkg-config libffi-dev unzip curl > /dev/null
+docker-php-ext-install -j"$(nproc)" ffi > /dev/null 2>&1
+if ! command -v composer >/dev/null 2>&1; then
+ curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+fi
+composer install --no-interaction --ignore-platform-reqs > /dev/null 2>&1
+chmod +x script/*.sh
+script/apply-patches.sh > /dev/null 2>&1 || true
+php script/capability-matrix.php
+'
+docker cp "$CID:/compiler/docs/capabilities.md" "$ROOT/docs/capabilities.md"
+echo "Updated $ROOT/docs/capabilities.md"
diff --git a/script/docker-ci.sh b/script/docker-ci.sh
index ecb7a76c0..6c607e336 100755
--- a/script/docker-ci.sh
+++ b/script/docker-ci.sh
@@ -12,7 +12,7 @@ mkdir -p /compiler && tar xf - -C /compiler
cd /compiler
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
- build-essential dpkg-dev pkg-config libffi-dev unzip git curl > /dev/null
+ build-essential dpkg-dev pkg-config libffi-dev libbsd0 unzip git curl > /dev/null
docker-php-ext-install -j"$(nproc)" ffi > /dev/null 2>&1
if ! command -v composer >/dev/null 2>&1; then
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
diff --git a/test/aot/NestedSuperglobalsAotTest.php b/test/aot/NestedSuperglobalsAotTest.php
new file mode 100644
index 000000000..9edaa8ad9
--- /dev/null
+++ b/test/aot/NestedSuperglobalsAotTest.php
@@ -0,0 +1,191 @@
+compileBin = realpath(__DIR__ . '/../../bin/compile.php');
+ if (!self::isLlvmReady()) {
+ $this->markTestSkipped(
+ 'LLVM 9 toolchain not available. Run script/install-llvm9.sh from the repository root.'
+ );
+ }
+ }
+
+ public function testNestedQueryStringStillServesFlatGetKey(): void
+ {
+ $source = <<<'PHP'
+assertNotFalse($outfile);
+ unlink($outfile);
+
+ $repoRoot = dirname(__DIR__, 2);
+ $env = self::llvmEnvironment($repoRoot);
+ $env['QUERY_STRING'] = 'user[name]=Ada&flat=ok';
+
+ $result = $this->compileAndRun($source, [], $outfile, $env);
+ @unlink($outfile);
+
+ $this->assertSame("ok\n", $result);
+ }
+
+ /**
+ * @param list $extraArgv
+ * @param array $runEnv
+ */
+ private function compileAndRun(string $code, array $extraArgv, string $outfile, array $runEnv): string
+ {
+ $repoRoot = dirname(__DIR__, 2);
+ $descriptorSpec = [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ];
+ $compileArgv = array_merge(
+ self::llvmEnvPrefix(),
+ self::phpCommand(),
+ array_merge([$this->compileBin], $extraArgv, ['-o', $outfile])
+ );
+ $compile = proc_open($compileArgv, $descriptorSpec, $pipes, $repoRoot, $runEnv);
+ fwrite($pipes[0], $code);
+ fclose($pipes[0]);
+ $compileErr = stream_get_contents($pipes[2]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ proc_close($compile);
+
+ $this->assertFileExists($outfile, trim($compileErr !== false ? $compileErr : ''));
+ $this->assertTrue(is_executable($outfile));
+
+ $run = proc_open([$outfile], $descriptorSpec, $runPipes, $repoRoot, $runEnv);
+ $output = stream_get_contents($runPipes[1]);
+ fclose($runPipes[0]);
+ fclose($runPipes[1]);
+ fclose($runPipes[2]);
+ $exitCode = proc_close($run);
+ $this->assertSame(0, $exitCode, 'AOT binary should exit with status 0');
+
+ return $output !== false ? $output : '';
+ }
+
+ /**
+ * @return array
+ */
+ private static function llvmEnvironment(string $repoRoot): array
+ {
+ $env = [];
+ foreach (array_merge($_ENV, $_SERVER) as $key => $value) {
+ if (is_string($value)) {
+ $env[$key] = $value;
+ }
+ }
+ $llvmDir = $repoRoot.'/.llvm';
+ if (is_file($llvmDir.'/libLLVM-9.so.1')) {
+ $prefix = realpath($llvmDir) ?: $llvmDir;
+ $env['PHP_COMPILER_LLVM_PATH'] = $prefix;
+ $ld = $env['LD_LIBRARY_PATH'] ?? '';
+ $env['LD_LIBRARY_PATH'] = '' === $ld ? $prefix : $prefix.':'.$ld;
+ $path = $env['PATH'] ?? '';
+ $env['PATH'] = '' === $path ? $prefix : $prefix.':'.$path;
+ }
+
+ return $env;
+ }
+
+ private static function isLlvmReady(): bool
+ {
+ if (null !== self::$llvmReady) {
+ return self::$llvmReady;
+ }
+ $llvmDir = dirname(__DIR__, 2).'/.llvm';
+ if (!is_file($llvmDir.'/libLLVM-9.so.1')) {
+ self::$llvmReady = false;
+
+ return false;
+ }
+ if ('' === getenv('PHP_COMPILER_LLVM_PATH')) {
+ putenv('PHP_COMPILER_LLVM_PATH='.$llvmDir);
+ }
+ try {
+ \PHPLLVM\Chooser::choose();
+ self::$llvmReady = true;
+ } catch (\Throwable $e) {
+ self::$llvmReady = false;
+ }
+
+ return self::$llvmReady;
+ }
+
+ /**
+ * @return list
+ */
+ private static function phpCommand(): array
+ {
+ $phpEnv = getenv('PHP_COMPILER_PHP');
+ if (false !== $phpEnv && '' !== $phpEnv) {
+ $cmd = preg_split('/\s+/', $phpEnv);
+ } else {
+ $cmd = [PHP_BINARY];
+ }
+ $extDir = getenv('PHP_COMPILER_EXT_DIR') ?: '/usr/lib/php/20220829';
+ if (is_dir($extDir)) {
+ foreach (['tokenizer', 'mbstring', 'dom', 'xml', 'xmlwriter', 'ffi', 'posix', 'phar'] as $ext) {
+ $so = $extDir.'/'.$ext.'.so';
+ if (is_file($so)) {
+ $cmd[] = '-d';
+ $cmd[] = 'extension='.$so;
+ }
+ }
+ }
+ $cmd[] = '-d';
+ $cmd[] = 'display_errors=0';
+
+ return $cmd;
+ }
+
+ /**
+ * @return list
+ */
+ private static function llvmEnvPrefix(): array
+ {
+ $llvmDir = dirname(__DIR__, 2).'/.llvm';
+ if (!is_file($llvmDir.'/libLLVM-9.so.1')) {
+ return [];
+ }
+ $prefix = realpath($llvmDir) ?: $llvmDir;
+ $ld = getenv('LD_LIBRARY_PATH');
+ $ldVal = false === $ld || '' === $ld ? $prefix : $prefix.':'.$ld;
+ $path = getenv('PATH');
+ $pathVal = false === $path || '' === $path ? $prefix : $prefix.':'.$path;
+
+ return [
+ 'env',
+ 'LD_LIBRARY_PATH='.$ldVal,
+ 'PATH='.$pathVal,
+ 'PHP_COMPILER_LLVM_PATH='.$prefix,
+ ];
+ }
+}
diff --git a/test/fixtures/aot/cases/nested_get_params.phpt b/test/fixtures/aot/cases/nested_get_params.phpt
new file mode 100644
index 000000000..9e0862d8e
--- /dev/null
+++ b/test/fixtures/aot/cases/nested_get_params.phpt
@@ -0,0 +1,11 @@
+--TEST--
+AOT: nested QUERY_STRING does not break flat $_GET keys (C runtime refresh)
+--ENV--
+QUERY_STRING=user[name]=Ada&flat=ok
+--FILE--
+