Browse documentation

Embedding the Engine

For Pascal developers who want to embed the GocciaScript engine in their own applications.

Executive Summary

  • Quick start — TGocciaRuntime.Create(...) creates the runtime layer for file loading and runtime extension installation; apply a runtime profile or install concrete runtime extensions for runtime globals and goccia: runtime modules. TGocciaEngine.Create(...) remains available for core-language-only embedders and requires an explicit executor (TGocciaInterpreterExecutor for tree-walk or TGocciaBytecodeExecutor for bytecode VM)
  • Sandboxing — Fix what the engine may reach with an immutable capability set (None by default); choose runtime extensions explicitly; inject custom globals via DefineLexicalBinding; enforce execution limits via timeout or instruction cap
  • Module resolution — Pluggable resolver with extensionless imports, import maps, custom content providers, virtual modules, and host modules
  • Transparent GC — Mark-and-sweep GC initializes automatically; FPU exceptions are masked for IEEE 754 semantics

Native application embedding is an important secondary GocciaScript goal. TGocciaRuntime is the shared Pascal embedding entry point for the runtime layer: filesystem module content loading, runtime module dispatch, and extension installation. Runtime globals such as console, fetch, and URL, plus import-only modules such as goccia:json5, goccia:toml, goccia:yaml, goccia:csv, goccia:tsv, goccia:jsonl, goccia:semver, goccia:test, and node:async_hooks, usually come from ApplyLoaderRuntimeProfile. TGocciaEngine remains available through Runtime.Engine and as a core-language-only API for embedders that intentionally do not want runtime globals or runtime modules.

Quick Start

The simplest way to run a script with the loader runtime surface (uses Classes, Goccia.Runtime, Goccia.RuntimeProfiles.Loader;):

Source := TStringList.Create;
Source.Text := 'console.log("hello from GocciaScript");';
Runtime := TGocciaRuntime.Create('<inline>', Source);
try
ApplyLoaderRuntimeProfile(Runtime.Core);
Runtime.Execute;
finally
Runtime.Free;
Source.Free;
end;

For files, load app.js into the Source list, pass the real filename to TGocciaRuntime.Create('app.js', Source), and apply the runtime profile or runtime extensions your host needs before executing. Use TGocciaEngine.RunScript* only for core-language scripts that do not need runtime file loading, runtime globals, or goccia: runtime modules.

Engine API

Class Methods (One-Shot Execution)

These helpers create, execute, and clean up in a single call. The TGocciaEngine methods create a core-language-only engine. The TGocciaRuntime methods attach the runtime layer for file loading, but they do not apply a runtime profile; install runtime extensions explicitly when scripts need runtime globals such as console, fetch, and URL, or import-only modules such as goccia:json5, goccia:toml, goccia:yaml, goccia:csv, goccia:tsv, goccia:jsonl, goccia:semver, or node:async_hooks.

MethodDescription
TGocciaEngine.RunScript(Source)Execute source text with core language built-ins only
TGocciaEngine.RunScript(Source, FileName)Execute source text with a diagnostic filename
TGocciaEngine.RunScriptFromStringList(Source, FileName)Execute from a caller-provided TStringList
TGocciaRuntime.RunScript(Source, FileName)Execute source text through the runtime layer
TGocciaRuntime.RunScriptFromFile(FileName)Load and execute a file through the runtime layer
TGocciaRuntime.RunScriptFromStringList(Source, FileName)Execute from a TStringList through the runtime layer
All methods return TGocciaScriptResult — a record containing the result value, per-phase timing (in microseconds), and the filename.

SourceType is an engine-level language option, not a runtime option. Set Engine.SourceType (or use the CLI --source-type=script|module) to choose script source or module source for the entry file. File names ending in .mjs or .mts infer module source unless an explicit source type is provided. File loading is separate: TGocciaEngine one-shot helpers accept source text or caller-provided TStringList instances only, while TGocciaRuntime.RunScriptFromFile is the runtime convenience API for loading an entry file.

TGocciaEngine.Execute remains the public "run the whole source pipeline and execute" API for embedders. Hosts that need parse artifacts without executing can call TGocciaSourcePipeline.Parse directly; its result object owns the AST and source map until the caller frees the result or transfers ownership with TakeProgramNode / TakeSourceMap. Hosts should use the source-pipeline entry points for module source, dynamic Function validation, and expression fragments rather than constructing TGocciaParser directly; this keeps parser policy in one place.

Instance Usage (Long-Lived Engine)

For interactive sessions (REPL, editor integration) or when you need to execute multiple scripts in the same scope, create an engine instance directly:

uses
Classes,
​
Goccia.Engine,
Goccia.Executor.Interpreter,
Goccia.Values.Primitives;
​
var
Engine: TGocciaEngine;
Executor: TGocciaInterpreterExecutor;
Source: TStringList;
ScriptResult: TGocciaScriptResult;
begin
Source := TStringList.Create;
Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('session', Source, Executor);
try
// First execution — defines a variable
Source.Text := 'const x = 42;';
Engine.Execute;
​
// Second execution — uses the variable from the first
Source.Text := 'x + 1;';
ScriptResult := Engine.Execute;
WriteLn(ScriptResult.Result.ToStringLiteral.Value); // 43
finally
Engine.Free;
end;
finally
Executor.Free;
Source.Free;
end;
end;

The TStringList is passed by reference — update its contents and call Execute again to run new code in the same global scope. Variables, functions, and classes defined in previous executions remain available.

Engine Constructor and Executor Ownership

Callers must pass an explicit executor — TGocciaInterpreterExecutor (in Goccia.Executor.Interpreter) for tree-walk or TGocciaBytecodeExecutor (in Goccia.Executor.Bytecode) for the bytecode VM. The engine does not own the executor; the caller frees it after the engine. TGocciaRuntime's file/source convenience overloads handle this internally for embedders who want the default interpreter setup. Every constructor also has an overload taking a TGocciaCapabilities that fixes what the engine may reach outside the process; the overloads without one use TGocciaCapabilities.None. See Permissions. For net, AllowsNetHost(host, port) judges a URL before any lookup and AllowsNetAddress(host, port, address) judges the address the host resolved to. A private, loopback, or link-local address passes only when every layer names it — with the standalone private scope or an IP or CIDR scope covering it — so TGocciaCapabilities.None.Allow(gcNet, 'private') reaches local services and nothing public, while a host allow alone never reaches a name that resolves privately. Config trust is a CLI policy, not an engine one: nothing in the engine reads a goccia.json or a trust store. An embedder that honors project config files reads a file's permission request with ReadConfigPermissionRequest (in Goccia.CLI.Permissions, from the entries CLI.ConfigFile.ParseConfigFile returns) and decides for itself whether to grant it — for example by passing it to ResolveCapabilities with AConfigGrantsAccepted set to its own decision. NormalizedPermissionBlock and PermissionBlockHash give the same canonical block and SHA-256 the CLI trusts, if the host wants to keep its own record. Set Engine.CapabilityProvenance to say where the set came from in the capabilities.effective audit event. See Permissions — Config trust.

Automatic Semicolon Insertion

ASI is disabled by default. To enable ECMAScript-compliant automatic semicolon insertion (ES2026 §12.10), include cfASI in the engine compatibility set after creating the engine:

Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Executor);
Engine.Compatibility := [cfASI]; // Semicolons are now optional per ES2026 rules
Engine.Execute;
finally
Engine.Free;
Executor.Free;
end;

When enabled, the parser inserts virtual semicolons at newline boundaries, before }, and at EOF. Restricted productions (return, throw, break) follow the [no LineTerminator here] rules.

Timing

All RunScript* methods and the Execute method return a TGocciaScriptResult record that includes nanosecond-precision timing for each pipeline phase:

uses
TimingUtils,
​
Goccia.Engine;
​
var
ScriptResult: TGocciaScriptResult;
begin
ScriptResult := TGocciaEngine.RunScript(Source, 'bench.js');
WriteLn('Lex: ', FormatDuration(ScriptResult.LexTimeNanoseconds));
WriteLn('Parse: ', FormatDuration(ScriptResult.ParseTimeNanoseconds));
WriteLn('Execute: ', FormatDuration(ScriptResult.ExecuteTimeNanoseconds));
WriteLn('Total: ', FormatDuration(ScriptResult.TotalTimeNanoseconds));
end;

FormatDuration (from TimingUtils) automatically selects the appropriate unit: ns for values below 0.5μs, μs for values below 0.5ms, ms with two decimal places for values up to 10s, and s above that.

TimingUtils provides three clock functions: GetNanoseconds and GetMilliseconds for monotonic duration timing (clock_gettime with CLOCK_MONOTONIC on Linux and CLOCK_MONOTONIC_RAW on macOS, QueryPerformanceCounter on Windows), and GetEpochNanoseconds for wall-clock epoch time (clock_gettime(CLOCK_REALTIME) on Unix/macOS, GetSystemTimeAsFileTime on Windows).

Host-Controlled Time And Randomness

Before attaching runtime extensions, use the engine-owned host environment; see Host Environment for deterministic setup and providers.

Engine Lifecycle & Realm Isolation

Every TGocciaEngine owns an initial TGocciaRealm (Goccia.Realm.pas) — the engine's ECMA-262 Realm Record for mutable intrinsic prototype objects (Array.prototype, Object.prototype, Map.prototype, every error prototype, every Temporal prototype, and so on), global state links, the realm [[TemplateMap]], loaded-module host state, and host-defined data. The realm is created in the engine constructor and torn down in the engine destructor; tear-down unpins every prototype and cached template object the realm owns so the GC can collect them before the next engine starts up.

This is the strongest isolation boundary the engine provides. Two engines created back-to-back on the same thread see pristine intrinsics — userland mutations on one engine's Array.prototype cannot leak into the next engine's Array.prototype, even if the mutation added a non-configurable property that JS-level cleanup cannot reverse.

When this matters for embedders

  • Test runners and conformance harnesses — Per-file engine recreation is the cleanest way to give each test file fresh intrinsics. GocciaTestRunner does this automatically; if you build a custom runner, free the engine between files rather than reusing it.
  • Sandboxes evaluating untrusted scripts — Each script can mutate Object.prototype. Recreating the engine between scripts is the only reliable way to guarantee the next script starts from a clean slate.
  • REPLs and long-lived sessions — These intentionally share intrinsics across Execute calls (so users can mutate Array.prototype from one line and observe the change on the next). Reuse the same engine instance.

What you do not need to do

  • Do not call SetCurrentRealm directly. Engine, interpreter, and bytecode entry points make the realm current through TGocciaExecutionContextStack; CurrentRealm remains only as the lookup facade used by value units.
  • Do not pin or unpin prototype objects manually after engine startup. The realm pins everything stored in a slot via SetSlot, and unpins them all at tear-down.
  • Do not cache prototype object pointers in long-lived Pascal state. Those objects are realm-scoped — they become invalid the moment the engine that owns them is freed. If you need the current Array.prototype, look it up live (e.g. via Engine.Interpreter.GlobalScope).

Threading

CurrentRealm is backed by thread-local execution-context state. Each worker thread that executes an engine gets its own realm stack. The thread pool used by GocciaTestRunner --jobs=N relies on this: each worker creates, enters, and destroys engines on its own thread, and realm tear-down on one worker has no effect on intrinsics seen by the others.

Module Resolution

The engine uses a pluggable module resolver (TGocciaModuleResolver) that supports extensionless imports, import-map-style aliases, and index file resolution.

Extension-Free Imports

Import paths can omit file extensions. The engine's own list comes first, in order: .js, .jsx, .ts, .tsx, .mjs, .mts, .json, .txt, .md. Installed runtime extensions append the structured-data extensions they own, so a host that applies the loader runtime profile also resolves .json5, .jsonc, .jsonl, .toml, .yaml, .yml, .csv, and .tsv:

// These all resolve through the shared module extension list:
import { add } from "./math-utils.js"; // explicit extension
import { add } from "./math-utils"; // extension resolved automatically
import { content } from "./notes"; // resolves to ./notes.txt or ./notes.md

Directory imports resolve to index files:

import { setup } from "./utils"; // resolves to ./utils/index.js (or .ts, .jsx, etc.)

Path Aliases

Aliases follow WHATWG import map matching rules:

Engine.AddAlias('lodash', 'vendor/lodash/index.js'); // exact match only
Engine.AddAlias('@/', 'src/'); // prefix match
Engine.AddAlias('@/components/', 'ui/lib/'); // more specific prefix

For an embedded engine, the alias target is resolved relative to the resolver's base directory, which defaults to the entry file's directory. In the shared CLI hosts, relative --alias targets use the invocation directory, while aliases loaded from configuration use the active project configuration file's directory, including aliases inherited through extends.

Exact vs prefix matching: A key without a trailing / is an exact match only. A key with a trailing / is a prefix match and appends the unmatched suffix to the target. This means lodash matches import "lodash" but not import "lodash/fp", while @/ matches @/utils/math.

Longest-prefix matching: When multiple prefix aliases overlap (e.g., @/ and @/components/), the resolver always picks the longest matching key. This means @/components/Button uses the @/components/ alias, not @/.

Scripts can then import using the alias:

import { formatDate } from "@/utils/dates";

TGocciaModuleResolver also exposes LoadImportMap(path) and DiscoverProjectConfig(startDirectory) helpers for browser-style import map JSON and goccia.json project configuration files. The shared CLI hosts (GocciaRunner, GocciaTestRunner, and GocciaBenchmarkRunner) all use Goccia.Modules.Configuration.ConfigureModuleResolver(...) on top of this resolver surface.

Config file discovery is automatic for CLI apps — TGocciaCLIApplication discovers goccia.toml / goccia.json5 / goccia.json (priority order: TOML > JSON5 > JSON) from the entry file's directory upward and applies config values before execution. When embedding the engine directly, this does not happen automatically. To replicate it, use the general-purpose CLI.ConfigFile unit (DiscoverConfigFile, ApplyConfigFile). Note that ApplyConfigFile only handles .json by default — to support .json5 and .toml, call EnsureConfigParsersRegistered (in Goccia.CLI.Application) first, or register your own parsers with RegisterConfigParser. For import-map resolution only, use TGocciaModuleResolver.DiscoverProjectConfig and LoadImportMap. See Configuration File for the full reference.

Bare Specifiers and node_modules

A bare specifier is refused for an embedded engine too; grant node_modules through the engine's capability set, optionally capping the ancestor walk. See Module Resolution for the supported exports subset, the module-field deviation from Node, and the CommonJS refusal.

Caps := TGocciaCapabilities.None.Allow(gcImport, 'node_modules'); // walk up from each importer
Caps := TGocciaCapabilities.None.Allow(gcImport, 'node_modules=/srv/app'); // ...but never above /srv/app

Custom Resolver

For resolution logic outside that surface (URL imports, in-memory modules, or a package layout of your own), subclass TGocciaModuleResolver and override the Resolve method:

uses
Goccia.Engine,
Goccia.Executor.Interpreter,
Goccia.Modules.Resolver;
​
type
TMyResolver = class(TGocciaModuleResolver)
public
function Resolve(const AModulePath, AImportingFilePath: string): string; override;
end;
​
function TMyResolver.Resolve(const AModulePath, AImportingFilePath: string): string;
begin
// Custom logic here — fall back to inherited for standard resolution
Result := inherited Resolve(AModulePath, AImportingFilePath);
end;
​
var
Resolver: TMyResolver;
Engine: TGocciaEngine;
Executor: TGocciaInterpreterExecutor;
begin
Resolver := TMyResolver.Create('/path/to/project');
Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Resolver, Executor);
try
Engine.Execute;
finally
Engine.Free;
end;
finally
Executor.Free;
Resolver.Free; // caller owns the resolver when injected
end;
end;

When no custom resolver is provided, the engine creates a default TGocciaModuleResolver whose base directory is the entry file's directory.

Custom Content Provider

Resolution and content loading are separate concerns. A custom resolver decides which module path to load; a custom content provider decides how the module text or JSON is retrieved once that path is resolved.

Use TGocciaModuleContentProvider when your modules come from memory, an archive, a database, or some other non-filesystem source:

uses
Classes,
SysUtils,
​
Goccia.Engine,
Goccia.Executor.Interpreter,
Goccia.Modules.ContentProvider,
Goccia.Modules.Loader,
Goccia.Modules.Resolver;
​
type
TMemoryResolver = class(TGocciaModuleResolver)
public
function Resolve(const AModulePath, AImportingFilePath: string): string; override;
end;
​
TMemoryContentProvider = class(TGocciaModuleContentProvider)
public
function Exists(const APath: string): Boolean; override;
function LoadContent(const APath: string): TGocciaModuleContent; override;
function TryGetLastModified(const APath: string;
out ALastModified: TDateTime): Boolean; override;
end;
​
function TMemoryResolver.Resolve(const AModulePath,
AImportingFilePath: string): string;
begin
Result := AModulePath;
end;
​
function TMemoryContentProvider.Exists(const APath: string): Boolean;
begin
Result := APath = 'memory:/dep.js';
end;
​
function TMemoryContentProvider.LoadContent(
const APath: string): TGocciaModuleContent;
begin
if APath = 'memory:/dep.js' then
Exit(TGocciaModuleContent.Create('export const value = 42;', 0));
​
raise Exception.Create('Module content not found: ' + APath);
end;
​
function TMemoryContentProvider.TryGetLastModified(const APath: string;
out ALastModified: TDateTime): Boolean;
begin
ALastModified := 0;
Result := False;
end;
​
var
Engine: TGocciaEngine;
Executor: TGocciaInterpreterExecutor;
ModuleLoader: TGocciaModuleLoader;
Resolver: TMemoryResolver;
Provider: TMemoryContentProvider;
begin
Resolver := TMemoryResolver.Create;
Provider := TMemoryContentProvider.Create;
Executor := TGocciaInterpreterExecutor.Create;
try
ModuleLoader := TGocciaModuleLoader.Create('memory:/app.js', Resolver,
Provider);
try
Engine := TGocciaEngine.Create('memory:/app.js', Source,
ModuleLoader, Executor);
try
Engine.Execute;
finally
Engine.Free;
end;
finally
ModuleLoader.Free; // caller owns injected module loaders
end;
finally
Executor.Free;
Provider.Free; // caller owns injected providers
Resolver.Free;
end;
end;

TGocciaEngine also accepts an injected module loader via its constructor. When no loader is supplied, it creates a default TGocciaModuleLoader with the standard resolver but no filesystem content provider. TGocciaRuntime installs the filesystem provider when attached, and the loader checks every read through it against the set: static imports inside the project are exempt, everything else needs a read grant (Permissions). Untrusted-source hosts can deny read and supply virtual modules, host modules, or a bounded custom content provider explicitly. Core-language-only embedders that need imports should inject their own provider. With no provider installed, a module load that gets as far as retrieval is refused with a script-catchable Error carrying code === "ERR_MODULE_LOADING_UNSUPPORTED" rather than a Pascal exception. That covers retrieval only: resolution runs first, so a specifier the resolver rejects — with the default resolver, one whose file is absent from the host filesystem — fails before the provider is consulted, carries no code, and raises TGocciaRuntimeError across the engine boundary for a static import. Keep guarding the boundary; see Module loading errors.

Virtual Modules

Prefer virtual modules for host configuration consumed through ordinary ES imports. See Virtual Module Configuration for their schema and behavior.

Host Modules

Host modules expose values created programmatically by an embedder. They are checked before filesystem resolution:

uses
Goccia.Modules;
​
var
Module: TGocciaModule;
begin
Module := TGocciaModule.Create('my-lib');
Module.ExportsTable.Add('version', TGocciaStringLiteralValue.Create('1.0.0'));
Engine.RegisterHostModule('my-lib', Module);
end;

Scripts can then import from the host module by name:

import { version } from "my-lib";
console.log(version); // "1.0.0"

RegisterHostModuleProvider is lazy; former global module names remain supported without warnings.

Console Output Capture

By default, console.log and friends write directly to stdout. The OutputCallback property lets you intercept all console output programmatically:

uses
Classes,
​
Goccia.Builtins.Console,
Goccia.Runtime,
Goccia.RuntimeExtensions.Console,
Goccia.RuntimeProfiles.Loader;
​
type
TMyLogger = class
procedure OnConsoleOutput(const AMethod, ALine: string);
end;
​
procedure TMyLogger.OnConsoleOutput(const AMethod, ALine: string);
begin
// AMethod is 'log', 'warn', 'error', 'info', 'debug', 'dir',
// 'assert', 'count', 'timeEnd', 'timeLog', 'trace', 'table', 'group'
// ALine is the fully formatted output string
WriteLn('[', AMethod, '] ', ALine);
end;
​
var
ConsoleExtension: TGocciaConsoleRuntimeExtension;
Runtime: TGocciaRuntime;
Source: TStringList;
Logger: TMyLogger;
begin
Logger := TMyLogger.Create;
Source := TStringList.Create;
Source.Text := 'console.log("hello"); console.warn("careful");';
Runtime := TGocciaRuntime.Create('app.js', Source);
try
ApplyLoaderRuntimeProfile(Runtime.Core);
ConsoleExtension := TGocciaConsoleRuntimeExtension(
Runtime.FindRuntimeExtension(TGocciaConsoleRuntimeExtension));
ConsoleExtension.BuiltinConsole.OutputCallback := Logger.OnConsoleOutput;
Runtime.Execute;
// Output:
// [log] hello
// [warn] Warning: careful
finally
Runtime.Free;
Source.Free;
Logger.Free;
end;
end;

The callback type is:

TGocciaConsoleOutputCallback = procedure(const AMethod, ALine: string) of object;

When OutputCallback is assigned, it takes priority over both OutputLines (the TStrings capture property) and the default WriteLn path. When not assigned, existing behavior is unchanged.

LogCallback (Independent Logging Channel)

LogCallback fires on every console call regardless of the Enabled flag, independent of the primary output path. This makes it safe for worker threads where Enabled is False to suppress stdout:

ConsoleExtension.BuiltinConsole.LogCallback := MyHandler.OnLog;
ConsoleExtension.BuiltinConsole.Enabled := False; // no stdout, but LogCallback still fires

The CLI hosts based on TGocciaCLIApplication (GocciaRunner, GocciaTestRunner, GocciaBenchmarkRunner, and GocciaREPL) apply a runtime profile and use LogCallback internally for --log=<file>, which captures console output to a log file in [method] line format. The TestRunner silences workers via Enabled := False (not by replacing JS methods), so LogCallback fires on every console call even in parallel mode. File writes are serialized with a critical section so --log is thread-safe even with --jobs=N.

Built-in Registration

Core language built-ins (Math, Object, Array, JSON, Promise, Temporal, typed arrays, etc.) are registered by TGocciaEngine. Runtime globals that are not part of the language core (Console, TextEncoder/TextDecoder, URL, fetch, performance, etc.) and import-only runtime modules (goccia:csv, goccia:json5, goccia:jsonl, goccia:toml, goccia:tsv, goccia:yaml, goccia:semver, goccia:test, node:async_hooks) are provided by concrete runtime units. Hosts can call ApplyLoaderRuntimeProfile for the ordinary CLI runtime surface or install only the extension classes they need for a smaller runtime surface. ApplyLoaderRuntimeProfile(ARuntime, False) suppresses the goccia:test registration for a host that installs TGocciaTestingLibraryRuntimeExtension itself — the testing globals are opt-in through that extension and are never part of the profile. The goccia: modules expose named exports only; callers can use namespace imports such as import * as CSV from "goccia:csv" when they want namespace-style access.

When you already have an engine, pass it to the runtime constructor:

Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Executor);
Runtime := TGocciaRuntime.Create(Engine);
try
ApplyLoaderRuntimeProfile(Runtime.Core);
Runtime.Execute;
finally
Runtime.Free;
Engine.Free;
end;
finally
Executor.Free;
end;

Passing an engine does not transfer ownership by default. Use TGocciaRuntime.Create(Engine, True) when the runtime should free the engine. The executor is always owned by the caller — free it after the engine.

Runtime Extensions

Runtime extensions are ordinary Pascal classes installed on TGocciaRuntimeCore:

Extension/profileProvidesNotes
ApplyLoaderRuntimeProfileordinary CLI runtime surface: console, goccia: data-format/SemVer modules, text assets, performance, text encoding, URL/fetch, and related runtime globalsUsed by GocciaRunner (both modes) and GocciaREPL
TGocciaTestingLibraryRuntimeExtensiondescribe, test, expectTesting framework; TestRunner installs this through ApplyTestRunnerRuntimeProfile
TGocciaBenchmarkRuntimeExtensionsuite, benchBenchmark framework; BenchmarkRunner installs this through ApplyBenchmarkRunnerRuntimeProfile
TGocciaFFIRuntimeExtensionFFI.open, FFILibrary, FFIPointerNative shared-library FFI; needs the ffi capability, which CLI tools grant for --allow-ffi or "allow-ffi" in a config file's permissions block

When embedding, grant ffi in the engine's capability set and call InstallFFIIfGranted(Runtime); the extension refuses to attach without the grant, and FFI.open checks each library path against the ffi scopes.

To add the test framework for a custom test runner:

Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('tests/my-test.js', Source, Executor);
Runtime := TGocciaRuntime.Create(Engine, True);
try
ApplyTestRunnerRuntimeProfile(Runtime.Core);
Runtime.Execute;
finally
Runtime.Free;
end;
finally
Executor.Free;
end;

Runtime globals and runtime modules can be reduced by installing only concrete extensions, for example Runtime.Install(TGocciaConsoleRuntimeExtension.Create).

Preprocessors and Compatibility

SystemTypeDefaultPurpose
PreprocessorsTGocciaPreprocessors[ppJSX]Source transformations before parsing
CompatibilityTGocciaCompatibilityFlags[]ECMAScript conformance and legacy-behavior toggles; leave empty for the recommended defaults
WarningUnsupportedFeaturesBooleanFalseParser diagnostic policy for disabled syntax; True restores warning/no-op recovery without enabling compatibility semantics
SourceTypeTGocciaSourceTypestScriptLoad entry as script source (default) or module source; .mjs and .mts file names infer stModule
StrictTypesBooleanFalseRuntime enforcement of type annotations (works in both interpreter and bytecode); setter propagates to the active executor and interpreter scope
Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Executor);
Engine.Preprocessors := []; // Disable JSX
Engine.Compatibility := [
cfASI,
cfNonStrictMode,
cfArgumentsObject,
cfLabel,
cfTraditionalFor,
cfForIn,
cfWhileLoops,
cfExperimentalJSModuleSource
]; // Enable selected source-pipeline flags
Engine.WarningUnsupportedFeatures := True; // Optional migration mode for disabled syntax diagnostics
Engine.SourceType := stModule; // Run entry as module source (top-level this is undefined; import.meta resolves)
Engine.StrictTypes := True; // Enforce type annotations in both execution modes
finally
Executor.Free;
end;

When SourceType is stModule, Execute runs the entry program in a fresh module scope (skModule) with this = undefined, mirroring the semantics imported modules already receive from the module loader (ES2026 §16.2.1.6.4). The CLI surface for this is --source-type=script|module and the matching goccia.json key "source-type". Without an explicit source type, .mjs and .mts entry files are loaded as module source.

Top-level binding persistence differs between source types. With SourceType = stScript, the engine reuses Interpreter.GlobalScope across every call to Execute, which is what makes the long-lived engine pattern above work: const x = 42 defined in one Execute is visible to the next. With SourceType = stModule, each Execute allocates a brand-new skModule child scope, so top-level let/const/class declarations live and die with that single call. If callers need cross-Execute persistence, keep SourceType at stScript (the default) or expose the desired symbols through the global scope (e.g. Engine.RegisterGlobal(...) or Engine.Interpreter.GlobalScope.DefineLexicalBinding(...)).

Injecting Custom Globals

You can inject Pascal functions and values into the script's global scope by working with the engine's interpreter scope directly.

Injecting a Value

uses
Classes,
SysUtils,
​
Goccia.Engine,
Goccia.Executor.Interpreter,
Goccia.Scope,
Goccia.Values.Primitives;
​
var
Engine: TGocciaEngine;
Executor: TGocciaInterpreterExecutor;
Source: TStringList;
begin
Source := TStringList.Create;
Source.Text := 'APP_VERSION;';
Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Executor);
try
// Inject a constant into the global scope
Engine.Interpreter.GlobalScope.DefineLexicalBinding(
'APP_VERSION',
TGocciaStringLiteralValue.Create('1.2.3'),
dtConst
);
Engine.Execute; // evaluates "1.2.3"
finally
Engine.Free;
end;
finally
Executor.Free;
Source.Free;
end;
end;

Injecting a Native Function

Native functions are Pascal methods exposed to GocciaScript. They receive arguments and a this value, and return a TGocciaValue.

uses
Classes,
DateUtils,
SysUtils,
​
Goccia.Arguments.Collection,
Goccia.Engine,
Goccia.Executor.Interpreter,
Goccia.Scope,
Goccia.Values.NativeFunction,
Goccia.Values.Primitives;
​
type
TMyHost = class
function GetTimestamp(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
end;
​
function TMyHost.GetTimestamp(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
begin
Result := TGocciaNumberLiteralValue.Create(DateTimeToUnix(Now));
end;
​
var
Engine: TGocciaEngine;
Executor: TGocciaInterpreterExecutor;
Source: TStringList;
Host: TMyHost;
Func: TGocciaNativeFunctionValue;
begin
Host := TMyHost.Create;
Source := TStringList.Create;
Source.Text := 'getTimestamp();';
Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Executor);
try
// Create a native function: callback, name, arity (-1 for variadic)
Func := TGocciaNativeFunctionValue.Create(Host.GetTimestamp, 'getTimestamp', 0);
Engine.Interpreter.GlobalScope.DefineLexicalBinding('getTimestamp', Func, dtConst);
Engine.Execute;
finally
Engine.Free;
end;
finally
Executor.Free;
Source.Free;
Host.Free;
end;
end;

The callback signature is:

TGocciaNativeFunctionCallback = function(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue of object;
  • AArgs — The arguments collection. Use AArgs.Length and AArgs.GetElement(I) to access arguments.
  • AThisValue — The this binding (relevant for method calls).
  • Return value — Must be a TGocciaValue. Use TGocciaUndefinedLiteralValue.UndefinedValue for void functions.

Injecting a Native Object with Methods

For a more structured API, create a TGocciaObjectValue and register native methods on it:

uses
Classes,
SysUtils,
​
Goccia.Arguments.Collection,
Goccia.Engine,
Goccia.Executor.Interpreter,
Goccia.Scope,
Goccia.Values.NativeFunction,
Goccia.Values.ObjectValue,
Goccia.Values.Primitives;
​
type
TFileSystemAPI = class
function ReadFile(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
function Exists(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
end;
​
function TFileSystemAPI.ReadFile(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
var
Path: string;
Content: TStringList;
begin
Path := AArgs.GetElement(0).ToStringLiteral.Value;
Content := TStringList.Create;
try
Content.LoadFromFile(Path);
Result := TGocciaStringLiteralValue.Create(Content.Text);
finally
Content.Free;
end;
end;
​
function TFileSystemAPI.Exists(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
begin
Result := TGocciaBooleanLiteralValue.Create(
FileExists(AArgs.GetElement(0).ToStringLiteral.Value)
);
end;
​
var
Engine: TGocciaEngine;
Executor: TGocciaInterpreterExecutor;
Source: TStringList;
API: TFileSystemAPI;
FSObject: TGocciaObjectValue;
begin
API := TFileSystemAPI.Create;
Source := TStringList.Create;
Source.Text := 'fs.readFile("data.txt");';
Executor := TGocciaInterpreterExecutor.Create;
try
Engine := TGocciaEngine.Create('app.js', Source, Executor);
try
FSObject := TGocciaObjectValue.Create;
FSObject.RegisterNativeMethod(
TGocciaNativeFunctionValue.Create(API.ReadFile, 'readFile', 1));
FSObject.RegisterNativeMethod(
TGocciaNativeFunctionValue.Create(API.Exists, 'exists', 1));
​
Engine.Interpreter.GlobalScope.DefineLexicalBinding('fs', FSObject, dtConst);
Engine.Execute;
finally
Engine.Free;
end;
finally
Executor.Free;
Source.Free;
API.Free;
end;
end;

Scripts can then call fs.readFile("path") and fs.exists("path") as if they were built-in.

Reading Return Values

TGocciaEngine.RunScript, TGocciaRuntime.RunScript, and Execute return a TGocciaScriptResult record. Its Result field holds the last evaluated expression as a TGocciaValue; use the type conversion methods on that value to extract Pascal values:

var
ScriptResult: TGocciaScriptResult;
begin
ScriptResult := TGocciaEngine.RunScript('40 + 2;');
​
// Type checking
if ScriptResult.Result is TGocciaNumberLiteralValue then
WriteLn(TGocciaNumberLiteralValue(ScriptResult.Result).Value); // 42.0
​
// Generic conversion methods (available on all TGocciaValue)
WriteLn(ScriptResult.Result.ToStringLiteral.Value); // "42"
WriteLn(ScriptResult.Result.ToNumberLiteral.Value); // 42.0
WriteLn(ScriptResult.Result.ToBooleanLiteral.Value); // True
WriteLn(ScriptResult.Result.TypeOf); // "number"
end;

Value Type Hierarchy

Pascal ClassJavaScript TypeKey Properties
TGocciaNumberLiteralValuenumber.Value: Double, .IsNaN, .IsInfinity
TGocciaStringLiteralValuestring.Value: string
TGocciaBooleanLiteralValueboolean.Value: Boolean
TGocciaNullLiteralValuenullSingleton via .NullValue
TGocciaUndefinedLiteralValueundefinedSingleton via .UndefinedValue
TGocciaObjectValueobject.GetProperty(Name), .AssignProperty(Name, Value)
TGocciaArrayValueobject (array).Elements: TGocciaValueList
TGocciaFunctionValuefunction.Call(Args, ThisValue)
TGocciaInstanceValueobject (class instance).ClassValue, .GetProperty(Name)

Error Handling

GocciaScript errors surface as Pascal exceptions. See Errors for the JavaScript-side error types and display format, and JSON Output for the JSON output envelope. On the Pascal side, wrap execution in try...except:

uses
SysUtils,
​
Goccia.Engine,
Goccia.Error;
​
try
TGocciaEngine.RunScript('undeclaredVariable;');
except
on E: TGocciaRuntimeError do
WriteLn('Runtime error: ', E.Message);
on E: TGocciaLexerError do
WriteLn('Lexer error: ', E.Message);
on E: TGocciaSyntaxError do
WriteLn('Syntax error: ', E.Message);
on E: Exception do
WriteLn('Unexpected error: ', E.Message);
end;

TGocciaLexerError inherits from TGocciaSyntaxError, so catching TGocciaSyntaxError alone covers both lexer and parser errors. Catch TGocciaLexerError first only when you need to distinguish them for diagnostics.

Exception ClassWhen
TGocciaSyntaxErrorAll early errors (base class for lexer and parser failures)
TGocciaLexerErrorInvalid tokens (unterminated strings, invalid characters) — subclass of TGocciaSyntaxError
TGocciaRuntimeErrorExecution errors (type errors, reference errors, throw statements)
TGocciaTypeErrorType-specific runtime error
TGocciaReferenceErrorUndefined variable access
TGocciaThrowValueJavaScript throw — wraps any thrown value including RangeError. Also raised from Execute, ExecuteProgram, RunModule, and RunModuleInScope with the reason of a promise the run left rejected with no handler (Errors). Engine.UnhandledRejections := urIgnore turns that off: the promises then stay on the microtask queue, where TGocciaMicrotaskQueue.Instance.TakeUnhandledRejection hands the oldest to a host and forgets the others. A host can ask while the run is in progress (from a native callback) or after an ExecuteProgram / RunModule that returned normally; Execute clears its queue before it returns, and freeing an engine while none is running clears the thread's, so nothing is left to take after either. To see every rejection, also of a run made with Execute, set Engine.OnUnhandledRejection: it is called for each such promise, oldest first, before the setting is applied (ADR 0125)
EObjectCheck, EAccessViolation, EInvalidPointer, EDivByZero, EPrivilege, EExternalExceptionEngine-integrity faults. The engine re-raises these past every guest catch, so they escape Engine.Execute even while guest code is running: they mean a pointer, a mapping, or the heap is no longer trustworthy. Report and exit — do not resume the process, and do not treat one as a script failure. EOutOfMemory is deliberately not one of them and stays catchable. See ADR 0109

Execution Limits

Two mechanisms prevent runaway scripts: wall-clock timeouts and instruction limits. Both use thread-local storage and are safe with parallel workers — each thread gets its own independent counter. Whether either is armed on a thread is mirrored in GThreadPolls (Goccia.ThreadPolls), which is all that a value allocation and a bytecode call read when neither is.

Timeout

StartExecutionTimeout arms a wall-clock deadline. ClearExecutionTimeout disarms it. The check is sampled (every 1024th call site) to minimize overhead.

uses
Goccia.Timeout;
​
StartExecutionTimeout(5000); // 5 000 ms
try
Engine.Execute;
finally
ClearExecutionTimeout;
end;

Raises TGocciaTimeoutError when the deadline is exceeded. A value of zero disables the timeout.

Instruction Limit

StartInstructionLimit caps the number of execution steps. In bytecode mode the counter increments on every dispatched instruction (exact). In interpreter mode it increments at function-call and loop-iteration checkpoints (approximate).

uses
Goccia.InstructionLimit;
​
StartInstructionLimit(1000000); // 1 000 000 steps
try
Engine.Execute;
finally
ClearInstructionLimit;
end;

Raises TGocciaInstructionLimitError when the limit is reached. A value of zero (the default) leaves the instruction budget inactive, so bytecode production dispatch omits the per-instruction poll. A positive limit selects the instrumented loop, which increments and checks the counter on every dispatched instruction.

Call Stack Depth Limit

SetMaxStackDepth caps the number of nested function calls. Exceeding the limit throws a JavaScript RangeError with the message "Maximum call stack size exceeded" (matching V8 convention). The TGocciaCLIApplication hosts set 2 200 frames (DEFAULT_MAX_STACK_DEPTH) and GocciaScriptLoaderBare sets 10 000, unless --max-stack overrides it; an embedder that never calls SetMaxStackDepth has no limit. A value of zero disables the limit entirely. In bytecode mode the memory ceiling then bounds the recursion instead: the VM's stacks are charged to TGarbageCollector.MaxBytes, and a recursion that would grow them past it throws the same catchable RangeError (ADR 0130). A module's top level is not a call. The entry's top level, and that of every module evaluated for a static import, an import() or a deferred namespace, allows the whole limit less the calls live when that module starts to evaluate, so a module imported from a top level allows all of it.

In bytecode mode the VM uses a trampoline: bytecode-to-bytecode calls are dispatched iteratively via an explicit frame stack, so the Pascal call stack stays flat regardless of JS call depth. The interpreter mode uses Pascal recursion and relies on the depth check to prevent overflow.

Every call of a function written in script counts as one nested call, however it is made: a plain or method call, new and super(), a getter or setter, a Proxy trap, a toString, valueOf or Symbol.toPrimitive method, an iterator's next, or a callback a built-in calls. A built-in function itself adds no frame, so [n].map(f) and the f it calls count as one. The interpreter does not count super() calls, accessors, Proxy traps, ToPrimitive methods or iterator methods. Most of those calls are native re-entries into the bytecode VM: the bytecode loop runs on a fresh Pascal stack frame instead of the trampoline, and each one costs a few kilobytes of native stack. How many fit depends on the thread's stack size and on the build. Once less than NATIVE_STACK_RESERVE (256 KiB, or a quarter of a stack smaller than 1 MiB) of the thread's stack is left, the VM throws the same RangeError instead of entering again, so no --max-stack value, including 0, lets these re-entries overflow the native stack. The check runs only where the bytecode loop is entered: a chain of native calls, such as Proxies forwarding to their targets or bound functions calling theirs, is not checked against the stack in between, and the delegation caps below bound only some of those chains. The bounds come from the operating system (/proc/self/maps and RLIMIT_STACK on Linux, pthread_get_stackaddr_np on Darwin, GetCurrentThreadStackLimits on Windows) and from the RTL's StackBottom where that is higher. On Linux the main thread's stack is taken to grow down to RLIMIT_STACK even where its mapping is not marked [stack], as under Valgrind. The first NATIVE_REENTRY_UNCHECKED_DEPTH (8) nested re-entries are not checked, so a program that does not nest them never looks the bounds up. Where they cannot be found, at most MAX_NATIVE_REENTRY_DEPTH (512) re-entries are live at once. A production x86-64 build on an 8 MiB stack, the Linux and macOS main-thread default and the test runner's worker size, fits 2 200 nested constructor, async-function, generator-next() or getter calls. A setter, a Proxy trap, a toString method or a callback from sort, JSON.stringify or Reflect.construct costs more, and recursion through one stops at between about 1 600 and 2 100 calls. Property lookups walk a chain of ordinary objects in a loop, with no length limit. A Proxy in the chain, a Proxy's target, a Proxy's trap and a handler that is itself a Proxy are entered by native calls, for every internal method a Proxy forwards ([[GetPrototypeOf]], [[SetPrototypeOf]], [[IsExtensible]], [[PreventExtensions]], [[GetOwnProperty]], [[DefineOwnProperty]], [[HasProperty]], [[Get]], [[Set]], [[Delete]], [[OwnPropertyKeys]], [[Call]] and [[Construct]]); at most MAX_PROPERTY_DELEGATION_DEPTH (2 000) of them are live at once, and past that the operation throws the same RangeError. A bound function between two Proxies is not counted, so a chain that alternates Proxies with bound functions, or a trap wrapped in several bound functions, can still exhaust the native stack. typeof and the callable check step through directly nested Proxies in a loop, since they cannot throw. Arrays, functions, classes and class instances in the chain are entered by native calls too, counted against MAX_OBJECT_DELEGATION_DEPTH (3 000). A prototype cycle closed through a Proxy, which ES2026 §10.1.2.1 OrdinarySetPrototypeOf accepts, ends this way. instanceof and Object.prototype.isPrototypeOf follow [[GetPrototypeOf]] in a loop instead and throw after MAX_PROXY_PROTOTYPE_STEPS (100 000) steps through a Proxy.

uses
Goccia.StackLimit;
​
SetMaxStackDepth(5000); // custom limit
// SetMaxStackDepth(0); // no limit
Engine.Execute;

The CLI tools expose all three limits as options; see Build System — Run Commands for usage.

FPU Exception Mask

FPU exceptions — divide-by-zero, overflow, underflow, invalid operation, denormalized operand, and precision loss — are hardware signals raised by the floating-point unit when an operation produces a special result. By default, FreePascal leaves some of these unmasked, which causes runtime exceptions on operations like 0.0 / 0.0 instead of returning NaN.

Both TGocciaEngine and TGocciaVM mask all FPU exceptions on creation (via SetExceptionMask) to enable IEEE 754 semantics (NaN, Infinity, -0). The previous mask is saved in the constructor and restored in the destructor, so the host application's FPU state is not permanently altered. This is transparent for one-shot execution (RunScript), but embedders creating long-lived engine instances should be aware that FPU exceptions are suppressed while the engine exists. If the host application depends on FPU exception handlers for its own error handling, those handlers will not fire while a TGocciaEngine or TGocciaVM instance is alive.

Microtask Queue (Promises)

The engine initializes a singleton microtask queue (TGocciaMicrotaskQueue) alongside the GC. Promise .then() callbacks are enqueued as microtasks and drained automatically after each Execute or ExecuteProgram call. Fetch completions are also pumped before these calls return, then their Promise reactions drain through the same microtask queue. Embedders do not need to drain either queue manually.

This means:

  • All synchronous code in the script runs to completion first.
  • All pending .then() callbacks fire after the script finishes.
  • Chained .then() handlers are processed in the same drain cycle.
  • On successful execution, the engine's pending fetch() requests complete before Execute returns; if execution throws, they are detached and their late completions are discarded. The engine neither waits for nor discards requests another engine on the same thread started, although its drain may settle those that have already completed (their reactions still run in the engine that owns them). The microtask queue is still only used for Promise reactions, not for network I/O.

The execution ordering follows ECMAScript specification semantics — the script is one macrotask, and microtasks drain after it completes. Thenable adoption is deferred via a microtask per the spec's PromiseResolveThenableJob.

// Promises work automatically — no manual queue management needed
Source.Text := 'Promise.resolve(42).then((v) => { globalThis.answer = v; });';
Engine.Execute; // microtasks drain before Execute returns

For long-lived engines (REPL-style), each Execute call drains its own microtasks. Promise callbacks from one execution will not leak into the next — even if the script throws an exception, the engine clears any pending microtasks in a finally block. Engines can also nest on a thread: an Execute that starts while a different engine's run is in progress (a sandbox runScript child, or another engine's Execute called from a native callback) drains and discards only its own jobs and leaves the enclosing engine's jobs queued, while a nested ExecuteProgram or RunModule drains the queue it was called in. Interpreter — Synchronous Microtask Queue describes the scopes behind this.

Garbage Collector

The engine initializes a mark-and-sweep garbage collector (TGarbageCollector) automatically. In most embedding scenarios, no manual GC interaction is needed. The GC collects unreachable values during execution.

For long-running engines (REPL-style), the GC runs automatically. If you need to trigger collection manually between script executions:

uses
Goccia.GarbageCollector;
​
TGarbageCollector.Instance.Collect;

Memory ceiling: The GC auto-detects physical memory and defaults to half of RAM, capped at 8 GB on 64-bit or 700 MB on 32-bit (512 MB fallback when detection fails). Override with MaxBytes to impose a custom limit. Allocations exceeding it raise a JavaScript RangeError:

TGarbageCollector.Instance.MaxBytes := 10 * 1024 * 1024; // 10 MB limit

Temporary roots: If your Pascal code holds references to TGocciaValue objects outside of any GocciaScript scope (e.g., in a Pascal list while the engine runs), protect them from collection:

TGarbageCollector.Instance.AddTempRoot(MyValue);
try
Engine.Execute; // MyValue won't be collected during execution
finally
TGarbageCollector.Instance.RemoveTempRoot(MyValue);
end;

Existing Embeddings

The repository includes these embedding examples:

ProgramFileDescription
GocciaRunnersource/app/GocciaRunner.dprExecutes source files (.js, .jsx, .ts, .tsx, .mjs, .mts) from disk or stdin, with optional JSON output, injected globals, and execution timeouts for one-shot automation
GocciaScriptLoaderBaresource/app/GocciaScriptLoaderBare.dprCore engine with a CLI-local print; no runtime profile
GocciaREPLsource/app/GocciaREPL.dprInteractive read-eval-print loop (long-lived engine)
GocciaTestRunnersource/app/GocciaTestRunner.dprRuns test suites with the test-runner runtime profile
GocciaBenchmarkRunnersource/app/GocciaBenchmarkRunner.dprRuns benchmarks with the benchmark-runner runtime profile from files or stdin
GocciaBundlersource/app/GocciaBundler.dprBundler CLI host — compiles source files to .gbc without execution

These serve as reference implementations for the patterns described above.

Application Base Class

TGocciaApplication (Goccia.Application.pas) provides the standard lifecycle for any GocciaScript host application: unified error handling and exit codes, with no CLI dependency. It does not manage the GC; the first TGocciaEngine constructor on a thread initializes it.

type
TMyApp = class(TGocciaApplication)
protected
procedure Execute; override;
end;
​
procedure TMyApp.Execute;
begin
// Your application logic here; creating an engine initializes the GC
// Errors are caught by HandleError
end;
​
begin
ExitCode := TGocciaApplication.RunApplication(TMyApp, 'MyApp');
end.

What the base class handles:

  • Exception dispatch via virtual HandleError (supports TGocciaError, TGocciaThrowValue, EGocciaBytecodeThrow with full source context and colored output)
  • Exit code management (0 = success, 1 = error, 2 = TCLIUsageError)

Overridable hooks:

  • Execute (abstract) — your application logic
  • HandleError(AException) — customize error display (e.g., JSON output)

For CLI tools, use TGocciaCLIApplication instead, which adds argument parsing, help generation, singleton lifecycle management, and a ConfigureCreatedEngine hook where tools attach TGocciaRuntimeCore and apply their chosen runtime profile or runtime extensions.

Minimal Embedding Checklist

  1. Add source/units/ and source/shared/ to your FreePascal unit search path (or use config.cfg)
  2. uses Goccia.Runtime, Goccia.RuntimeProfiles.Loader, Goccia.Values.Primitives;
  3. Create TGocciaRuntime.Create(...) for the runtime layer and file loading
  4. Use Runtime.Engine for engine-level options such as ASI, source type, parser diagnostic policy, and compatibility flags
  5. Choose your runtime surface, runtime globals, and goccia: modules via runtime profiles or runtime extensions
  6. Inject custom globals via Runtime.Engine.Interpreter.GlobalScope.DefineLexicalBinding(...)
  7. Handle exceptions from Goccia.Error
  8. Free the runtime when done; it owns and frees the engine only when it created the engine itself, or when you used an ownership-transfer overload such as TGocciaRuntime.Create(Engine, True). TGocciaRuntime.Create(Engine) is non-owning by default, so embedders wrapping an existing engine must also free that engine.

Prefer virtual modules; global injection remains supported without warnings.