
# 값 변환

`setGlobal`, `call`, JS에서 호출되는 호스트 함수 등 PHP↔JS 경계를 통과하는 모든 호출은 변환기를 거칩니다:

- 호스트 값이 엔진으로 들어올 때의 **PHP → JS** (`setGlobal()`, `call()` 인수, 호스트 함수 반환값).
- JS 값이 다시 나갈 때의 **JS → PHP**. 규칙이 약간 다른 두 가지 경로가 있습니다. `eval()` 및 `call()`의 결과와 JS가 호스트 함수에 전달하는 인수입니다.

변환은 복사 방식입니다. 원시 값은 원시 값으로 매핑되고, 배열과 객체는 새 구조로 복사되며, PHP 호출 가능 객체는 JS 함수로 래핑됩니다. 호출 가능 객체 자체를 제외하면 참조로 공유되는 것은 없습니다.

## PHP → JS

| PHP 값 | JS 결과 |
|---|---|
| `null` | `null` |
| `true` / `false` | `true` / `false` |
| `int` | `number` (IEEE 754 double; 2^53를 넘는 정수는 정밀도를 잃음) |
| `float` | `number` (NaN, ±Infinity, ±0 보존) |
| `string` | `string` (UTF-8 PHP 바이트를 UTF-16 코드 단위로 해석), PHP 함수 이름인 경우에도 해당 |
| 리스트 `array` (키 0, 1, 2, … 순서대로) | 새 `Array`, 요소를 재귀적으로 변환 |
| 그 밖의 모든 `array` | 새 일반 `Object`, 값을 재귀적으로 변환 |
| `Closure`, 호출 가능한 객체, `[$object, 'method']`와 같은 호출 가능한 배열 | `function`, PHP 호출 가능 객체를 호출 |
| 그 밖의 모든 객체, `Generator`, 리소스 | `undefined` |

혼합 키 배열(PHP의 `[0 => 'a', 'name' => 'b']`)은 일반 객체가 됩니다. JS의 배열은 연속된 정수 인덱스 공간을 가져야 하기 때문입니다.

```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
```

배열은 복사되므로 JS에서 변경해도 JS에 그대로 남습니다:

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

호출할 수 없는 PHP 객체는 변환되지 않고 `undefined`로 전달됩니다([이슈 #18](https://github.com/inline0/phasis/issues/18)). 먼저 `get_object_vars()` 등을 사용해 배열로 변환하거나 메서드를 클로저로 노출하세요. [공유 객체](https://phasis.inline0.dev/ko/docs/interop/shared-objects)를 참조하세요.

## JS → PHP: `eval()` 및 `call()` 결과

| JS 값 | PHP 결과 |
|---|---|
| `undefined`, `null` | `null` |
| `boolean` | `bool` |
| `number` (정수이며 int 범위 내) | `int` (`-0`는 `0`이 됨) |
| `NaN`, `Infinity`, `-Infinity` | `NAN`, `INF`, `-INF` |
| `number` (기타) | `float` |
| `string` | `string` (UTF-8) |
| `bigint`, `symbol` | `null` |
| `function` | `null` |
| `Array` | 리스트 `array`, 요소를 재귀적으로 변환 |
| `TypedArray` | 숫자로 이루어진 리스트 `array` |
| 그 밖의 모든 객체(일반 객체, `Error`, `Date`, `Map`, `Set`, `Promise`, `ArrayBuffer`, …) | 자체 데이터 속성을 담은 연관 `array`. 접근자와 함수 값 속성은 제외 |

마지막 행은 내부 슬롯에 상태를 저장하는 객체가 빈 배열로 변환된다는 뜻입니다. `new Date(0)`, `new Map([["a", 1]])`, `Promise.resolve(5)`는 모두 `[]`을 반환합니다. `Error`는 자체 `message` 및 `stack`을 반환합니다. 자기 자신을 참조하는 객체는 내부 참조를 `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)');              // []
```

이러한 값 중 하나가 PHP에서 필요하면 JS 내부에서 변환하세요:

```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: 호스트 함수 인수

JS가 PHP 호스트 함수에 전달하는 인수는 원시 값, 배열, 일반 객체에 대해 같은 규칙을 따르며 다음과 같은 차이가 있습니다:

| JS 값 | PHP 인수 |
|---|---|
| `TypedArray`, `ArrayBuffer`, `DataView` | 변환되지 않은 엔진 자체의 `JsValue` 객체 |
| 객체 | 접근자(게터 실행)와 함수 값 속성을 포함한 모든 자체 속성의 연관 `array` |
| 화살표 함수 | `array` (대개 비어 있음) |
| `function` 키워드로 선언된 `function` 또는 자기 자신을 참조하는 객체 | 변환하지 않음: 복사가 끝나지 않아 PHP 메모리가 부족해집니다([이슈 #21](https://github.com/inline0/phasis/issues/21)) |

아직 호스트 함수에서 JS 콜백을 받을 방법은 없습니다. 이 경계를 넘어갈 때는 함수가 아닌 데이터를 전달하세요.

## 숫자와 정밀도

JavaScript 숫자는 IEEE 754 배정밀도입니다. PHP 정수는 플랫폼 고유 형식입니다(모든 최신 빌드에서 64비트). 변환은 ±2^53까지의 정수를 정확하게 보존합니다. 더 큰 PHP 정수는 `number`이 되며 소수점 이하 16자리 이후의 정밀도를 잃습니다. 그보다 큰 정수도 정확히 계산해야 한다면 JS 내부에서 `BigInt`를 사용하고 결과를 문자열로 전달하세요.

## 문자열

PHP 문자열은 바이트 배열이고 JS 문자열은 UTF-16 코드 단위 시퀀스입니다. 엔진은 PHP 바이트를 UTF-8로 해석한 뒤 내부에서 UTF-16으로 다시 인코딩하여 두 형식 사이를 변환합니다. 유효한 UTF-8 입력은 손실 없이 변환됩니다. 유효한 UTF-8이 아닌 바이트는 U+FFFD로 대체되지 않고 그대로 전달됩니다.

`String.length`, 인덱스 접근(`s[0]`), 슬라이싱은 모두 ECMAScript 사양에 따라 바이트나 코드 포인트가 아닌 UTF-16 코드 단위를 셉니다. 따라서 `'🍕'`와 같은 보충 평면 문자가 포함된 문자열은 `.length === 2`을 갖습니다(U+1F355의 서로게이트 상위 및 하위 쌍).

## 왕복 변환 안정성

일반 데이터는 왕복 변환 후에도 유지됩니다. 스칼라, 리스트, 연관 배열로 이루어진 PHP 값이 `setGlobal()`을 통해 들어간 뒤 `eval()` 또는 `call()`을 통해 돌아오면 동일한 구조가 됩니다. 단, 정수형 실수는 `int`로 돌아오며(`1.0`는 `1`을 반환) 예외입니다.

위 표에서 `null`, `undefined` 또는 `[]`로 매핑되는 나머지 값은 왕복 변환되지 않습니다.
