diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md
index 7fdc44685e4..b08d935e09d 100644
--- a/docs/bootstrap-inventory.md
+++ b/docs/bootstrap-inventory.md
@@ -12,7 +12,7 @@ Regenerate: `php script/bootstrap-inventory.php`
| Phase A inventory files (M2 ratio SSOT) | 7381 |
| Phase A ratio-deferred paths | 0 |
| Source constructs flagged (blockers) | 0 |
-| Source constructs flagged (warnings) | 23918 |
+| Source constructs flagged (warnings) | 23917 |
## Compiler CFG gaps (`lib/Compiler.php`)
@@ -4493,7 +4493,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
| `ext/standard/stream_context_get_options.php` | 0 | 1 |
| `ext/standard/stream_context_get_params.php` | 0 | 1 |
| `ext/standard/stream_context_set_default.php` | 0 | 1 |
-| `ext/standard/stream_context_set_option.php` | 0 | 3 |
+| `ext/standard/stream_context_set_option.php` | 0 | 2 |
| `ext/standard/stream_context_set_options.php` | 0 | 2 |
| `ext/standard/stream_context_set_params.php` | 0 | 2 |
| `ext/standard/stream_copy_to_stream.php` | 0 | 1 |
@@ -29637,7 +29637,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
### `ext/standard/JitFdiv.php`
**Warnings** (review for bootstrap subset):
-- 15 class method(s)
+- 17 class method(s)
### `ext/standard/JitFeof.php`
@@ -31404,7 +31404,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
### `ext/standard/JitStreamContextSetOption.php`
**Warnings** (review for bootstrap subset):
-- 4 class method(s)
+- 15 class method(s)
### `ext/standard/JitStreamContextSetOptions.php`
@@ -36201,19 +36201,19 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
- new HashTable (line 127)
- new Variable (line 129)
- new Variable (line 307)
-- new HashTable (line 367)
-- new Variable (line 368)
-- new Variable (line 384)
-- new Variable (line 521)
-- new HashTable (line 541)
-- new Variable (line 550)
-- new HashTable (line 567)
-- new Variable (line 569)
-- new HashTable (line 579)
-- new Variable (line 581)
-- new Variable (line 601)
-- new HashTable (line 606)
-- new Variable (line 608)
+- new HashTable (line 408)
+- new Variable (line 409)
+- new Variable (line 425)
+- new Variable (line 562)
+- new HashTable (line 582)
+- new Variable (line 591)
+- new HashTable (line 608)
+- new Variable (line 610)
+- new HashTable (line 620)
+- new Variable (line 622)
+- new Variable (line 642)
+- new HashTable (line 647)
+- new Variable (line 649)
- 28 class method(s)
### `ext/standard/VmStreamContextOptions.php`
@@ -37851,13 +37851,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
### `ext/standard/diskfreespace.php`
**Warnings** (review for bootstrap subset):
-- new disk_free_space (line 23)
+- new disk_free_space (line 24)
- 2 class method(s)
### `ext/standard/disktotalspace.php`
**Warnings** (review for bootstrap subset):
-- new disk_total_space (line 23)
+- new disk_total_space (line 24)
- 2 class method(s)
### `ext/standard/dl.php`
@@ -40833,8 +40833,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
### `ext/standard/stream_context_set_option.php`
**Warnings** (review for bootstrap subset):
-- new Variable (line 65)
-- 3 class method(s)
+- 2 class method(s)
- 1 closure(s)
### `ext/standard/stream_context_set_options.php`
@@ -43453,7 +43452,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
### `ext/xml/xml_error_string.php`
**Warnings** (review for bootstrap subset):
-- new ArgumentCountError (line 26)
+- new ArgumentCountError (line 31)
- 2 class method(s)
### `ext/xml/xml_get_current_byte_index.php`
@@ -45870,8 +45869,8 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag
### `lib/BuiltinParamNames.php`
**Warnings** (review for bootstrap subset):
-- new ArgumentCountError (line 2393)
-- new ArgumentCountError (line 2404)
+- new ArgumentCountError (line 2396)
+- new ArgumentCountError (line 2407)
- 19 class method(s)
- 1 closure(s)
diff --git a/ext/standard/JitStreamContextSetOption.php b/ext/standard/JitStreamContextSetOption.php
index 8c6f92e7b70..8854be86745 100644
--- a/ext/standard/JitStreamContextSetOption.php
+++ b/ext/standard/JitStreamContextSetOption.php
@@ -4,16 +4,22 @@
namespace PHPCompiler\ext\standard;
+use PHPCompiler\JIT\BasicBlockHelper;
use PHPCompiler\JIT\Builtin\StreamContextRuntime;
use PHPCompiler\JIT\Context;
+use PHPCompiler\JIT\ExceptionBridge;
use PHPCompiler\JIT\HashTableHelper;
use PHPCompiler\JIT\JitValueBox;
use PHPCompiler\JIT\Variable as JITVariable;
+use PHPCompiler\VM\Variable as VmVariable;
+use PHPLLVM\Builder;
use PHPLLVM\Value;
-/** LLVM lowering for stream_context_set_option() (#3448). */
+/** LLVM lowering for stream_context_set_option() (#3448, #30645). */
final class JitStreamContextSetOption
{
+ private static int $guardSeq = 0;
+
public static function invoke(Context $context, JITVariable ...$args): Value
{
// Arity checked by stream_context_set_option::call via requireArgCountRangeJit (#30584).
@@ -23,40 +29,197 @@ public static function invoke(Context $context, JITVariable ...$args): Value
JitStreamContextRequiredArg::validate($context, $args[0], 'stream_context_set_option', 1);
- $ctxHt = self::loadContextArray($context, $args[0]);
- if (2 === $argc) {
- $optHt = self::loadOptionsArray($context, $args[1], 2);
- $context->builder->call(
- $context->lookupFunction('__phpc_stream_context_merge_options'),
- $ctxHt,
- $optHt
- );
- } else {
- // 3 or 4 args: singular form; omitted value is null (#30584 / php-src stub).
- $wrapperVal = self::loadValuePointer($context, $args[1], 2);
- $optionVal = self::loadValuePointer($context, $args[2], 3);
- if (4 === $argc) {
- $valueVal = self::loadValuePointer($context, $args[3], 4);
- } else {
- $nullSlot = JitValueBox::alloc($context);
- $valueVal = JitValueBox::pointer($context, $nullSlot);
- $context->builder->call($context->lookupFunction('__value__writeNull'), $valueVal);
+ $arg3 = $argc >= 3 ? $args[2] : null;
+ $arg4 = $argc >= 4 ? $args[3] : null;
+ $optionIsNull = self::operandIsNull($arg3);
+ $valueProvided = null !== $arg4;
+
+ if (self::isCompileTimeArray($args[1])) {
+ if (!$optionIsNull) {
+ return self::emitValueError($context, VmStreamContext::SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY);
}
- $context->builder->call(
- $context->lookupFunction('__phpc_stream_context_set_single_option'),
- $ctxHt,
- $wrapperVal,
- $optionVal,
- $valueVal
- );
+ if ($valueProvided) {
+ return self::emitValueError($context, VmStreamContext::SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY);
+ }
+
+ return self::emitMergeOptions($context, $args[0], $args[1]);
+ }
+
+ if (self::isCompileTimeNonArray($args[1])) {
+ if ($optionIsNull) {
+ return self::emitValueError($context, VmStreamContext::SET_OPTION_OPTION_NAME_NULL_ON_STRING);
+ }
+ if (!$valueProvided) {
+ return self::emitValueError($context, VmStreamContext::SET_OPTION_VALUE_REQUIRED_ON_STRING);
+ }
+
+ return self::emitSetSingleOption($context, $args[0], $args[1], $args[2], $args[3]);
}
+ return self::emitRuntimeWrapperDispatch(
+ $context,
+ $args[0],
+ $args[1],
+ $arg3,
+ $arg4,
+ $optionIsNull,
+ $valueProvided
+ );
+ }
+
+ private static function emitRuntimeWrapperDispatch(
+ Context $context,
+ JITVariable $ctxArg,
+ JITVariable $wrapperArg,
+ ?JITVariable $arg3,
+ ?JITVariable $arg4,
+ bool $optionIsNull,
+ bool $valueProvided
+ ): Value {
+ $tag = 'sctxso'.(string) ++self::$guardSeq;
+ $isArray = self::emitRuntimeIsArray($context, $wrapperArg);
+ $arrBb = BasicBlockHelper::append($context, 'sctx_set_opt_arr_'.$tag);
+ $strBb = BasicBlockHelper::append($context, 'sctx_set_opt_str_'.$tag);
+ $joinBb = BasicBlockHelper::append($context, 'sctx_set_opt_join_'.$tag);
+ $context->builder->branchIf($isArray, $arrBb, $strBb);
+
+ $context->builder->positionAtEnd($arrBb);
+ if (!$optionIsNull) {
+ ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY);
+ BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_arr_opt_dead_'.$tag);
+ } elseif ($valueProvided) {
+ ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY);
+ BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_arr_val_dead_'.$tag);
+ } else {
+ self::emitMergeOptionsVoid($context, $ctxArg, $wrapperArg);
+ }
+ $context->builder->branch($joinBb);
+
+ $context->builder->positionAtEnd($strBb);
+ if ($optionIsNull) {
+ ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_OPTION_NAME_NULL_ON_STRING);
+ BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_str_opt_dead_'.$tag);
+ } elseif (!$valueProvided) {
+ ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_VALUE_REQUIRED_ON_STRING);
+ BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_str_val_dead_'.$tag);
+ } else {
+ assert(null !== $arg3 && null !== $arg4);
+ self::emitSetSingleOptionVoid($context, $ctxArg, $wrapperArg, $arg3, $arg4);
+ }
+ $context->builder->branch($joinBb);
+
+ $context->builder->positionAtEnd($joinBb);
+
+ return self::emitTrue($context);
+ }
+
+ private static function emitRuntimeIsArray(Context $context, JITVariable $arg): Value
+ {
+ $ptr = self::loadValuePointer($context, $arg, 2);
+ $map = $context->structFieldMap['__value__'];
+ $typeByte = $context->builder->load(
+ $context->builder->structGep($ptr, $map['type'])
+ );
+ $i8 = $context->getTypeFromString('int8');
+ $kind = $context->builder->and($typeByte, $i8->constInt(0x7f, false));
+ $isArray = $context->builder->icmp(
+ Builder::INT_EQ,
+ $kind,
+ $i8->constInt(VmVariable::TYPE_ARRAY, false)
+ );
+ $isHt = $context->builder->icmp(
+ Builder::INT_EQ,
+ $kind,
+ $i8->constInt(JITVariable::TYPE_HASHTABLE & 0x7f, false)
+ );
+
+ return $context->builder->or($isArray, $isHt);
+ }
+
+ private static function emitMergeOptions(Context $context, JITVariable $ctxArg, JITVariable $optArg): Value
+ {
+ self::emitMergeOptionsVoid($context, $ctxArg, $optArg);
+
+ return self::emitTrue($context);
+ }
+
+ private static function emitMergeOptionsVoid(Context $context, JITVariable $ctxArg, JITVariable $optArg): void
+ {
+ $ctxHt = self::loadContextArray($context, $ctxArg);
+ $optHt = self::loadOptionsArray($context, $optArg, 2);
+ $context->builder->call(
+ $context->lookupFunction('__phpc_stream_context_merge_options'),
+ $ctxHt,
+ $optHt
+ );
+ }
+
+ private static function emitSetSingleOption(
+ Context $context,
+ JITVariable $ctxArg,
+ JITVariable $wrapperArg,
+ JITVariable $optionArg,
+ JITVariable $valueArg
+ ): Value {
+ self::emitSetSingleOptionVoid($context, $ctxArg, $wrapperArg, $optionArg, $valueArg);
+
+ return self::emitTrue($context);
+ }
+
+ private static function emitSetSingleOptionVoid(
+ Context $context,
+ JITVariable $ctxArg,
+ JITVariable $wrapperArg,
+ JITVariable $optionArg,
+ JITVariable $valueArg
+ ): void {
+ $ctxHt = self::loadContextArray($context, $ctxArg);
+ $wrapperVal = self::loadValuePointer($context, $wrapperArg, 2);
+ $optionVal = self::loadValuePointer($context, $optionArg, 3);
+ $valueVal = self::loadValuePointer($context, $valueArg, 4);
+ $context->builder->call(
+ $context->lookupFunction('__phpc_stream_context_set_single_option'),
+ $ctxHt,
+ $wrapperVal,
+ $optionVal,
+ $valueVal
+ );
+ }
+
+ private static function emitValueError(Context $context, string $message): Value
+ {
+ ExceptionBridge::emitValueErrorAndAbort($context, $message);
+ BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_ve_dead_'.(string) ++self::$guardSeq);
+
+ return self::emitTrue($context);
+ }
+
+ private static function emitTrue(Context $context): Value
+ {
$slot = JitValueBox::alloc($context);
JitValueBox::writeBool($context, $slot, $context->constantFromBool(true));
return JitValueBox::pointer($context, $slot);
}
+ private static function isCompileTimeArray(JITVariable $arg): bool
+ {
+ return JITVariable::TYPE_HASHTABLE === $arg->type
+ || 0 !== ($arg->type & JITVariable::IS_NATIVE_ARRAY);
+ }
+
+ private static function isCompileTimeNonArray(JITVariable $arg): bool
+ {
+ return !self::isCompileTimeArray($arg) && JITVariable::TYPE_VALUE !== $arg->type;
+ }
+
+ private static function operandIsNull(?JITVariable $arg): bool
+ {
+ return null === $arg
+ || JITVariable::TYPE_NULL === $arg->type
+ || $arg->isNullConstant;
+ }
+
private static function loadContextArray(Context $context, JITVariable $arg): Value
{
if (JITVariable::TYPE_HASHTABLE === $arg->type) {
diff --git a/ext/standard/VmStreamContext.php b/ext/standard/VmStreamContext.php
index 476dc293a25..4686648e2e5 100644
--- a/ext/standard/VmStreamContext.php
+++ b/ext/standard/VmStreamContext.php
@@ -329,23 +329,64 @@ private static function canonicalHashTable(Variable $context): HashTable
}
/**
- * stream_context_set_option() — singular or batch wrapper option write (ext/standard/streams.c, #3448).
+ * php-src streamsfuncs.c zend_argument_value_error(3, "cannot be null when argument #2 … is a string") (#30645).
+ */
+ public const SET_OPTION_OPTION_NAME_NULL_ON_STRING =
+ 'stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string';
+
+ /**
+ * php-src streamsfuncs.c zend_argument_value_error(4, "must be provided when argument #2 … is a string") (#30645).
+ */
+ public const SET_OPTION_VALUE_REQUIRED_ON_STRING =
+ 'stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string';
+
+ /**
+ * php-src streamsfuncs.c zend_argument_value_error(3, "must be null when argument #2 … is an array") (#30645).
+ */
+ public const SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY =
+ 'stream_context_set_option(): Argument #3 ($option_name) must be null when argument #2 ($wrapper_or_options) is an array';
+
+ /**
+ * php-src streamsfuncs.c zend_argument_value_error(4, "cannot be provided when argument #2 … is an array") (#30645).
+ */
+ public const SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY =
+ 'stream_context_set_option(): Argument #4 ($value) cannot be provided when argument #2 ($wrapper_or_options) is an array';
+
+ /**
+ * stream_context_set_option() — singular or batch wrapper option write (ext/standard/streams.c, #3448, #30645).
+ *
+ * Two-arg array form merges a full options array; four-arg string form sets one wrapper option.
+ * Incomplete string form / extra array-form args → Zend ValueError (not LogicException / silent true).
*
- * Two-arg form merges a full options array; four-arg form sets one wrapper option.
+ * $arg3/$arg4 PHP null means the argument was omitted; a Variable (including TYPE_NULL) was passed.
*/
public static function setOption(Variable $context, Variable $arg2, ?Variable $arg3 = null, ?Variable $arg4 = null): bool
{
$context = self::requireRepresentation($context, 'stream_context_set_option');
$resolved2 = $arg2->resolveIndirect();
- if (Variable::TYPE_ARRAY === $resolved2->type && (null === $arg3 || null === $arg4)) {
+ $optionIsNull = null === $arg3 || Variable::TYPE_NULL === $arg3->resolveIndirect()->type;
+ $valueProvided = null !== $arg4;
+
+ if (Variable::TYPE_ARRAY === $resolved2->type) {
+ if (!$optionIsNull) {
+ throw new \ValueError(self::SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY);
+ }
+ if ($valueProvided) {
+ throw new \ValueError(self::SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY);
+ }
+
return self::setOptions($context, $arg2);
}
- if (null === $arg3 || null === $arg4) {
- throw new \LogicException('stream_context_set_option(): missing wrapper/option/value arguments');
+
+ if ($optionIsNull) {
+ throw new \ValueError(self::SET_OPTION_OPTION_NAME_NULL_ON_STRING);
+ }
+ if (!$valueProvided) {
+ throw new \ValueError(self::SET_OPTION_VALUE_REQUIRED_ON_STRING);
}
- $wrapperName = self::coerceOptionKeyString($resolved2, 'stream_context_set_option', 2, 'wrapper');
- $optionName = self::coerceOptionKeyString($arg3->resolveIndirect(), 'stream_context_set_option', 3, 'option');
+ $wrapperName = self::coerceOptionKeyString($resolved2, 'stream_context_set_option', 2, 'wrapper_or_options');
+ $optionName = self::coerceOptionKeyString($arg3->resolveIndirect(), 'stream_context_set_option', 3, 'option_name');
$exportedValue = VmHttpBuildQuery::export($arg4);
if (!\is_scalar($exportedValue) && null !== $exportedValue && !\is_array($exportedValue)) {
$exportedValue = '';
diff --git a/ext/standard/stream_context_set_option.php b/ext/standard/stream_context_set_option.php
index f8ba78d131e..36ad31f90e3 100644
--- a/ext/standard/stream_context_set_option.php
+++ b/ext/standard/stream_context_set_option.php
@@ -17,6 +17,7 @@
* stream_context_set_option() — singular or batch wrapper option write (ext/standard/streams.c, #3448).
*
* Excess/missing argc → Zend ArgumentCountError (#30584; php-src stub arity 2..4).
+ * Incomplete string form → Zend ValueError (#30645); do not synthesize a null $value.
*/
final class stream_context_set_option extends Internal
{
@@ -32,14 +33,19 @@ public function execute(Frame $frame): void
$argc = \count($frame->calledArgs);
if (2 === $argc) {
$ok = VmStreamContext::setOption($frame->calledArgs[0], $frame->calledArgs[1]);
+ } elseif (3 === $argc) {
+ // Omit $value so setOption can distinguish missing arg #4 from an explicit null (#30645).
+ $ok = VmStreamContext::setOption(
+ $frame->calledArgs[0],
+ $frame->calledArgs[1],
+ $frame->calledArgs[2]
+ );
} else {
- // 3 or 4: singular form; omitted value defaults to null like the stub.
- $value = 4 === $argc ? $frame->calledArgs[3] : self::nullVar();
$ok = VmStreamContext::setOption(
$frame->calledArgs[0],
$frame->calledArgs[1],
$frame->calledArgs[2],
- $value
+ $frame->calledArgs[3]
);
}
BuiltinExecute::writeReturn(
@@ -59,12 +65,4 @@ public function call(Context $context, JITVariable ...$args): Value
return JitStreamContextSetOption::invoke($context, ...$args);
}
-
- private static function nullVar(): Variable
- {
- $v = new Variable();
- $v->null();
-
- return $v;
- }
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index dd79235fb59..b43d8aab5f4 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -144,6 +144,8 @@
./test/compliance/StreamFilterAppendPrependReflectionVMTest.php
./test/compliance/SocketReflectionStubsVMTest.php
./test/compliance/StreamContextSetOptionReturnTrueVMTest.php
+ ./test/compliance/StreamContextSetOptionIncomplete30645VMTest.php
+ ./test/compliance/StreamContextSetOptionIncomplete30645JITTest.php
./test/compliance/StreamSocketEnableCryptoReflectionVMTest.php
./test/compliance/HtmlDecodeFlagsNamedReflectionVMTest.php
./test/compliance/StreamIsattyReflectionVMTest.php
diff --git a/test/compliance/StreamContextSetOptionIncomplete30645JITTest.php b/test/compliance/StreamContextSetOptionIncomplete30645JITTest.php
new file mode 100644
index 00000000000..7cd160ed691
--- /dev/null
+++ b/test/compliance/StreamContextSetOptionIncomplete30645JITTest.php
@@ -0,0 +1,36 @@
+ self::parsePHPT(
+ __DIR__.'/cases/stdlib/'.$file,
+ $file
+ );
+ }
+
+ public function setUp(): void
+ {
+ $this->BIN = realpath(__DIR__.'/../../bin/jit.php');
+ if (!LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) {
+ $this->markTestSkipped(
+ 'LLVM 9 toolchain not available. Run script/install-llvm9.sh or use the 22.04-dev Docker image.'
+ );
+ }
+ }
+}
diff --git a/test/compliance/StreamContextSetOptionIncomplete30645VMTest.php b/test/compliance/StreamContextSetOptionIncomplete30645VMTest.php
new file mode 100644
index 00000000000..09a797a1d97
--- /dev/null
+++ b/test/compliance/StreamContextSetOptionIncomplete30645VMTest.php
@@ -0,0 +1,27 @@
+ self::parsePHPT(
+ __DIR__.'/cases/stdlib/'.$file,
+ $file
+ );
+ }
+
+ public function setUp(): void
+ {
+ $this->BIN = realpath(__DIR__.'/../../bin/vm.php');
+ }
+}
diff --git a/test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt b/test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt
new file mode 100644
index 00000000000..233ce3d656e
--- /dev/null
+++ b/test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt
@@ -0,0 +1,33 @@
+--TEST--
+stream_context_set_option string-form incomplete args → ValueError (#30645)
+--FILE--
+ stream_context_set_option($c, 'http'),
+ static fn () => stream_context_set_option($c, 'http', 'method'),
+ static fn () => stream_context_set_option($c, ['http' => ['timeout' => 1]], 'method'),
+] as $fn) {
+ try {
+ $r = $fn();
+ echo 'NO_THROW ', var_export($r, true), "\n";
+ } catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+ } catch (Throwable $e) {
+ echo get_class($e), ':', $e->getMessage(), "\n";
+ }
+}
+var_export(stream_context_set_option($c, ['http' => ['method' => 'GET']]));
+echo "\n";
+var_export(stream_context_set_option($c, 'http', 'method', 'POST'));
+echo "\n";
+$opts = stream_context_get_options($c);
+echo $opts['http']['method'], "\n";
+?>
+--EXPECT--
+stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string
+stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string
+stream_context_set_option(): Argument #3 ($option_name) must be null when argument #2 ($wrapper_or_options) is an array
+true
+true
+POST
diff --git a/test/repro/maintainer_gap_stream_context_set_option_incomplete.php b/test/repro/maintainer_gap_stream_context_set_option_incomplete.php
new file mode 100644
index 00000000000..25b1a892bcb
--- /dev/null
+++ b/test/repro/maintainer_gap_stream_context_set_option_incomplete.php
@@ -0,0 +1,16 @@
+getMessage(), "\n";
+}
+try {
+ var_export(stream_context_set_option($c, 'http', 'method'));
+} catch (Throwable $e) {
+ echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+var_export(stream_context_set_option($c, ['http' => ['method' => 'GET']]));
+echo "\n";
+var_export(stream_context_set_option($c, 'http', 'method', 'GET'));
+echo "\n";
diff --git a/test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php b/test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php
new file mode 100644
index 00000000000..68317ee8537
--- /dev/null
+++ b/test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php
@@ -0,0 +1,71 @@
+markTestSkipped('LLVM 9 toolchain not available');
+ }
+ $root = dirname(__DIR__, 2);
+ $src = sys_get_temp_dir().'/phpc_30645_'.getmypid().'.php';
+ $bin = sys_get_temp_dir().'/phpc_30645_'.getmypid().'.bin';
+ file_put_contents($src, <<<'PHP'
+getMessage(), "\n";
+}
+try {
+ stream_context_set_option($c, 'http', 'method');
+ echo "NO_THROW\n";
+} catch (Throwable $e) {
+ echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+var_export(stream_context_set_option($c, 'http', 'method', 'GET'));
+echo "\n";
+PHP);
+ $compile = 'PHP_COMPILER_HELPER_RUNTIME_O=0 '
+ .escapeshellarg(PHP_BINARY).' '
+ .escapeshellarg($root.'/bin/compile.php')
+ .' -o '.escapeshellarg($bin).' '.escapeshellarg($src).' 2>&1';
+ exec($compile, $compileOut, $compileRc);
+ $this->assertSame(0, $compileRc, implode("\n", $compileOut));
+ $this->assertFileExists($bin);
+ try {
+ $runOut = [];
+ exec(escapeshellarg($bin).' 2>&1', $runOut, $runRc);
+ $joined = implode("\n", $runOut);
+ $this->assertSame(0, $runRc, $joined);
+ $this->assertStringContainsString(
+ 'ValueError: stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string',
+ $joined
+ );
+ $this->assertStringContainsString(
+ 'ValueError: stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string',
+ $joined
+ );
+ $this->assertStringContainsString('true', $joined);
+ $this->assertStringNotContainsString('NO_THROW', $joined);
+ $this->assertStringNotContainsString('LogicException', $joined);
+ } finally {
+ @unlink($src);
+ @unlink($bin);
+ }
+ }
+}
diff --git a/test/unit/Issue30645StreamContextSetOptionIncompleteTest.php b/test/unit/Issue30645StreamContextSetOptionIncompleteTest.php
new file mode 100644
index 00000000000..174b0bf9bcd
--- /dev/null
+++ b/test/unit/Issue30645StreamContextSetOptionIncompleteTest.php
@@ -0,0 +1,38 @@
+assertNotFalse($code);
+ $rt = new Runtime();
+ $block = $rt->parseAndCompile($code, 'issue_30645.php');
+ ob_start();
+ $rt->run($block);
+ $out = ob_get_clean();
+ $this->assertStringContainsString(
+ 'ValueError: stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string',
+ $out
+ );
+ $this->assertStringContainsString(
+ 'ValueError: stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string',
+ $out
+ );
+ $this->assertStringContainsString("true\ntrue\n", $out);
+ $this->assertStringNotContainsString('LogicException', $out);
+ $this->assertStringNotContainsString('NO_THROW', $out);
+ $this->assertStringNotContainsString('missing wrapper/option/value', $out);
+ }
+}