
# API

EksekveringsAPI er én klasse: `Phasis\Engine`. Alt andet (`JsValue`, `JsFunction`, `JsObject`, …) er internt i motoren og eksponeres kun for værtsfunktioner, der forbinder PHP↔JS. Den eneste anden offentlige grænseflade er den selvstændige [parser API](https://phasis.inline0.dev/da/docs/parser) (klasserne `Phasis\Parser\Parser` og `Phasis\Ast`), som parser kildekode uden at eksekvere den.

## Konstruktion

```php
use Phasis\Engine;

$engine = new Engine();                 // lazy mode (default)
$engine = new Engine(eager: true);      // install every built-in up front
```

En ny `Engine` stiller hele standardbiblioteket til rådighed — `Object`, `Array`, `String`, `Number`, `Symbol`, `BigInt`, `Promise`, `RegExp`, `Error` og sprogets grundlæggende intrinsics installeres med det samme, så specifikationen virker (klasser kan ikke bruges uden `Function.prototype`, async/await uden `Promise` eller regex-literal uden `RegExp`). Alt andet — `Math`, `JSON`, `Map`, `Set`, `Date`, `TypedArray` og array-buffer-familien, `Atomics`, `Proxy`, `Reflect`, `console`, `Intl`, weak-ref-familien, `Temporal`, `ShadowRealm`, Web Platform Pack og Fetch Pack — registreres som en pladsholderadgangsfunktion og materialiseres ved første læsning. Læsning af globalen er gennemsigtig: `Map`, `typeof Map`, `'Map' in globalThis` og `Object.keys(globalThis).includes('Map')` fungerer alle, og efter første læsning erstattes pladsholderen af en almindelig datadeskriptor.

Angiv `eager: true`, hvis du specifikt har brug for adfærden før lazy-indlæsning — alle indbyggede funktioner installeres ved konstruktion, og hele deres deskriptorflade er synlig ved introspektion. Overensstemmelsestestrammen (`bin/test262`, `bin/wpt`) opretter motorer i eager-tilstand, så test262/WPT ser de deskriptorformer, de forventer.

## eval

```php
public function eval(string $source): mixed
```

Parser og eksekverer `$source` som et klassisk script i motorens globale scope og kører derefter hændelsesløkken, indtil den er tom. Returnerer værdien af det sidst evaluerede udtryk konverteret til PHP (eller `null`, hvis programmet ikke havde et afsluttende udtryk). Se [Værdikonvertering](https://phasis.inline0.dev/da/docs/interop/values) for reglerne.

```php
$result = $engine->eval('1 + 2');           // 3
$result = $engine->eval('JSON.stringify({a: 1})');  // '{"a":1}'
$result = $engine->eval('[1,2,3].map(x => x * 2)'); // [2, 4, 6] (PHP array)
```

Et Promise pakkes ikke ud: `eval('Promise.resolve(5)')` returnerer et tomt array. Da hændelsesløkken er afviklet, når `eval()` returnerer, skal den afsluttede værdi gemmes i en global variabel og læses med endnu et `eval()`:

```php
$engine->eval('let out; (async () => { out = await Promise.resolve(9); })();');
$engine->eval('out');   // 9
```

En eksplicit JS `throw`, som intet fanger, vises som `Phasis\Exceptions\JsThrowable` med meddelelsen `"Name: message"`, og egenskaben `$jsValue` indeholder den udløste `JsValue`. Fejl, som motoren selv udløser, f.eks. ved læsning af en egenskab på `null` eller en udefineret variabel, vises i stedet som `Phasis\Exceptions\TypeError`, `ReferenceError` eller `RangeError`. Alle disse nedarver fra `Phasis\Exceptions\RuntimeError`; parsefejl nedarver fra `Phasis\Exceptions\SyntaxError`. Se [Undtagelser](#exceptions).

```php
try {
    $engine->eval('throw new Error("boom")');
} catch (\Phasis\Exceptions\JsThrowable $e) {
    echo $e->getMessage();
    // Error: boom
}
```

## evalAsModule

```php
public function evalAsModule(string $source, ?string $path = null): mixed
```

Evaluerer `$source` som et ES-modul: `import` / `export`, `await` på øverste niveau og `import.meta` er tilgængelige, og relative specifikationer løses i forhold til `$path` (en absolut filsti). Hændelsesløkken køres som ved `eval()`, og der returneres altid `null`; læs resultater via globaler eller konsolbufferen.

```php
$path = __DIR__ . '/entry.mjs';
$engine->evalAsModule(file_get_contents($path), $path);
```

Indlæseren løser kun relative og absolutte stier; se [Kendte begrænsninger](https://phasis.inline0.dev/da/docs/compatibility/limitations#module-loader).

## execFile

```php
public function execFile(string $path): mixed
```

Læser en fil fra disken og sender indholdet til `eval()`, så den køres som et klassisk script uanset filtypenavnet; en fil med `import` / `export` fejler med en SyntaxError. Filens faktiske sti bliver udgangspunkt for dynamiske `import()`-specifikationer. Brug `evalAsModule()` til modulfiler.

```php
$engine->execFile(__DIR__ . '/script.js');
```

Hvis filen ikke kan læses, udsender PHP en advarsel, og `execFile()` udløser en almindelig `\RuntimeException` ("Cannot read file: …").

## setGlobal

```php
public function setGlobal(string $name, mixed $value): void
```

Binder en global variabel, som er synlig ved alle efterfølgende JS-evalueringer. PHP-værdier konverteres til JS:

| PHP-type | JS-type |
|---|---|
| `null` | `null` |
| `bool` | `boolean` |
| `int` / `float` | `number` |
| `string` | `string` (også når den navngiver en funktion, som `'strlen'`) |
| array (liste) | `Array` (en kopi) |
| array (associativt) | `Object` (en kopi) |
| `Closure`, kaldeligt objekt, `[$object, 'method']` | `function` |
| ethvert andet objekt, generator eller ressource | `undefined` |

Arrays kopieres: ændringer, som JS foretager i dem, ses ikke i PHP-arrayet. Almindelige PHP-objekter kan endnu ikke sendes ([issue #18](https://github.com/inline0/phasis/issues/18)); se [Delte objekter](https://phasis.inline0.dev/da/docs/interop/shared-objects) for løsninger.

```php
$engine->setGlobal('config', ['debug' => true, 'version' => '1.0']);
$engine->setGlobal('log', fn(string $msg) => error_log($msg));
```

Inde i JS:

```javascript
console.log(config.version);   // "1.0"
log('hello');                  // calls the PHP closure
```

## call

```php
public function call(string $name, mixed ...$args): mixed
```

Kalder en global JS-funktion med argumenterne PHP (konverteret som ved `setGlobal`). Returværdien konverteres tilbage til PHP. Kald af et navn, der ikke er defineret, udløser `Phasis\Exceptions\ReferenceError`; kald af en global, der ikke er en funktion, udløser `Phasis\Exceptions\TypeError`.

```php
$engine->eval('function add(a, b) { return a + b; }');
echo $engine->call('add', 2, 3);   // 5
```

## Konsoloutput

```php
public function getConsoleOutput(): string
public function getConsoleLines(): array
public function clearConsole(): void
```

`console.log`, `console.info`, `console.warn`, `console.error` og `console.debug` udskriver ikke noget. Hvert kald tilføjer en formateret linje til en buffer på motoren. `getConsoleLines()` returnerer de bufrede linjer som en liste af strenge, `getConsoleOutput()` returnerer dem samlet med `\n`, og `clearConsole()` tømmer bufferen. Bufferen ryddes ikke mellem `eval()`-kald.

```php
$engine->eval('console.log("a", 1); console.warn({ b: 2 })');
$engine->getConsoleLines();    // ['a 1', '{ b: 2 }']
echo $engine->getConsoleOutput();
// a 1
// { b: 2 }
$engine->clearConsole();
```

## setLimit

```php
public function setLimit(string $limit, int $value): void
```

Angiver en justerbar ressourcegrænse under kørsel. Der findes præcis én:

| Grænse | Standardværdi | Beskrivelse |
|---|---|---|
| `maxLoopIterations` | `100000` | maksimalt antal iterationer pr. `for` / `while` / `do…while`-løb i tree-walking-fortolkeren |

```php
$engine->setLimit('maxLoopIterations', 1_000_000);
```

Alle andre navne ignoreres uden varsel; der er ingen grænser for strenglængde, konsoloutput eller køretid.

Løkkegrænsen håndhæves endnu ikke overalt. Løkker, der kører på bytekode-VM'en, som omfatter funktionskroppe og programmer på øverste niveau, som bytekodekompileren accepterer, tælles aldrig, så `while (true) {}` kører, indtil PHP stopper den ([issue #19](https://github.com/inline0/phasis/issues/19)). Stol ikke på, at den stopper ikke-betroet kode; brug PHP's `max_execution_time` eller en separat proces.

Når grænsen nås, kan fejlen fanges af en JS `try` / `catch` (som en `Error` med meddelelsen "Maximum loop iterations exceeded"). Hvis intet fanger den, når den PHP som `Phasis\Exceptions\InternalError`, der nedarver fra `Phasis\Exceptions\RuntimeError`, ikke fra `JsThrowable`.

To yderligere grænser er indbygget i motoren og kan ikke ændres under kørsel: `Phasis\Runtime\CallStack::maxDepth` (4,096 JS frames) og JsToPhp-transpilerens dybdegrænse for transpilerede closures (8,192 PHP frames). Overskrides en af dem, udløses "Maximum call stack size exceeded", som også når frem til PHP som `InternalError`. De ligger langt over ethvert legitimt JS-program — at hæve dem kræver en kodeændring, ikke en konfigurationsindstilling.

## nulstil

```php
public function reset(): void
```

Rydder alle brugerdefinerede globale værdier og konsolbufferen og genopbygger standardbiblioteket og hændelsesløkken.

På en motor, der er konstrueret i standardtilstanden lazy, mister `reset()` i øjeblikket alle lazily installerede indbyggede funktioner: bagefter er `Map`, `JSON`, `Math`, `console`, `fetch` og resten udefinerede ([issue #22](https://github.com/inline0/phasis/issues/22)). Indtil det er rettet, skal du i stedet konstruere en ny `Engine` eller konstruere med `eager: true`, hvor `reset()` fungerer.

## Hændelsesløkke

Motoren leveres med en JavaScript-hændelsesløkke med `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` og `queueMicrotask` på det globale objekt. Mikrotasks (Promise-handlere, `queueMicrotask`) afvikles mellem hver makrotask; timere udløses i fristorden via en opgavekø pr. realm.

```php
public function runEventLoop(): void
public function tickEventLoop(): bool
public function pendingTaskCount(): int
public function getEventLoop(): \Phasis\Runtime\EventLoop
```

`eval()` og `evalAsModule()` kalder automatisk `runEventLoop()`, når den synkrone kode er færdig. Et script, der planlægger `setTimeout(cb, 5000)` på topniveau, afvikler derfor `cb`, før `eval()` returnerer — løkken bruger **virtuel tid** og springer uret frem til den næste ventende frist i stedet for at sove PHP. Svarer til `node script.js`-semantik.

Til indlejring i realtid (langlivede servere, UI-integration, planlægning efter et rigtigt ur) kører `tickEventLoop()` én iteration (afvikler mikrotasks og udløser eventuelle forfaldne timere) og returnerer. `pendingTaskCount()` rapporterer ventende timere; en indlejrer kan selv styre løkken:

```php
while ($engine->pendingTaskCount() > 0) {
    $engine->tickEventLoop();
    usleep(1000);
}
```

En løbsk `setInterval` er begrænset: `runEventLoop()` stopper efter 100,000 cyklusser og udløser en almindelig `\RuntimeException` — fejlen i JS-koden vises som en tydelig PHP-fejl i stedet for et hæng. Intervallet forbliver planlagt, så alle senere `eval()` på samme motor udløser den samme fejl; kassér motoren bagefter. Ikke-indfangede fejl i et timer-callback sendes via `error_log()`, og løkken fortsætter afviklingen; én fejlbehæftet timer stopper ikke programmet.

## Fetch-hooks

Phasis leveres med `fetch()` aktiveret som standard og bruger `ext-curl` fra PHP som underliggende transport. Tre hooks lader en indlejrer udskifte transporten, styre anmodninger eller aktivere lagring af cookies. Alle tre accepterer enten en callable af typen PHP eller en metode på et objekt; se [Web-API'er](https://phasis.inline0.dev/da/docs/compatibility/web-apis) for kontrakten for værdiformer.

### setFetchTransport

```php
public function setFetchTransport(callable $transport): void
```

Erstatter standardtransporten CurlTransport. Callable-funktionen modtager en anmodningsbeskrivelse og et valgfrit `AbortSignal` JsObject og returnerer en svarbeskrivelse.

```php
// Use Guzzle as the transport instead of curl
$client = new GuzzleHttp\Client();
$engine->setFetchTransport(function (array $req, $signal) use ($client) {
    $r = $client->request($req['method'], $req['url'], [
        'headers' => $req['headers'],
        'body' => $req['body'],
        'http_errors' => false,
    ]);
    return [
        'status' => $r->getStatusCode(),
        'statusText' => $r->getReasonPhrase(),
        'headers' => array_map(
            fn ($name, $values) => array_map(fn ($v) => [$name, $v], $values),
            array_keys($r->getHeaders()),
            $r->getHeaders(),
        ),
        'body' => (string) $r->getBody(),
    ];
});
```

Et almindeligt mønster ved indlejring: installér en mock-transport i tests og den rigtige curl-transport i produktion.

### setFetchPolicy

```php
public function setFetchPolicy(callable $hook): void
```

Hook til politik før afsendelse. Modtager `Request` JsObject; kan udløse en fejl for at afvise, returnere en ændret `Request` for at omskrive eller returnere `null`/ingen værdi for at tillade.

```php
// Block non-HTTPS and inject auth on a specific host
$engine->setFetchPolicy(function ($req) {
    $url = $req->get('url')->value;
    if (str_starts_with($url, 'http://')) {
        throw new \RuntimeException('HTTPS only');
    }
    if (str_contains($url, 'api.example.com')) {
        $req->get('headers')->call('set', 'Authorization', 'Bearer ...');
    }
    return null;
});
```

### setCookieJar

```php
public function setCookieJar(mixed $jar): void
```

Valgfrit cookielager. Accepterer ethvert objekt, der eksponerer `get(string $url): string` og `set(string $url, string $header): void` — på PHP-siden eller JS-siden.

```php
class SimpleJar
{
    private array $cookies = [];
    public function get(string $url): string { return implode('; ', $this->cookies); }
    public function set(string $url, string $header): void { $this->cookies[] = $header; }
}
$engine->setCookieJar(new SimpleJar());
```

Uden `setCookieJar()` ignorerer `fetch` både indgående `Set-Cookie`-headere og udgående `Cookie`-headere — standarden er **ingen cookies**.

## Undtagelser

| Klasse | Nedstammer fra | Hvornår |
|---|---|---|
| `Phasis\Exceptions\JsThrowable` | `RuntimeError` | en eksplicit JS `throw`, som intet fangede |
| `Phasis\Exceptions\TypeError`, `ReferenceError`, `RangeError` | `RuntimeError` | en fejl, som motoren selv udløste (f.eks. `null.x` eller en udefineret variabel), og som intet fangede |
| `Phasis\Exceptions\InternalError` | `RuntimeError` | løkkegrænsen eller kaldestakgrænsen blev overskredet |
| `Phasis\Exceptions\RuntimeError` | `\RuntimeException` | basisklasse for ovenstående |
| `Phasis\Parser\ParseError` | `Phasis\Exceptions\SyntaxError` (`\RuntimeException`) | kilden kan ikke parses |
| `\RuntimeException` | | `execFile()` kan ikke læse filen; hændelsesløkken nåede sin grænse på 100,000 cyklusser |

Hvis du vil fange alle fejl i et script, skal du fange `Phasis\Exceptions\RuntimeError` og `Phasis\Exceptions\SyntaxError` eller blot `\RuntimeException`.

Undtagelser udløst af dine egne PHP-værtsfunktioner er anderledes: De passer i øjeblikket uændret gennem JS `try` / `catch` og når frem til den, der kalder `eval()`, med deres oprindelige klasse ([issue #20](https://github.com/inline0/phasis/issues/20)). Se [Værtsfunktioner](https://phasis.inline0.dev/da/docs/interop/host-functions#throwing-back-into-js).

## Livscyklus

`Engine` er hurtig at konstruere (~få ms ved koldstart), og standardbiblioteket installeres lazy. I de fleste situationer med indlejring er én langlivet motor pr. proces tilstrækkelig. Ved brug med flere lejere skal hver lejer eller anmodning have sin egen motor, så globale værdier ikke lækker mellem dem. Den pålidelige måde at starte på en ren tavle i dag er at konstruere en ny motor (se [nulstil](#reset)).

```php
foreach ($requests as $req) {
    $engine = new Engine();
    $engine->setGlobal('request', ['path' => $req->path, 'query' => $req->query]);
    $engine->execFile($req->scriptPath);
}
```
