A drop of JavaScript — sandboxed by default
GocciaScript is a JavaScript engine: a sandbox-first ECMAScript runtime and toolchain for AI agents. Hosts define the available capabilities, runtime surface, and execution limits. It uses modern recommended defaults while tracking ECMAScript compatibility through generated test262 reports.
GocciaScript is implemented in FreePascal, supports Delphi, and can also be embedded in native applications. Native embedding is an important secondary goal; the primary product goal is AI-agent execution under an explicit host-defined capability model. It is not trying to become Node.js or a browser host.
GocciaRunner's sandbox mode copies explicit host paths into an in-memory
virtual filesystem, runs an entry script with host-owned limits, and reports
sandbox changes as a diff. Copies are snapshots, not live mounts, and scripts
receive no ambient host filesystem access.
// agent-workspace/main.js
import fs from "fs";
const input = fs.readFileSync("/task.txt", "utf8");
fs.mkdirSync("/out", { recursive: true });
fs.writeFileSync("/out/result.txt", input.toUpperCase());./build.pas runner
./build/GocciaRunner agent-workspace/main.js \
--copy agent-workspace/task.txt \
--timeout=5000 \
--diffThe entry file lands at /main.js and the copied input at /task.txt.
Nothing the script writes reaches the host unless the host asks for it: an
input copied with --copy-rw instead of --copy has the files a successful
run changed inside it written back to their host paths, and everything else in
the virtual filesystem is discarded. A program that only reports and a program
that fixes are therefore the same program, and the difference is a word on the
host's command line. See
Permissions — Sandbox mode.
The host can also define globals, virtual modules, network grants
(--allow-net), instruction and memory limits, deterministic time/randomness, and
application-specific APIs. See Build System — GocciaRunner sandbox mode
and Built-ins — Sandbox Modules.
GocciaScript implements core ECMAScript and runs the official test262 corpus on every PR and main commit. The exact current result is rendered from published main-branch data on the live ECMAScript compatibility dashboard; canonical prose does not freeze a historical percentage.
The recommended language profile is product policy, not the implementation ceiling. Standard core forms disabled by default all have explicit compatibility paths:
| Form | Default | Enablement / exposure |
|---|---|---|
var |
Disabled | --compat-var |
traditional function syntax |
Disabled | --compat-function |
== / != |
Disabled | --compat-loose-equality |
| ASI | Disabled | --compat-asi |
| labels | Disabled | --compat-label |
traditional for(;;) |
Disabled | --compat-traditional-for-loop |
for...in |
Disabled | --compat-for-in-loop |
while / do...while |
Disabled | --compat-while-loops |
arguments |
Disabled | --compat-arguments-object |
non-strict Script semantics and with |
Strict / disabled | --compat-non-strict-mode |
eval |
Not installed by normal hosts | private GocciaTest262Runner conformance host |
Function() |
Disabled | --unsafe-function-constructor |
ShadowRealm |
Not installed | --unsafe-shadowrealm |
Annex B's browser-only legacy surface is not a general pre-1.0 target; see ADR 0085. See Language and Language Tables for the detailed semantics and feature matrix.
GocciaScript implements the official
TC39 Type Annotations proposal and
its types-as-comments runtime model. Supported annotations have no runtime
effect by default. GocciaScript additionally provides the optional
--strict-types extension, which enforces supported annotations and relevant
inferred primitive contracts at runtime in interpreter and bytecode modes.
--strict-types is a runtime contract extension, not a replacement for a static
structural type checker such as tsc. See
Type Annotations for the supported syntax and its
parsing rules.
GocciaScript is not a complete Node.js host: it does not provide CommonJS,
process, Buffer, or the general node: module set. Bare-specifier lookup
through node_modules (ESM only, a subset of Node's resolver) is opt-in with
--allow-import=node_modules; see Module Resolution.
GocciaRunner's sandbox mode does provide a Node-compatible fs API over its
virtual filesystem:
- synchronous forms such as
readFileSync,writeFileSync,mkdirSync,readdirSync,statSync,rmSync,renameSync, andcopyFileSync; - Node-shaped callback forms for the same operation families;
fs.promisesforms,Statsobjects, and Node-shaped filesystem errors.
The documented method set never reaches the ambient host filesystem. See Sandbox Modules for supported options, return types, error shapes, and method-level deviations.
The default profile includes modern ECMAScript forms such as let/const,
arrow functions, classes with private fields, for...of, async/await, ES
modules, decorators, and proposal-compatible type syntax.
Core built-ins include Math, JSON, Object, Function, Array, Boolean,
Number, BigInt, String, RegExp, Symbol, Set, Map, WeakSet,
WeakMap, WeakRef, FinalizationRegistry, Promise, Temporal, Intl,
Iterator, DisposableStack, AsyncDisposableStack, Proxy, Reflect,
ArrayBuffer, SharedArrayBuffer, DataView, Atomics, and TypedArrays
(Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
Int32Array, Uint32Array, Float16Array, Float32Array, Float64Array,
BigInt64Array, BigUint64Array), alongside the global functions
queueMicrotask, structuredClone, atob, and btoa. The loader runtime
profile (applied by GocciaRunner, GocciaREPL, GocciaTestRunner, and
GocciaBenchmarkRunner) adds
console, performance, fetch, Headers, Response (WHATWG Fetch — GET/HEAD only),
AbortController, AbortSignal, EventTarget, Event, URL,
URLSearchParams, TextEncoder, and TextDecoder. Error constructors include Error, EvalError, TypeError,
ReferenceError, RangeError, SyntaxError, URIError, AggregateError,
SuppressedError, and DOMException.
Non-standard data-format APIs and SemVer are import-only Goccia runtime modules, not auto-installed globals: goccia:csv, goccia:json5, goccia:jsonl, goccia:toml, goccia:tsv, goccia:yaml, and goccia:semver. They expose named exports only; use import * as CSV from "goccia:csv" when you want the namespace-object shape. There is no default export.
goccia:ast is an experimental runtime module behind --experimental-ast. It exposes one parse function that returns a source file's statement structure — kinds, offsets, line/column, nesting — plus its comments, so a lint rule or a codemod can be a GocciaScript program. Every offset and position is into the text that was passed, including for a .tsx file the engine rewrote before parsing. See Built-ins, ADR 0117, and ADR 0118.
node:async_hooks is an import-only module too, at Node's own address. It exports AsyncLocalStorage and AsyncResource, named and on the default export; the async_hooks observer API (createHook, executionAsyncId, and the rest) is out of scope. The engine propagates the async context, so a store bound with run survives await and every promise-reaction continuation. See the Async Context reference and ADR 0112.
Native FFI needs an explicit ffi grant (--allow-ffi[=<library>,...] or "allow-ffi" in a config file's permissions block). It provides native-layout structures, unions, fixed-length arrays, callbacks, and guarded library lifetimes through GocciaScript's custom bidirectional ABI machinery. See the FFI reference and ADR 0095.
See Built-in Objects for the complete API reference.
class CoffeeShop {
#name = "Goccia Coffee";
#beans = ["Arabica", "Robusta", "Ethiopian"];
#prices = { espresso: 2.5, latte: 4.0, cappuccino: 3.75 };
getMenu() {
return this.#beans.map((bean) => `${bean} blend`);
}
calculateTotal(order) {
return order.reduce((total, item) => total + (this.#prices[item] ?? 0), 0);
}
get name() {
return this.#name;
}
}
const shop = new CoffeeShop();
const order = ["espresso", "latte"];
const total = shop.calculateTotal(order);
console.log(`Welcome to ${shop.name}!`);
console.log(`Your order total: $${total.toFixed(2)}`);- FreePascal compiler (
fpc)- macOS:
brew install fpc - Ubuntu/Debian:
sudo apt-get install fpc - Windows:
choco install freepascal
- macOS:
# Dev build of everything
./build.pas
# Production build
./build.pas --prod
# Build the runner only
./build.pas runnerSee Build System for build modes, targets, clean builds, and troubleshooting.
./build.pas runner && ./build/GocciaRunner example.js
printf "const x = 2 + 2; x;" | ./build/GocciaRunner --printBy default, the runner is silent about the script's last evaluated value.
Pass --print to emit it; use --output=json for programmatic consumers. See
Build System for runner options,
bytecode mode, JSON output, sandbox execution, import maps, config files, and
resource limits.
GocciaScript includes bytecode execution, and GocciaBundler compiles source to
the public .gbc artifact.
./build/GocciaRunner example.js --mode=bytecode
./build/GocciaBundler example.js
./build/GocciaRunner example.gbcSee Bytecode VM for the current bytecode executor architecture.
Use one fixed JavaScript-visible clock, UTC time zone, and portable random stream in either execution mode:
./build/GocciaRunner example.js --deterministicTimeouts and profiling still use the real monotonic clock. The equivalent config key is "deterministic": true; embedders can inject their own clock and RNG providers through the engine host environment.
For custom providers, pass a JavaScript module to --host-environment or implement the Pascal host interfaces. See Host Environment for both examples and the provider contract.
./build.pas repl && ./build/GocciaREPLGocciaScript has 12,000+ JavaScript end-to-end tests across 1,500+ test files, covering language features, built-in objects, and edge cases.
Some test directories request permissions in their configs, so accept them for
the run with -P (or trust the suite once per checkout with
./build/GocciaTestRunner --trust tests/ and drop -P):
./build.pas testrunner
./build/GocciaTestRunner -P tests
./build/GocciaTestRunner -P tests --mode=bytecodeThe test runner supports Vitest-compatible external and inline snapshots,
property shapes, asymmetric matchers, custom serializers, and -u updates.
Importing vi from "vitest" resolves to a bundled compatibility shim, so
suites written against vi.fn, vi.spyOn, and factory-form vi.mock (a
synchronous arrow factory returning an object literal — no automock, no
spread-based partial mock) run unmodified; see
Test Framework API for the full factory constraints and
the members that are not implemented.
See Testing for test organization and Build System for runner options.
./build.pas benchmarkrunner && ./build/GocciaBenchmarkRunner benchmarks
./build/GocciaBenchmarkRunner benchmarks/fibonacci.jsThe benchmark runner auto-calibrates iterations per benchmark, reports ops/sec with variance (CV%) and engine-level timing breakdown (lex/parse/execute). Output formats: console (default), text, csv, json, compact-json (the same envelope as json without build, memory, stdout, or stderr). Calibration and measurement parameters are configurable via environment variables. Retained AWFY and JetStream reference measurements are published through the Performance Barometer.
The recommended profile favors modern, explicit forms. The corresponding core ECMAScript forms remain implemented through the compatibility paths listed above:
- Arrow functions and methods by default —
const greet = (name) => `Hello, ${name}!`;; traditionalfunctionsyntax uses--compat-function. - Iterator-oriented loops by default — use array methods, iterators, or
for...of; traditionalfor(;;),for...in,while, anddo...whileeach have targeted compatibility flags. - Classes with private fields —
class Account { #balance = 0; ... } - ES modules — default, named, and namespace imports/exports are supported; project code prefers named exports for clarity.
- Strict equality by default —
===and!==(==/!=require--compat-loose-equality)
Beyond the static imports of its own project a script reaches nothing by default: other host reads, computed dynamic imports, fetch, FFI, and node_modules all throw PermissionDenied until the host grants them with --allow-read, --allow-net, --allow-ffi, or --allow-import (or a config file's permissions block). A deny always wins. See Permissions.
A config file's permissions block is only a request: its grants take effect once the user trusts the config (--trust <path>) or accepts it for one run (-P). A block that only denies needs no trust. See Permissions — Config trust.
The CLI tools share WHATWG-style import map support with --import-map=<file.json>, --alias key=value, and automatic goccia.json discovery for project-level module aliases. Host-supplied dependencies should normally be configured as virtual ES modules with --module, --modules, or a config modules object; they participate in the same import pipeline as filesystem modules. Global injection remains supported for compatibility.
An import-map entry can also name a github: provider package. GocciaRunner --add and --install pin it in goccia.lock.json, and a run loads it once the import capability grants it (--allow-import=github). See Provider Imports.
Structured data files and text assets can also be imported directly:
import { name, version } from "./package.json";
import { name as packageName } from "./config.toml";
import { name as appName } from "./config.yaml";
import { content, metadata } from "./README.md";Runtime parsers are available through named Goccia modules for JSON5, TOML, YAML, JSONL, CSV, and TSV. See Built-in Objects and Language for the full data format reference.
import * as TOML from "goccia:toml";
import * as YAML from "goccia:yaml";
TOML.parse(sourceText); // TOML 1.1.0 configuration data
YAML.parse(sourceText); // block scalars, anchors/aliases, merge keys, YAML 1.2 tagsSee Language and Architecture Decision Records for the full conformance details.
JSONL parsing is also available from goccia:jsonl via parse(text) and parseChunk(text), and .jsonl files can still be imported as structured-data modules.
Async/await with full Promise support, including top-level await:
const fetchData = async () => {
const result = await Promise.resolve({ status: "ok" });
return result;
};
// Top-level await (ES2022+)
const data = await fetchData();Strict equality by default — === and !==; == and != are available only with --compat-loose-equality.
For a full guided walkthrough, see the Tutorial. For the complete implementation, default-profile, and compatibility-path detail, see Language.
FreePascal is the cross-platform toolchain used for normal builds, releases, and
the documented embedding API. The repository also includes
GocciaScript.Delphi.groupproj and
Delphi projects for the REPL, the Runner, the Bare Script Loader, Test Runner,
Benchmark Runner, and Bundler on Win32 and Win64.
The Delphi support contract requires the complete application matrix and all applicable Pascal and JavaScript tests to pass with the same runtime semantics as the FreePascal build. Delphi 12 Community Edition contributors validate that contract through the IDE; see Delphi Validation and Build System.
Native hosts can embed GocciaScript to run portable JavaScript with application-specific globals, modules, capabilities, and limits. See Embedding the Engine.
GocciaScript supports two execution modes that share the same source pipeline (preprocessors, lexer, parser, AST):
flowchart LR
Source["Source Code"] --> Preprocessors["Preprocessors"] --> Lexer --> Parser --> AST
AST --> Interpreter["Tree-Walk Interpreter"] --> Result1["Result"]
AST --> Compiler["Bytecode Compiler"] --> VM["Goccia VM"] --> Result2["Result"]
Both execution modes share the same value types, built-ins, scope chain, and mark-and-sweep GC. The bytecode executor uses a Goccia-owned VM with tagged TGocciaRegister values (unboxed scalars) that fall back to TGocciaValue for heap objects, not a generic VM layer.
See Architecture for pipelines and layers, Interpreter for tree-walk execution, Bytecode VM for bytecode execution, Core patterns for implementation patterns, and GocciaScript Context for canonical terminology.
- Explicitness: Modules, classes, methods, and properties use explicit, descriptive names even at the cost of verbosity. Shortcuts are avoided.
- OOP over everything: Rely on type safety of specialized classes rather than generic data structures.
- Define vs Assign:
Definecreates a new variable binding;Assignchanges an existing one. These are distinct operations throughout the codebase (see Core patterns). - Pure evaluation: The evaluator is composed of pure functions with no side effects.
- No global mutable state: All runtime state flows through explicit parameters — the evaluation context, the scope chain, and value objects.
- Virtual dispatch: Property access (
GetProperty/SetProperty), type discrimination (IsPrimitive/IsCallable), and scope chain resolution (GetThisValue/GetOwningClass/GetSuperClass) all use virtual methods, replacing type checks with single VMT calls.
See Core patterns and Interpreter for the design rationale.
| Document | Description |
|---|---|
| Vision | Why GocciaScript exists: sandboxed AI agent runtime and embeddable desktop platform |
| Tutorial | Your first GocciaScript program — a guided walkthrough for newcomers |
| Language | ECMAScript support, recommended defaults, compatibility flags, and rationale |
| Language Tables | Quick-reference: ECMAScript feature matrix and TC39 proposal status |
| Type Annotations | TypeScript-compatible type syntax, --strict-types, and the < disambiguation rules |
| Built-in Objects | Available built-ins and API reference |
| FFI Built-ins | Native libraries, aggregate types, callbacks, lifetimes, and safety limits |
| Temporal Built-ins | Temporal API: dates, times, durations, time zones |
| Binary Data Built-ins | ArrayBuffer, SharedArrayBuffer, TypedArray API |
| Async Context | node:async_hooks: AsyncLocalStorage, AsyncResource, and what propagates |
| Errors | Error types, parser/runtime display, JSON output, Error.cause, try/catch/finally |
| Architecture | Pipelines, main layers, design direction, duplication boundaries |
| Interpreter · Bytecode VM | Tree-walk and bytecode execution modes |
| Core patterns | Recurring implementation patterns |
| GocciaScript Context | Canonical project terminology and glossary |
| Value System | Type hierarchy, virtual property access, primitives, objects |
| Garbage Collector | Mark-and-sweep GC: architecture, contributor rules, design rationale |
| Adding Built-in Types | Step-by-step guide for adding new built-in types |
| Embedding the Engine | Embedding GocciaScript in FreePascal applications |
| Module Resolution | Resolution order, opt-in node_modules lookup, and the deviations from Node |
| Provider Imports | github: import-map entries, goccia.lock.json, the .goccia cache, and verify on load |
| Virtual Module Configuration | CLI, config-file, and embedding reference for host-supplied modules |
| Host Environment | Injecting JavaScript-visible clock, time-zone, and random providers |
| Permissions | The capability model: read, net, ffi, import, deny-wins, the module-graph exemption, PermissionDenied |
| Capability Audit Events | Structured host capability decisions, embedding sink, and CLI JSONL output |
| Testing | Test organization, running tests, coverage, CI |
| Test Framework API | Assertions, mocks, lifecycle hooks, async patterns |
| Benchmarks | Benchmark runner, output formats, writing benchmarks |
| Build System | Build commands, compiler configuration, CI/CD |
| Profiling | Bytecode VM profiling: opcodes, functions, output formats |
| Architecture Decision Records | Durable architectural decisions and trade-offs |
| Contributing | Single contribution standard: workflow, mandatory rules, testing, FreePascal style |
| AGENTS.md | Agent operating manual for coding assistants; CONTRIBUTING.md is the contributing guide for everyone |
CONTRIBUTING.md is the contributing guide for all contributors (humans and AI): workflow, mandatory rules, testing, FreePascal code style, ./format.pas, editor setup, build/run quick reference, and the documentation index.
AGENTS.md (and CLAUDE.md, which points to it) is only for AI assistants—how to use the repo and defer to CONTRIBUTING. It is not a second contributing guide and should stay short.
See LICENSE for details.
