
# Wertkonvertierung

Jeder Aufruf über die PHP↔JS-Grenze – `setGlobal`, `call` oder eine aus JS aufgerufene Host-Funktion – durchläuft einen Konverter:

- **PHP → JS**, wenn Werte aus dem Host in die Engine gelangen (`setGlobal()`, `call()`-Argumente, Rückgabewerte von Host-Funktionen).
- **JS → PHP**, wenn JS-Werte zurückgegeben werden. Dafür gibt es zwei Pfade mit leicht unterschiedlichen Regeln: Ergebnisse von `eval()` und `call()` sowie Argumente, die JS an eine Host-Funktion übergibt.

Bei der Konvertierung werden Kopien erstellt. Primitive Werte werden auf primitive Werte abgebildet, Arrays und Objekte in neue Strukturen kopiert und PHP-Aufrufbare als JS-Funktionen verpackt. Abgesehen von den Aufrufbaren wird nichts per Referenz gemeinsam genutzt.

## PHP → JS

| PHP-Wert | JS-Ergebnis |
|---|---|
| `null` | `null` |
| `true` / `false` | `true` / `false` |
| `int` | `number` (IEEE 754 Double; Ganzzahlen über 2^53 verlieren an Genauigkeit) |
| `float` | `number` (NaN, ±Infinity, ±0 bleiben erhalten) |
| `string` | `string` (UTF-8-PHP-Bytes werden als UTF-16-Codeeinheiten interpretiert), auch wenn sie eine PHP-Funktion benennen |
| Listen-`array` (Schlüssel 0, 1, 2, … der Reihe nach) | neues `Array`, Elemente rekursiv konvertiert |
| jedes andere `array` | neues einfaches `Object`, Werte rekursiv konvertiert |
| `Closure`, aufrufbares Objekt, aufrufbares Array wie `[$object, 'method']` | `function`, das das PHP-Aufrufbare aufruft |
| jedes andere Objekt, `Generator`, Ressource | `undefined` |

Arrays mit gemischten Schlüsseln (PHPs `[0 => 'a', 'name' => 'b']`) werden zu einfachen Objekten, da Arrays in JS einen lückenlosen Bereich ganzzahliger Indizes benötigen.

```php
$engine->setGlobal('list', [1, 2, 3]);                 // → JS Array
$engine->setGlobal('map',  ['a' => 1, 'b' => 2]);      // → JS Object
$engine->setGlobal('mixed', [0 => 'x', 'k' => 'v']);   // → JS Object
```

Da Arrays kopiert werden, bleiben Änderungen an JS in JS:

```php
$list = [1, 2, 3];
$engine->setGlobal('list', $list);
$engine->eval('list.push(4)');
count($list);                 // 3
$engine->eval('list.length'); // 4
```

Nicht aufrufbare PHP-Objekte lassen sich nicht konvertieren; sie kommen als `undefined` an ([Issue #18](https://github.com/inline0/phasis/issues/18)). Wandle sie zuerst in Arrays um, zum Beispiel mit `get_object_vars()`, oder stelle ihre Methoden als Closures bereit. Siehe [Gemeinsam genutzte Objekte](https://phasis.inline0.dev/de/docs/interop/shared-objects).

## JS → PHP: Ergebnisse von `eval()` und `call()`

| JS-Wert | PHP-Ergebnis |
|---|---|
| `undefined`, `null` | `null` |
| `boolean` | `bool` |
| `number` (ganzzahlig, innerhalb des int-Bereichs) | `int` (`-0` wird zu `0`) |
| `NaN`, `Infinity`, `-Infinity` | `NAN`, `INF`, `-INF` |
| `number` (sonstige) | `float` |
| `string` | `string` (UTF-8) |
| `bigint`, `symbol` | `null` |
| `function` | `null` |
| `Array` | Liste `array`, Elemente rekursiv konvertiert |
| `TypedArray` | Liste `array` mit Zahlen |
| jedes andere Objekt (einfache Objekte, `Error`, `Date`, `Map`, `Set`, `Promise`, `ArrayBuffer`, …) | assoziatives `array` seiner eigenen Dateneigenschaften, wobei Accessors und Eigenschaften mit Funktionswert übersprungen werden |

Die letzte Zeile bedeutet, dass Objekte mit internem Zustand zu einem leeren Array werden: `new Date(0)`, `new Map([["a", 1]])` und `Promise.resolve(5)` geben alle `[]` zurück. Ein `Error` gibt sein eigenes `message` und `stack` zurück. Bei einem Objekt mit Selbstreferenz werden die internen Referenzen zu `null`.

```php
$engine->eval('[1, 2, 3]');                // [1, 2, 3]
$engine->eval('({a: 1, b: 2})');           // ['a' => 1, 'b' => 2]
$engine->eval('new Uint8Array([1, 2, 3])'); // [1, 2, 3]
$engine->eval('1234567890123456789n');     // null
$engine->eval('new Date(0)');              // []
```

Wandle die betreffenden Werte in JS um, wenn du sie in PHP brauchst:

```php
$engine->eval('(1234567890123456789n).toString()');  // "1234567890123456789"
$engine->eval('new Date(0).toISOString()');           // "1970-01-01T00:00:00.000Z"
$engine->eval('Object.fromEntries(new Map([["a", 1]]))'); // ['a' => 1]
```

## JS → PHP: Argumente von Host-Funktionen

Argumente, die JS an eine PHP-Host-Funktion übergibt, folgen bei primitiven Werten, Arrays und einfachen Objekten denselben Regeln, mit folgenden Unterschieden:

| JS-Wert | PHP-Argument |
|---|---|
| `TypedArray`, `ArrayBuffer`, `DataView` | das eigene `JsValue`-Objekt der Engine, unverändert |
| Objekt | assoziatives `array` aller eigenen Eigenschaften, einschließlich Accessors (deren Getter ausgeführt werden) und Eigenschaften mit Funktionswert |
| Arrow-Funktion | ein `array` (meist leer) |
| `function` mit dem Schlüsselwort `function` deklariert oder ein beliebiges Objekt mit Selbstreferenz | keine Konvertierung: Die Kopie endet nie und PHP hat keinen Speicher mehr ([Issue #21](https://github.com/inline0/phasis/issues/21)) |

Eine JS-Callback-Funktion lässt sich derzeit nicht in einer Host-Funktion empfangen. Übergib Daten, keine Funktionen, über diese Grenze.

## Zahlen und Genauigkeit

JavaScript-Zahlen sind IEEE-754-Double-Werte. PHP-Ganzzahlen sind plattformnativ (bei allen modernen Builds 64-Bit). Die Konvertierung erhält Ganzzahlen bis ±2^53 exakt; größere PHP-Ganzzahlen werden zu `number` und verlieren ab 16 Dezimalstellen an Genauigkeit. Verwende `BigInt` in JS, wenn du größere Ganzzahlen exakt berechnen musst, und gib das Ergebnis als Zeichenfolge aus.

## Zeichenfolgen

PHP-Zeichenfolgen sind Byte-Arrays; JS-Zeichenfolgen sind Folgen von UTF-16-Codeeinheiten. Die Engine überbrückt den Unterschied, indem sie PHP-Bytes als UTF-8 interpretiert und intern erneut als UTF-16 kodiert. Bei gültiger UTF-8-Eingabe bleibt alles unverändert. Ungültige UTF-8-Bytes werden unverändert durchgereicht, statt durch U+FFFD ersetzt zu werden.

`String.length`, der indizierte Zugriff (`s[0]`) und das Slicing zählen gemäß der ECMAScript-Spezifikation UTF-16-Codeeinheiten – keine Codepoints und keine Bytes. Strings mit astralen Zeichen wie `'🍕'` haben daher `.length === 2` (die beiden Surrogathälften von U+1F355).

## Stabilität beim Roundtrip

Einfache Daten überstehen einen Roundtrip: Ein PHP-Wert aus Skalaren, Listen und assoziativen Arrays, der über `setGlobal()` eingegeben und über `eval()` oder `call()` zurückgegeben wird, hat dieselbe Struktur – mit Ausnahme ganzzahliger Floats, die als `int` zurückkommen (`1.0` gibt `1` zurück).

Alles andere in den obigen Tabellen, das auf `null`, `undefined` oder `[]` abgebildet wird, ist nicht roundtripfähig.
