Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/runtime-semantics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Runtime semantics (VM / JIT / AOT)

Documented behavior for web and array access. See also [#176](https://github.com/PurHur/php-compiler/issues/176) (capability matrix).

## Undefined array keys ([#273](https://github.com/PurHur/php-compiler/issues/273))

When `error_reporting` includes `E_WARNING` (default in VM: full `E_ALL`):

| Context | Missing key read | Warning |
|---------|------------------|---------|
| VM (`bin/vm.php`, `phpc run`) | Value is `null` | `Warning: Undefined array key "key"` (string keys quoted; integer keys unquoted) |
| JIT / AOT (`__hashtable__` string keys) | Value is `null` / empty | Same message via `__compiler_undefined_array_key_warning_cstr` on stderr |

`isset($arr['missing'])` and `empty($arr['missing'])` do **not** emit warnings.

Writes to missing keys (`$arr['new'] = 1`) create the key without a warning (PHP behavior).

Recommended app pattern once [#99](https://github.com/PurHur/php-compiler/issues/99) lands: `$name = $_GET['name'] ?? 'Guest';`

## Verification

```bash
docker run --rm -v "$(pwd):/compiler" -w /compiler php-compiler:22.04-dev \
./phpc run examples/001-SimpleWeb/example.php
# Without ?name= — VM emits Warning on stderr; page still renders with null coerced in echo.

./script/ci-local.sh --filter UndefinedArrayKey
./script/ci-local.sh --filter undefined_array_key_get
```
17 changes: 17 additions & 0 deletions lib/AOT/runtime/superglobals_refresh.c
Original file line number Diff line number Diff line change
Expand Up @@ -1002,3 +1002,20 @@ __string__ *__compiler_strip_tags(__string__ *input, __string__ *allowed)
return result;
}
}

/*
* Zend parity for missing array string keys (issue #273).
* Called from JIT __hashtable__readStringKeyValue when lookup returns NULL.
*/
void __compiler_undefined_array_key_warning_cstr(const char *key, size_t len)
{
if (!key) {
return;
}
fprintf(stderr, "Warning: Undefined array key \"%.*s\"\n", (int) len, key);
}

void __compiler_undefined_array_key_warning_long(long long key)
{
fprintf(stderr, "Warning: Undefined array key %lld\n", key);
}
17 changes: 17 additions & 0 deletions lib/JIT/Builtin/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ public function register(): void {
);
$fnFormatDt = $this->context->module->addFunction('__compiler_format_datetime', $fntypeFormatDt);
$this->context->registerFunction('__compiler_format_datetime', $fnFormatDt);
$fntypeUndefKeyStr = $this->context->context->functionType(
$void,
false,
$i8p,
$sizeT
);
$fnUndefKeyStr = $this->context->module->addFunction(
'__compiler_undefined_array_key_warning_cstr',
$fntypeUndefKeyStr
);
$this->context->registerFunction('__compiler_undefined_array_key_warning_cstr', $fnUndefKeyStr);
$fntypeUndefKeyLong = $this->context->context->functionType($void, false, $i64);
$fnUndefKeyLong = $this->context->module->addFunction(
'__compiler_undefined_array_key_warning_long',
$fntypeUndefKeyLong
);
$this->context->registerFunction('__compiler_undefined_array_key_warning_long', $fnUndefKeyLong);
$i8p = $this->context->getTypeFromString('int8*');
$i64p = $this->context->getTypeFromString('int64*');
$libcFns = [
Expand Down
25 changes: 25 additions & 0 deletions lib/JIT/Builtin/Type/HashTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,31 @@ private function implementReadStringKeyValue(): void
$ht = $fn->getParam(0);
$key = $fn->getParam(1);
$valPtr = $this->lookupStringKeyValue($fn, $block, $ht, $key);
$afterLookup = $fn->appendBasicBlock('strkey_read_val_after_lookup');
$this->context->builder->branch($afterLookup);
$this->context->builder->positionAtEnd($afterLookup);
$isNull = $this->context->builder->icmp(Builder::INT_EQ, $valPtr, $valPtr->typeOf()->constNull());
$hasValue = $fn->appendBasicBlock('strkey_read_val_has_value');
$warn = $fn->appendBasicBlock('strkey_read_val_warn');
$merge = $fn->appendBasicBlock('strkey_read_val_merge');
$this->context->builder->branchIf($isNull, $warn, $hasValue);
$this->context->builder->positionAtEnd($warn);
$strMap = $this->context->structFieldMap['__string__'];
$i8p = $this->context->getTypeFromString('int8*');
$keyLen = $this->context->builder->load(
$this->context->builder->structGep($key, $strMap['length'])
);
$keyBytes = $this->stringDataPtr($key);
$keyCStr = $this->context->builder->pointerCast($keyBytes, $i8p);
$this->context->builder->call(
$this->context->lookupFunction('__compiler_undefined_array_key_warning_cstr'),
$keyCStr,
$keyLen
);
$this->context->builder->branch($merge);
$this->context->builder->positionAtEnd($hasValue);
$this->context->builder->branch($merge);
$this->context->builder->positionAtEnd($merge);
$this->context->builder->returnValue($valPtr);
}

Expand Down
6 changes: 5 additions & 1 deletion lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ public function run(Block $block): int {
$offset->stringOffset($container, $arg3->toInt());
$arg1->indirect($offset);
} elseif ($container->type === Variable::TYPE_ARRAY) {
$arg1->indirect($container->toArray()->findVariable($arg3, false));
$table = $container->toArray();
if (!$table->keyExists($arg3)) {
$this->context->errors->undefinedArrayKey($arg3);
}
$arg1->indirect($table->findVariable($arg3, false));
} else {
throw new \LogicException('Illegal offset');
}
Expand Down
4 changes: 3 additions & 1 deletion lib/VM/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ class Context {
private array $superglobalVars = [];

public Runtime $runtime;


public ErrorReporter $errors;

public function __construct(Runtime $runtime) {
$this->runtime = $runtime;
$this->errors = new ErrorReporter();
}

public function constantFetch(string $name): ?Variable {
Expand Down
62 changes: 62 additions & 0 deletions lib/VM/ErrorReporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM;

/**
* Zend-style warnings for compiled VM code (issue #273).
*/
final class ErrorReporter
{
public const E_WARNING = 2;

private int $errorReporting;
private bool $displayErrors;

public function __construct(
int $errorReporting = E_ALL,
bool $displayErrors = true
) {
$this->errorReporting = $errorReporting;
$this->displayErrors = $displayErrors;
}

public function setErrorReporting(int $level): void
{
$this->errorReporting = $level;
}

public function setDisplayErrors(bool $display): void
{
$this->displayErrors = $display;
}

public function undefinedArrayKey(Variable $index, ?string $file = null): void
{
if (0 === ($this->errorReporting & self::E_WARNING)) {
return;
}
$key = $this->formatArrayKey($index);
$message = "Warning: Undefined array key {$key}";
if (null !== $file && '' !== $file) {
$message .= " in {$file}";
}
$message .= "\n";
if ($this->displayErrors) {
fwrite(STDERR, $message);
}
}

private function formatArrayKey(Variable $index): string
{
if (Variable::TYPE_STRING === $index->type) {
return '"' . $index->toString() . '"';
}
if (Variable::TYPE_INTEGER === $index->type) {
return (string) $index->toInt();
}

return '"' . $index->toString() . '"';
}
}
12 changes: 12 additions & 0 deletions lib/VM/HashTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ public function iterateKeyed(bool $resolveIndirect = false): \Generator
}
}

public function keyExists(Variable $index): bool
{
switch ($index->type) {
case Variable::TYPE_INTEGER:
return null !== $this->findIndex($index->toInt());
case Variable::TYPE_STRING:
return null !== $this->find($index->toString());
default:
throw new \LogicException("Unknown index type {$index->type}");
}
}

public function findVariable(Variable $index, bool $forWrite): ?Variable {
switch ($index->type) {
case Variable::TYPE_INTEGER:
Expand Down
10 changes: 10 additions & 0 deletions test/real/cases/undefined_array_key_get.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Web: missing $_GET key yields null (Zend warning when error_reporting includes E_WARNING)
--ENV--
QUERY_STRING=
--FILE--
<?php
$v = $_GET['name'];
echo $v === null ? "null\n" : "set\n";
--EXPECT--
null
43 changes: 43 additions & 0 deletions test/unit/VM/UndefinedArrayKeyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM;

use PHPUnit\Framework\TestCase;

final class UndefinedArrayKeyTest extends TestCase
{
public function testHashTableKeyExists(): void
{
$ht = new HashTable();
$key = new Variable();
$key->string('present');
$this->assertFalse($ht->keyExists($key));

$val = new Variable();
$val->string('x');
$ht->add('present', $val);
$this->assertTrue($ht->keyExists($key));

$missing = new Variable();
$missing->string('absent');
$this->assertFalse($ht->keyExists($missing));
}

public function testErrorReporterFormatsArrayKeysLikePhp8(): void
{
$reporter = new ErrorReporter();
$ref = new \ReflectionClass(ErrorReporter::class);
$method = $ref->getMethod('formatArrayKey');
$method->setAccessible(true);

$stringKey = new Variable();
$stringKey->string('name');
$this->assertSame('"name"', $method->invoke($reporter, $stringKey));

$intKey = new Variable();
$intKey->int(0);
$this->assertSame('0', $method->invoke($reporter, $intKey));
}
}
Loading