Build System
For contributors setting up their development environment or troubleshooting builds.
Executive Summary
- Self-hosted build —
./build.pasis a FreePascal script viainstantfpc; no Make/CMake/npm required - Two modes —
--dev(default: debug info, checks) and--prod(O4, stripped, smart-linked) - CI/CD —
ci.ymlfor main/tags (full platform matrix),pr.ymlfor PRs (ubuntu-latest x64 only, with benchmark comparison) - Auto-formatter —
./format.paswith Lefthook pre-commit hook enforces code style automatically - Explicit clean recovery —
./build.pas --clean <target>clears stale FPC artifacts before diagnosing misleading compiler/resource failures
GocciaScript uses a self-hosted build script written in FreePascal, executed via instantfpc. No external build tools (Make, CMake, npm) are required beyond the FreePascal compiler itself.
This file is the authoritative command reference for local builds and CLI usage. Root-level files such as README.md, CONTRIBUTING.md, and
AGENTS.md link here instead of duplicating the complete command list.
Prerequisites
See Prerequisites in the README for FPC installation instructions.
- instantfpc — Comes bundled with FreePascal. Used to run
build.pasas a script.
Delphi 12 contributors build the Win32 and Win64 native application matrix from source/app/GocciaScript.Delphi.groupproj; see Delphi Validation for the complete IDE workflow and validation contract.
Build Commands
Build Modes
The build script supports two modes via --dev (default) and --prod flags:
./build.pas runner # Dev build (default)./build.pas loaderbare # Dev build of Bare Script Loader./build.pas test262runner # Dev build of native Test262 runner./build.pas --dev runner # Explicit dev build./build.pas --prod runner # Production build./build.pas --prod # Production build of all components./build.pas --clean --prod # Clean, then production build of all components./build.pas --prod runner repl # Production build of specific components
Build Everything
./build.pas # Dev build of all components./build.pas --prod # Production build of all components
Builds all components in order: tests, runner, loaderbare, fuzzharness,
testrunner, test262runner, tomlcompliancerunner, wasmtestrunner,
benchmarkrunner, bundler, and repl. The default full build does not clean first; pass --clean explicitly
when you need to remove stale build artifacts.
Build Specific Components
./build.pas repl # Interactive REPL./build.pas runner # Runner (host mode and sandbox mode)./build.pas loaderbare # Bare Script Loader (core engine only)./build.pas fuzzharness # Fuzz harness (both executors)./build.pas testrunner # JavaScript test runner + native FFI fixture./build.pas test262runner # Native Test262 conformance runner./build.pas tomlcompliancerunner # Native TOML compliance runner./build.pas wasmtestrunner # Manifest-driven test runner for the Wasm/LAKON host./build.pas benchmarkrunner # Performance benchmark runner./build.pas bundler # Bundler (compile to .gbc)./build.pas tests # Pascal unit tests
Multiple components can be specified:
./build.pas runner repl
Clean Builds
./build.pas --clean # Clean, then build all components./build.pas --clean runner # Clean then build the runner
./build.pas loader and ./build.pas sandboxrunner fail with a message naming
the replacement: 0.14 renamed GocciaScriptLoader to GocciaRunner and merged
GocciaSandboxRunner into its sandbox mode.
Cleaning is explicit. ./build.pas and ./build.pas --prod build all components without cleaning; use ./build.pas --clean for a clean full build or ./build.pas --clean <target...> to clean before selected targets.
Use the explicit clean form after a branch switch, merge, PR sync, generated resource
update, or unexplained FPC/resource compiler error. FPC 3.2.2 can report stale
compiled state as misleading messages such as Compilation raised exception internally or Error while compiling resources; retry with --clean first,
then diagnose the reported source line only if the same target still fails.
Compile and Run
./build.pas runner && ./build/GocciaRunner ./example.jsprintf "const x = 2 + 2; x;" | ./build/GocciaRunnerprintf 'import { bench, group } from "goccia:microbench"; group("stdin", () => { bench("sum", () => 1 + 1); });\n' | ./build/GocciaBenchmarkRunner --source-type=module
GocciaRunner and GocciaScriptLoaderBare are silent about the script's last evaluated value unless you pass --print. For local dev that's fine; for CI scripts and shell pipelines that previously parsed Result: <value> from the runner's stdout, pass --print and parse the bare value on the line after the timing banner — or switch to --output=json and read the result field, which is always populated regardless of --print.
Leading Unix shebang lines such as #!/usr/bin/env goccia are treated as comments by the lexer, so executable scripts can be run directly without preprocessing.
For a smaller runtime surface without the loader runtime profile, build and run the Bare Script Loader:
./build.pas loaderbare && ./build/GocciaScriptLoaderBare example.jsprintf "Goccia.version;" | ./build/GocciaScriptLoaderBare --print
Run a pinned Test262 checkout with the dedicated native runner:
./build.pas test262runner./build/GocciaTest262Runner \--suite-dir=/path/to/test262 \--mode=bytecode \--jobs=4 \--output=test262-results.json
See Test262 Harness Contract for shard, profile, host, and report semantics.
Coverage and profiling output are available from the runner:
./build/GocciaRunner example.js --coverage --coverage-format=lcov --coverage-output=coverage.lcov./build/GocciaRunner example.js --profile=all --profile-output=profile.json
Compile and Test
./build.pas testrunner./build/GocciaTestRunner -P tests./build/GocciaTestRunner -P tests --mode=bytecode
./build.pas testrunner also builds fixtures/ffi/libfixture.* for the
current platform, which the folder-configured FFI JavaScript tests need.
Useful test-runner forms:
./build/GocciaTestRunner -P tests --jobs=4./build/GocciaTestRunner -P tests --no-progress --exit-on-first-failure --silent./build/GocciaTestRunner -P tests --output=results.json --log=test-console.log./build/GocciaTestRunner -P tests --coverage --coverage-format=lcov --coverage-output=coverage.lcov
Bytecode Mode
All execution tools support --mode=bytecode to compile and run via the Goccia bytecode VM instead of the tree-walk interpreter. --profile (see Profiling) and --coverage (see Testing — Coverage) select bytecode on their own and ignore --mode.
# Execute via bytecode VM./build/GocciaRunner example.js --mode=bytecodeprintf "const x = 2 + 2; x;" | ./build/GocciaRunner --mode=bytecode --print# Load and execute a pre-compiled .gbc file./build/GocciaRunner output.gbc# Emit structured JSON for programmatic consumersprintf "console.log('hi'); 2 + 2;" | ./build/GocciaRunner --output=json# Emit a smaller JSON envelope without build, memory, stdout, or stderr.# Console output remains in the normalized `output` array; errors stay in `error`.printf "console.log('hi'); 2 + 2;" | ./build/GocciaRunner --output=compact-json# `compact-json` is recognised by every runner that emits JSON.# - GocciaTestRunner: pass it as the value of --output. `--output=json` and# `--output=compact-json` emit JSON to stdout (suppressing the human-readable# summary); any other --output value is treated as a file path that receives# the full JSON envelope../build/GocciaTestRunner -P tests --output=compact-json# - GocciaBenchmarkRunner: pass it as the value of --format alongside the# existing console/text/csv/json options. `--output=<path>` still selects the# destination file when provided../build/GocciaBenchmarkRunner benchmarks --format=compact-json --output=out.json# Inject globals from the CLIprintf "x + y;" | ./build/GocciaRunner --global x=10 --global y=20 --printprintf "name;" | ./build/GocciaRunner --globals=context.json --output=jsonprintf "name;" | ./build/GocciaRunner --globals=context.json5 --output=jsonprintf "name;" | ./build/GocciaRunner --globals=context.toml --output=json# `--global name=value` parses the value as JSON and keeps it as a string when that fails; `--globals=file` accepts JSON, JSON5, TOML, or YAML by file extension. GocciaTestRunner accepts both — that is how a suite gets `process` (docs/testing-api.md).# Injected globals can override earlier injected values, but not built-in globals like console# Load an explicit import map./build/GocciaRunner app.js --import-map=imports.json# Add one-off import-map-style aliases from the CLI (relative targets use the invocation directory)./build/GocciaRunner app.js --alias @/=./src/ --alias config=./config/default.js# Grant capabilities beyond the project's own imports (all denied by default;# see docs/permissions.md for scopes, config blocks, and what each binary honors)./build/GocciaRunner app.js --allow-read=../shared --allow-net=api.example.com./build/GocciaRunner app.js --allow-ffi=./libs/libcalc.so# Import lockfile-pinned github: packages named in the import map, and refuse# the network when one is not cached (see docs/provider-imports.md)./build/GocciaRunner app.js --allow-import=github:acme --cached-only# Pin a provider package, or re-check every pin in CI (install mode)./build/GocciaRunner --add lib=github:acme/lib@v1.0.0/index.js./build/GocciaRunner --install --frozen --check-refs --allow-import=github:acme# Resolve bare specifiers against node_modules (see docs/module-resolution.md).# The plain scope walks up from each importing file; =<dir> caps the walk at that directory../build/GocciaRunner app.js --allow-import=node_modules./build/GocciaRunner app.js --allow-import=node_modules=./project# The same module-resolution and virtual-module flags are available on the shared CLI hosts../build/GocciaTestRunner -P tests --import-map=imports.json --alias @/=./tests/helpers/./build/GocciaBenchmarkRunner benchmarks --import-map=imports.json./build/GocciaREPL --import-map=imports.json# REPL supports the same engine options as the runner:./build/GocciaREPL --log=repl.log # Console log capture./build/GocciaREPL --max-stack=5000 # Custom call stack depth limit./build/GocciaREPL --max-memory=10MiB # GC heap limit./build/GocciaREPL --timeout=5s # Applied to each evaluated input# When --import-map is omitted, the CLI walks up from the entry file's directory# and uses the first goccia.json (or .json5 / .toml) it finds.printf 'import { add } from "@/math"; add(1, 2);' | ./build/GocciaRunner# Abort long-running scripts (500ms, 5s, 2m, or plain milliseconds): "Fatal error: file timed out after 100ms"printf "const f = () => Promise.resolve().then(f); f();" | ./build/GocciaRunner --timeout=100ms# Abort after a fixed number of bytecode instructionsprintf "const f = () => Promise.resolve().then(f); f();" | ./build/GocciaRunner --max-instructions=1000000 --mode=bytecode# Set call stack depth limit (default 2200; 0 = no depth limit, bytecode recursion then stops at --max-memory, ADR 0130)./build/GocciaRunner example.js --max-stack=5000./build/GocciaRunner example.js --max-stack=0# Caveat (#1274): with 0, deep interpreted recursion exits 1 with "Fatal error: Stack overflow"# Write .map source map alongside execution./build/GocciaRunner example.jsx --source-map --mode=bytecode# Run tests via bytecode VM./build/GocciaTestRunner -P tests --mode=bytecode# Control parallel worker threads (default: CPU count; --jobs=1 forces sequential)./build/GocciaRunner example.js --jobs=4./build/GocciaTestRunner -P tests --jobs=4./build/GocciaBenchmarkRunner benchmarks --jobs=1# Split a single input file (or stdin) on `---` separator lines and dispatch# each section as an independent file across the worker pool./build/GocciaRunner scenarios.js --multifile./build/GocciaTestRunner suites.js --multifile --jobs=4mkdir -p dist # the bundler does not create --output directories./build/GocciaBundler scenarios.js --multifile --output=dist/printf '1;\n---\n2;\n---\n3;\n' | ./build/GocciaRunner --multifile# Run benchmarks via bytecode VM./build/GocciaBenchmarkRunner benchmarks --mode=bytecodeprintf 'import { bench, group } from "goccia:microbench"; group("stdin", () => { bench("sum", () => 1 + 1); });\n' | ./build/GocciaBenchmarkRunner - --source-type=module --mode=bytecode
--multifile (split a single input on --- separators)
When --multifile is set, each input — whether a file or stdin — is scanned for lines whose trimmed content equals exactly ---. If any such separator is present, the input is split at those lines and each section is dispatched through the regular worker pool as if it were its own input file. Sections execute in fully isolated TGocciaEngine instances, so per-section state (intrinsic prototype mutations, top-level bindings) cannot leak across sections.
Section naming. The [partN] suffix (1-indexed) is appended to the input name:
- File
foo.jswith three sections producesfoo[part1].js,foo[part2].js,foo[part3].js. The suffix is inserted before the extension soExtractFileExt,ChangeFileExt, JSX/.tsxdispatch, and the bundler's.gbcoutput naming continue to work without any per-site changes. - Stdin with three sections produces
<stdin>[part1],<stdin>[part2],<stdin>[part3]. The suffix is appended (no extension to insert before). - Section names appear as the
fileNamefield in JSON output, in error messages, in coverage reports, and in source maps.
Separator semantics. A line is a separator iff its trimmed content equals --- exactly. ----, --- comment, and ---abc are not separators. Empty leading and trailing sections are dropped silently (a file that starts or ends with --- does not produce empty sections), as are sections between consecutive separators. A file with no separator is treated as a single section, so --multifile is safe to enable globally via goccia.json.
Per-runner behaviour.
GocciaRunnerruns each section in a fresh engine; results appear separately in human-readable output and as separatefiles[]entries in--output=json.GocciaTestRunnerruns each section as a separate test file; the aggregate counts (totalFiles,totalTests, etc.) include every section.GocciaBundleremits one.gbcper section.--output=<file>is rejected with--multifilebecause the input may expand to multiple outputs; pass a directory instead.GocciaBenchmarkRunnerproduces one file entry per section in the report.GocciaREPLaccepts--multifilefor symmetry but ignores it (the REPL takes no file arguments).
Combining with other options. --source-map=<file> is rejected with --multifile for the same reason as the bundler's --output=<file> — a single source-map output cannot represent multiple section sources. All other options compose normally, including --jobs for parallel section dispatch and goccia.json integration via "multifile": true.
Configuration File (goccia.json)
All CLI options can also be set via a project configuration file. The CLI discovers the file by walking up from the entry file's directory and checking for (in priority order):
goccia.tomlgoccia.json5goccia.json
The first file found is loaded and applied as the root config. When running multiple files (e.g. via GocciaTestRunner or GocciaBundler), a per-file config is also discovered from each file's directory. The full precedence is:
- CLI options (highest priority — always win)
- Per-file config (
goccia.toml,goccia.json5, orgoccia.jsonnearest to the file being processed) - Root config (discovered from the entry file's directory at startup, or supplied via
--config) - File extension default (
.mjsand.mtsinfer module source) - System default (engine defaults)
GocciaTestRunner keeps explicit multi-file test invocations isolated: when you pass more than one input path and do not pass --config, the first file's auto-discovered config is not promoted to a root config for the rest of the list. Each file still gets its nearest per-file config. Pass --config=<path> when you intentionally want one shared root config across an explicit test file list.
--config=<path> — Override auto-discovery and load the root config from an explicit location. Available on every TGocciaCLIApplication binary (GocciaRunner, GocciaTestRunner, GocciaBenchmarkRunner, GocciaBundler, GocciaREPL); GocciaScriptLoaderBare, GocciaTest262Runner, and GocciaWasmTestRunner do not take it.
The path may be either a file (any registered extension — .json, .json5, .toml; the parser is selected by extension), or a directory, in which case the CLI looks for goccia.toml → goccia.json5 → goccia.json in that directory only (same priority as auto-discovery). The directory form does not walk upward — that's the point of the explicit override.
# File form (use this exact file)./build/GocciaRunner example.js --config=./configs/strict.toml./build/GocciaTestRunner -P tests --config=./configs/ci.json# Directory form (find goccia.{toml,json5,json} inside, no walk-up)./build/GocciaRunner example.js --config=./configs/
Relative paths are resolved against the current working directory. A missing file or a directory with no recognised goccia.* is a hard error so a typo is not silently ignored. CLI options still take precedence over values from the file, and per-file configs continue to be discovered normally for individual files.
source-type follows that same precedence. Without an explicit CLI or config value, .mjs and .mts entry files are parsed and evaluated as module source; other script extensions default to script source.
{"mode": "bytecode","source-type": "module","warning-unsupported-features": true,"compat-asi": true,"compat-non-strict-mode": true,"compat-arguments-object": true,"compat-loose-equality": true,"compat-label": true,"compat-traditional-for-loop": true,"compat-for-in-loop": true,"compat-while-loops": true,"experimental-js-module-source": true,"strict-types": true,"unsafe-shadowrealm": true,"deterministic": true,"timeout": "5s","max-memory": "10MiB","max-stack": 2200,"inspect-depth": 5,"permissions": {"allow-net": ["api.example.com", "cdn.example.com"],"allow-ffi": true},"imports": {"@/": "./src/"}}
Config keys mirror CLI option names (e.g. --mode -> "mode", --max-memory -> "max-memory"). A config value is the value assigned to a config key and the setting it carries: boolean flags must be exactly true or false, and array-valued options like alias use JSON arrays. Capability grants and denies go in a permissions object, never at the top level; limits take the same units as on the command line. See Permissions for the permissions block, how its relative scopes resolve, and the removed keys that now fail with status 2. warning-unsupported-features is a parser diagnostic policy flag, not a compatibility flag: it restores warning/recovery behavior for disabled syntax without enabling that syntax's runtime semantics. The imports object is handled by the module resolver and coexists with CLI option keys. --deterministic (or "deterministic": true) freezes JavaScript-visible time at the Unix epoch, uses UTC, and starts a portable seeded random stream; --host-environment=./provider.js (or the "host-environment" config key) supplies custom providers instead. Timeouts, profiling, and benchmark measurement remain live; see Host Environment for the provider contract.
extends — A config file can inherit from a base config using the extends key. The path is resolved relative to the config file's directory. Child values override parent values:
{"extends": "../goccia.json","mode": "bytecode"}
This is useful for test subdirectories that need specific flags. For example, tests/language/asi/goccia.json can enable ASI for all tests in that folder:
{"compat-asi": true}
Likewise, tests/built-ins/FFI/goccia.json grants ffi only for the FFI tests, scoped to the fixture libraries (relative to the config file):
{"permissions": {"allow-ffi": ["../../../fixtures/ffi"]}}
Inputs named in config — A modules manifest, a globals file or module, and a host-environment module named in a config are read under the capability set of the script the config governs, judged with the directory of the config that names them as the project: relative paths resolve from that config file, and outside its directory they need a read grant. Under extends, a file a base config names is judged against the base's own directory. A JavaScript or TypeScript manifest or globals module runs in an engine of its own on the script's executor (interpreter or bytecode), so an error it throws is reported as it is for the same module passed with --globals. Only data crosses back, so a globals module a config names may export only values JSON carries as they are, at any depth; a function, symbol, BigInt, or other non-JSON value fails the run, naming its export path. A host-environment module is loaded as a guest module, never host-owned. The same options on the command line are unchanged. See Permissions and Virtual Modules.
Output paths in config — A config may set the options that write host files — log, audit-log, coverage-output, profile-output, source-map, the test runner's, benchmark runner's, and bundler's output (when it names a file), and, in GocciaRunner's sandbox section, the copy-rw inputs and diff-file — only to a path inside its own directory. A relative value resolves against the directory of the config file that declares it, existing directories along the path are resolved, and a symbolic link at the name itself is refused; anything else fails with status 1, naming the key and the config. The write itself is opened from the config's directory, checked to be the same directory it was, and walked one component at a time without following links (openat with O_NOFOLLOW on POSIX hosts), so a directory on the path swapped for a link during the run refuses the write rather than redirecting it; Windows re-checks the path just before writing instead. Sandbox write-back and the diff file are pinned the same way whether a config or the command line names them (Permissions — Write-back). The same options on the command line write wherever they are told to.
Config trust — A config's allow-* permissions and unsafe-* keys are requests: they take effect only once trusted, while every other key (compat-*, limits, import maps, aliases, and deny-* permissions) applies automatically. Before any file runs, the CLI checks every config that governs its inputs; an untrusted one fails the run with status 2 and a report naming each config and the command that would fix it. Trust is recorded in a per-user store outside the repository:
./build/GocciaTestRunner --trust tests/ # review and trust every config under tests/./build/GocciaTestRunner --list-trusted # what is trusted, and what changed since./build/GocciaTestRunner --untrust tests/ # forget it again./build/GocciaTestRunner -P tests # accept the requests for this run only (CI)./build/GocciaTestRunner --ignore-config-permissions tests # command-line grants only
The trust options (--trust, --untrust, --list-trusted, --yes, -P/--accept-config-permissions, --ignore-config-permissions, --trust-store=<path>) are command-line-only and available on every TGocciaCLIApplication binary (GocciaRunner, GocciaTestRunner, GocciaBenchmarkRunner, GocciaBundler, GocciaREPL); GocciaWasmTestRunner takes only -P, and GocciaScriptLoaderBare and GocciaTest262Runner read no config. See Permissions — Config trust for the store location and format, what a hash covers, and the report.
TOML equivalent (goccia.toml):
compat-asi = truemode = "bytecode"timeout = "5s"max-memory = "10MiB"max-stack = 2200inspect-depth = 5[permissions]allow-ffi = true
CLI vs. embedding — Config file discovery is automatic for all CLI applications (GocciaRunner, in both modes, GocciaTestRunner, GocciaBenchmarkRunner, GocciaBundler, GocciaREPL) because they inherit from TGocciaCLIApplication. When embedding the engine directly, config file loading is not automatic. Use the shared CLI.ConfigFile unit to get the same behavior.
Note: ApplyConfigFile only handles .json out of the box. To support .json5 and .toml config files, call EnsureConfigParsersRegistered (in Goccia.CLI.Application) first, as TGocciaCLIApplication.Execute does, or register your own parsers with RegisterConfigParser.
uses CLI.ConfigFile, CLI.Options, Goccia.CLI.Application;// Register the JSON5 and TOML parsers (required before discovery)EnsureConfigParsersRegistered;// Discover a config file from a starting directoryConfigPath := DiscoverConfigFile(EntryDir,['goccia'], ['.toml', '.json5', '.json']);// Parse it (with extends resolution) and apply to your optionsif ConfigPath <> '' thenApplyConfigFile(ConfigPath, YourOptions);
For import-map resolution only (without CLI option loading), use the module resolver API directly:
uses Goccia.Modules.Resolver;ConfigPath := TGocciaModuleResolver.DiscoverProjectConfig(EntryDir);if ConfigPath <> '' thenResolver.LoadImportMap(ConfigPath);
GocciaBundler (Bundler)
GocciaBundler is a dedicated tool for compiling source files to .gbc bytecode without executing them. It accepts the same input modes as GocciaRunner (file, multiple files, directory, stdin):
# Compile a single file (output: example.gbc alongside the source)./build/GocciaBundler example.js# Custom output path (the directory must already exist)mkdir -p dist./build/GocciaBundler example.js --output=dist/example.gbc# Compile all source files in a directory (1:1 .gbc output alongside each source)./build/GocciaBundler src/# Compile a directory to a specific output directory./build/GocciaBundler src/ --output=dist/# Compile from stdin (--output required)printf "const x = 2 + 2; x;" | ./build/GocciaBundler --output=out.gbc# Multiple positional input files./build/GocciaBundler a.js b.js c.js# Write source map alongside .gbc./build/GocciaBundler example.jsx --source-map# Explicit source map path (single file only)./build/GocciaBundler example.jsx --source-map=out.map# Enable automatic semicolon insertion during parsing./build/GocciaBundler example.js --compat-asi# Parallel compilation (default: CPU count)./build/GocciaBundler src/ --jobs=4
--output directories must already exist; the bundler does not create them. A missing directory for
several files or --multifile is currently reported as "--output must be a directory"
(#1279).
See bytecode-vm.md for the bytecode VM architecture and binary format.
GocciaRunner sandbox mode
In sandbox mode GocciaRunner runs one entry inside an isolated in-memory
virtual filesystem instead of against the host. --sandbox, any --copy or
--copy-rw input, or a trusted sandbox section in the root config turns it
on. The guest sees only that filesystem and may import "fs" and "goccia"
(Sandbox Modules).
Permissions — Sandbox mode specifies what it may
reach, how inputs are copied in and written back, and the config section.
./build.pas runner# Empty filesystem; the entry is copied to /main.js../build/GocciaRunner main.js --sandbox# Copy inputs read-only to /task.txt and /fixtures, then print a JSON diff../build/GocciaRunner main.js --copy task.txt --copy fixtures --diff# An entry inside a copied directory runs there: /src/main.js../build/GocciaRunner src/main.js --copy src# Run a path that exists only in the sandbox../build/GocciaRunner --copy tree=/ --entry=/main.js# Keep the changes a successful run makes under /src../build/GocciaRunner fix.mjs --copy-rw src --diff=unified# Bound the run and its filesystem; fetch reaches only api.example.com../build/GocciaRunner main.js --sandbox --max-memory=64MiB \--max-fs-bytes=4MiB --allow-net=api.example.com
| Option | Effect |
|---|---|
--sandbox | Run in sandbox mode with an empty virtual filesystem; the entry file is copied to /<name> |
--copy <host>[=<sandbox>] | Copy a host file or directory into the sandbox read-only (default /<basename>); enables sandbox mode. Repeatable |
--copy-rw <host>[=<sandbox>] | Copy like --copy, then write changed files back to the host after a successful run |
--entry <sandbox-path> | Run a path inside the sandbox instead of a host file |
--diff[=json|unified] | Print sandbox filesystem changes after the run (default: json) |
--diff-file <path> | Write the diff to a host file instead of printing it; the format comes from .json or .diff unless --diff=<format> names it |
--max-fs-bytes=<bytes> | Maximum bytes in the sandbox filesystem (default: 16MiB) |
--max-fs-nodes=<N> | Maximum files and directories in the sandbox filesystem (default: 4096) |
The engine options and limits apply as in host mode: --mode, --source-type,
--timeout, --max-memory, --max-instructions, --max-stack, and
--max-fetch-bytes all bound the sandboxed program, and configuration is
discovered as for any other run. Options that only make sense against the host
are usage errors (exit 2) in sandbox mode: standard input or -, a directory,
more than one positional input, a .gbc entry, --multifile, --output,
--coverage*, --profile*, --source-map, --host-environment, and
--global / --globals, also when only the config's sandbox section turns
sandbox mode on. --entry, --diff, --diff-file, and --max-fs-* on the
command line without sandbox mode are usage errors that name --sandbox and
--copy; max-fs-bytes and max-fs-nodes in a config file apply in sandbox
mode and are ignored, unvalidated, in host mode. A config's modules,
globals, and host-environment are not applied in sandbox mode, while
--module and --modules on the command line, the user's explicit choice, are.
Errors the host reports in sandbox mode, an invalid option value included, go
to standard error.
Standard output carries only what the guest prints — --print prints the last
value, undefined included, as in host mode — followed by a printed diff.
There is no timing banner. The write-back: report goes to standard error.
Diff output is explicit. JSON is the default format; it always includes a
separate metadataChanges array whose per-path changes object contains only
changed atimeMs, mtimeMs, ctimeMs, and birthtimeMs fields, so
timestamp-only changes never appear as content modifications. The unified
format never includes metadata. --diff-file=<path> writes the diff to a host
file instead of printing it, choosing JSON for .json and unified for .diff.
An explicit --diff=<format> wins over the extension, and any other extension
(.patch included) without one is an error.
The sandbox config section, the removed GocciaSandboxRunner flags, and the
rules for copy targets, entries, and write-back are in
Permissions — Sandbox mode.
Build Output
All compiled binaries go to the build/ directory:
| Binary | Source | Description |
|---|---|---|
build/GocciaREPL | source/app/GocciaREPL.dpr | Interactive read-eval-print loop |
build/GocciaRunner | source/app/GocciaRunner.dpr | Execute .js files or stdin input, with optional JSON output; in sandbox mode, run one entry inside a virtual filesystem |
build/GocciaScriptLoaderBare | source/app/GocciaScriptLoaderBare.dpr | Execute file or stdin source with the core engine and CLI-local print; no loader runtime profile or conformance-only host hooks |
build/GocciaTest262Runner | source/app/GocciaTest262Runner.dpr | Discover, shard, execute, classify, and report the pinned Test262 corpus using isolated native workers |
build/GocciaTestRunner | source/app/GocciaTestRunner.dpr | JavaScript test runner |
build/GocciaTOMLComplianceRunner | source/app/compliance/GocciaTOMLComplianceRunner.dpr | Native pinned toml-test runner |
build/GocciaFuzzHarness | source/app/GocciaFuzzHarness.dpr | Crash harness that drives one input through the lexer, parser, and both executors |
build/GocciaWasmTestRunner | source/app/GocciaWasmTestRunner.dpr | Single-threaded, manifest-driven test runner for the Wasm/LAKON host |
build/GocciaBenchmarkRunner | source/app/GocciaBenchmarkRunner.dpr | Performance benchmark runner for files or stdin input |
build/GocciaBundler | source/app/GocciaBundler.dpr | Bundler (source to .gbc) |
build/Goccia.Values.Primitives.Test | *.Test.pas | Pascal unit test binaries |
Intermediate files (.o, .or, .ppu, generated resource lists) go to build/compiled/ to keep the source tree clean. Each executable target compiles into an isolated build/compiled/targets/<target>/ unit cache, and Pascal unit test programs compile into isolated build/compiled/tests/<test-program>/ subdirectories so one program's FPC unit cache cannot poison the next.
Compiler Configuration
config.cfg (Shared Flags)
-Fu./source/units # Engine unit search path-Fu./source/generated # Generated runtime data unit search path-Fu./source/shared # Shared infrastructure unit search path-Fu./source/app # CLI application unit search path-Fu./souffle # Legacy Souffle VM path; the directory no longer exists-Fi./source/units # Engine include file search path-Fi./source/shared # Shared include file search path-Fi./souffle # Legacy Souffle VM path; the directory no longer exists-FUbuild/compiled # Default unit output directory-FEbuild # Executable output directory
These path flags are shared by both build modes. Mode-specific flags are added by build.pas. Each build target overrides -FU to isolate its own Pascal unit cache.
Build Mode Flags
| Flag | Development (--dev) | Production (--prod) |
|---|---|---|
| Optimization | -O- (disabled) | -O4 (aggressive) |
| Debug info | -gw -godwarfsets (DWARF) | — (none) |
| Line info | -gl (debug line numbers) | — |
| Stack checking | -Ct | — |
| Range checking | -Cr | — |
| Assertions | -Sa | — |
| Strip symbols | — | -Xs |
| Smart linking | — | -CX -XX |
| Define | — | -dPRODUCTION |
Shared.inc and Goccia.inc
Common compiler settings live in source/shared/Shared.inc, which is included by source/units/Goccia.inc. Engine units include Goccia.inc (which pulls in Shared.inc and adds Goccia-specific defines); shared infrastructure units include Shared.inc directly.
Shared.inc provides:
{$IFDEF FPC}{$mode delphiunicode} {$H+} // Delphi syntax and UTF-16 `string`{$ENDIF}{$IFNDEF PRODUCTION}{$overflowchecks on} // Runtime arithmetic overflow detection{$rangechecks on} // Runtime array bounds checking{$ENDIF}{$modeswitch advancedrecords} // Records with methods
Under the current project settings, string and Char are UTF-16 under both FreePascal and Delphi. Length, Copy, indexing, source spans, and ordinary ECMAScript String operations therefore count UTF-16 code units. Encoded data uses TBytes and the explicit adapters in TextEncoding.pas.
Overflow and range checks are enabled in development mode for safety. In production builds, the -dPRODUCTION define disables these checks for maximum performance.
How build.pas Works
The build script is a FreePascal program that runs via the instantfpc shebang:
#!/usr/bin/env instantfpc
It:
- Creates the
build/directory if it doesn't exist. - Parses
--dev/--prodflags to determine the build mode (defaults to--dev). - Parses remaining arguments to determine which components to build.
- Counts and prints source statistics: total lines of code (LOC), significant lines of code (SLOC, excluding blanks and comments), and file count.
- Calls
fpcwith@config.cfg, a target-specific-FUbuild/compiled/...override, mode-specific flags, and suppressed verbose output flags (-vw-n-h-i-l-d-u-t-p-c-x-). - If the
FPC_TARGET_CPUenvironment variable is set, prepends-P<arch>to the compiler arguments (used by CI to target x86_64 on Windows where the FPC package defaults to i386). - For the
teststarget, auto-discovers all*.Test.pasfiles insource/units/andsource/shared/, compiling each program with its ownbuild/compiled/tests/<test-program>/unit-output directory.
The GitHub Actions cross-compilation workflow uses a reduced cached FPC toolchain rather than a full target-side FCL install. It prebuilds the RTL, rtl-objpas, rtl-generics, and fcl-process, and also caches the official fcl-base sources so cross builds can resolve units such as Base64 on demand from the shipped FPC packages.
Project Structure for Compilation
GocciaScript/├── build.pas # Build script (entry point)├── config.cfg # Shared FPC configuration├── source/│ ├── app/ # CLI applications│ │ ├── GocciaREPL.dpr # REPL program source│ │ ├── GocciaRunner.dpr # Runner program source (host and sandbox mode)│ │ ├── GocciaScriptLoaderBare.dpr # Core-engine-only script loader│ │ ├── GocciaTest262Runner.dpr # Native Test262 conformance runner│ │ ├── GocciaTestRunner.dpr # Test runner program source│ │ ├── GocciaBenchmarkRunner.dpr # Benchmark runner program source│ │ ├── GocciaBundler.dpr # Bundler│ │ ├── Goccia.CLI.Application.pas # CLI application base class│ │ ├── Goccia.CLI.Help.pas # CLI help generation│ ├── shared/ # Shared infrastructure│ │ ├── Shared.inc # Common compiler settings│ │ ├── TestingPascalLibrary.pas # Pascal test framework (TTestSuite)│ │ ├── TimingUtils.pas # Cross-platform timing and duration formatting│ │ ├── HashMap.pas # Purpose-built hash maps│ │ ├── OrderedMap.pas # Ordered hash maps│ │ ├── OrderedStringMap.pas # String-keyed ordered maps│ │ ├── StringBuffer.pas # String buffer│ │ ├── FileUtils.pas # File utilities│ │ ├── BaseMap.pas # Base map type│ │ └── JSONParser.pas # JSON parser│ ├── generated/ # Generated runtime data│ │ ├── Generated.TimeZoneData.pas # Timezone resource metadata/link unit│ │ └── Generated.TimeZoneData.res # Embedded IANA TZif resource│ └── units/ # Goccia engine units│ ├── Goccia.inc # Goccia compiler directives (includes Shared.inc)│ ├── Goccia.Bytecode*.pas # Bytecode definitions, templates, modules, binary I/O│ ├── Goccia.VM*.pas # Bytecode VM, frames, closures, upvalues, exceptions│ ├── *.pas # All engine unit source files│ └── *.Test.pas # Pascal unit test programs└── build/ # All output (gitignored)├── compiled/ # Intermediate files│ ├── *.o # Object files│ ├── *.or # Resource object files│ ├── *.ppu # Compiled unit files│ ├── targets/ # Isolated executable target unit caches│ └── tests/ # Isolated Pascal test unit caches└── (binaries) # Executable output
Generated Timezone Data
source/generated/Generated.TimeZoneData.pas and source/generated/Generated.TimeZoneData.res are produced by scripts/generate-timezone-data.js. By default, the generator downloads the latest IANA tzdata-latest.tar.gz, compiles it with zic, packs the resulting TZif files into a single resource payload, and emits a small Pascal unit that links the resource. Pass a local zoneinfo directory, a local tzdata tarball, or an explicit URL to generate from a different source. .github/workflows/timezone-data-bump.yml resolves the latest immutable IANA release URL on the first day of each month, regenerates both files, and opens an automated PR only when the generated output changes.
The generator requires zic, tar, and fpcres. fpcres writes the FreePascal resource consumed by {$R Generated.TimeZoneData.res}.
Temporal embeds this generated timezone resource by default through source/units/Goccia.inc. Define GOCCIA_TEMPORAL_NO_EMBEDDED_TZDATA to build without the resource fallback.
Generated Intl Data
source/generated/Generated.IntlData.pas and source/generated/Generated.IntlData.res are produced by scripts/generate-intl-data.js. By default, the generator downloads CLDR JSON data, packs it into a single resource payload, and emits a small Pascal unit that links the resource.
The generator requires fpcres. fpcres writes the FreePascal resource consumed by {$R Generated.IntlData.res}.
Intl embeds this generated CLDR resource by default through source/units/Goccia.inc. Define GOCCIA_INTL_NO_EMBEDDED_CLDR to build without the resource fallback.
Generated Unicode Data
source/generated/Generated.UnicodeData.pas and source/generated/Generated.UnicodeData.res are produced by scripts/generate-unicode-data.js. By default, the generator downloads Unicode Character Database (UCD) files for a given Unicode version, packs General_Category, Script, Script_Extensions, and binary property range tables into a single resource payload, and emits a small Pascal unit that links the resource.
source/generated/Generated.UnicodeCaseData.pas and source/generated/Generated.UnicodeNormalizationData.pas are produced by their namesake scripts. They keep ECMAScript default casing and normalization semantics identical across FPC and Delphi without making ICU or an operating-system locale service a runtime dependency.
The generator requires fpcres. fpcres writes the FreePascal resource consumed by {$R Generated.UnicodeData.res}.
RegExp embeds this generated UCD resource by default through source/units/Goccia.inc. Define GOCCIA_REGEXP_NO_EMBEDDED_UCD to build without the resource fallback. When system ICU is available, the engine queries ICU directly for Unicode property data; the embedded resource serves as a fallback for systems without ICU.
CI/CD
GitHub Actions CI is split into two workflow files, plus nightly.yml, which checks every three hours whether a newer commit has passed ci.yml on main and publishes it as the nightly prerelease if so:
ci.yml — Push to main + tags
Job graph: supersede -> toolchain -> build -> test / toml-compliance / json5-compliance / test262 -> test262-merge / awfy / jetstream-workload -> jetstream / web-tooling-workload -> web-tooling / benchmark / cli -> artifacts (main) / release (tags). jetstream-plan and web-tooling-plan feed their workload matrices, and test-structure runs on its own.
All matrix strategies use fail-fast: false, so one platform failing does not cancel other platforms. The lanes that fan out from build (test, toml-compliance, json5-compliance, test262, awfy, jetstream-workload, web-tooling-workload, benchmark, cli) do not depend on each other; only the merge jobs wait for their own lane (test262-merge for test262, jetstream for jetstream-workload, web-tooling for web-tooling-workload). The first job, supersede, cancels superseded main runs as a whole so their builds stop holding macOS runners. GitHub does not process runs in push order, so it compares commits rather than relying on a concurrency group. On a push to main, it lists the workflow's other queued or in-progress main push runs and compares each run's commit with its own. It cancels runs whose commit is an ancestor of its own, and cancels its own run when its commit is an ancestor of another run's. Runs for the same commit and diverged commits are left alone, and a failed API call only logs a warning. toolchain waits for supersede, so cancellation happens before any macOS work. Tag runs and every workflow_dispatch run, including one on main, are never cancelled. The shard-merging report jobs (test262-merge, jetstream, web-tooling) and the AWFY and JetStream publish steps use !cancelled() rather than always(), so they still run after a failed shard or workload but a cancelled run publishes nothing. Each Blob publisher uploads the report before the daily pointer that makes it visible, so an interrupted publish leaves either the previous pointer or one to a complete report.
Runs on the full platform matrix:
| OS | Architecture |
|---|---|
| Ubuntu Latest | x64, ARM64 |
| macOS Latest | x64, ARM64 |
| Windows Latest | x86 (i386-win32) |
build — Installs FPC, compiles the source/app/*.dpr programs with --prod, stages those binaries plus Pascal test executables, and uploads that staged set as intermediate artifacts.
test (needs build) — Runs all JavaScript tests and Pascal unit tests on all platforms.
toml-compliance — Downloads the prebuilt GocciaTOMLComplianceRunner, prepares the pinned toml-test checkout, trusts the runner exit status, validates the report envelope, and uploads the per-platform JSON report. The pin is bumped weekly by .github/workflows/toml-test-bump.yml.
json5-compliance — Downloads GocciaTestRunner, verifies that the committed generated parser suite names the SHA in tests/compliance/json5.pin, and runs that suite together with the local stringify suite. The workflow trusts TestRunner's exit status, validates its JSON report shape, and uploads that report.
test262 (needs build, ubuntu-latest x64 only, non-blocking) — Downloads the gocciascript-x86_64-linux build, checks out tc39/test262 at the SHA pinned in scripts/test262-suite-sha.txt, and runs a matrix of native GocciaTest262Runner shards. GitHub's strategy index and total define each shard; the merge step discovers the shard reports by glob and uses scripts/run_test262_suite.ts --merge-shards to produce the canonical report. The run step uses continue-on-error: true because the conformance lane is allowed to carry known steady-state failures while the engine closes compatibility gaps. On main, the JSON is also stashed via actions/cache/save under test262-baseline-<sha> so the PR workflow can compute Δ vs main, and cd website && bun run publish-test262 ../test262-results.json publishes the compressed run report plus its UTC daily dashboard pointer to Vercel Blob when BLOB_READ_WRITE_TOKEN is configured. Main runs also upload the test262-profile artifact and publish matching aggregate/detail profile payloads under the separate test262-profiles/ Blob namespace for weekly performance review. The one-off cd website && bun run backfill-test262 command seeds retained artifact reports and reruns expired historical days directly into Blob. The pin is bumped weekly by .github/workflows/test262-bump.yml. See docs/test262.md for the harness and profile report contracts.
awfy (needs build, ubuntu-latest x64 only) — Downloads the gocciascript-x86_64-linux build, installs QuickJS, installs the latest Node Current release via actions/setup-node for the Node reference engine, checks out smarr/are-we-fast-yet at the SHA pinned in perf/awfy/manifest.json, and runs node scripts/awfy-ci-report.js --awfy-dir awfy-suite/benchmarks/JavaScript --output awfy-report.json. The report set is ciReport from the manifest: all pinned AWFY JavaScript benchmarks under Goccia bytecode, QuickJS, and Node Current, with five interleaved samples per engine. The job uploads awfy-report as a 30-day artifact. On main, cd website && bun run publish-awfy ../awfy-report.json publishes the compressed report plus its UTC daily pointer to the separate awfy/ Vercel Blob namespace when BLOB_READ_WRITE_TOKEN is configured.
jetstream-plan / jetstream-workload / jetstream (needs build, ubuntu-latest x64 only) — Reads the six-workload manifest into a fail-independent matrix, then each workload job downloads the same production Goccia binary, installs QuickJS and Node Current, and sparse-checks out the exact upstream files at the pinned JetStream 3.0 commit. Each shard keeps five interleaved process repetitions per engine and checkpoints its report after every sample; the final job merges all shards into jetstream-report, publishes complete or degraded reports on main unless the run was cancelled, then preserves the validation failure as the job result. The compressed report and UTC daily pointer live under the independent jetstream/ Blob namespace. The website combines retained JetStream and AWFY summaries at /performance and fetches only the latest full reports for workload detail.
web-tooling-workload / web-tooling (needs build, ubuntu-latest x64 only) — The workload matrix downloads the gocciascript-x86_64-linux build, installs the latest Node Current release as the upstream build runtime, checks out v8/web-tooling-benchmark at the SHA pinned in perf/web-tooling/manifest.json, and installs upstream dependencies without running postinstall. Each matrix job runs one pinned workload under Goccia bytecode and uploads its report shard. Each workload bundle contains one upstream benchmark module and only its declared third_party payloads; the Goccia harness times one direct fn() invocation per process sample. The web-tooling merge job requires one structurally complete shard for every manifest workload and uploads the normalized all-workload web-tooling-report as a 30-day artifact. On main, cd website && bun run publish-web-tooling ../web-tooling-report.json publishes the compressed report plus its UTC daily pointer to the separate web-tooling/ Vercel Blob namespace when BLOB_READ_WRITE_TOKEN is configured.
benchmark (needs build) — Runs all benchmarks on all platforms. On main (ubuntu-latest x64), it additionally emits JSON and validates the report shape. PR comparison no longer reads a cached baseline from here — each PR builds and benchmarks main on its own runner (ADR 0076). The main bytecode benchmark lane also captures deterministic VM profile details, uploads the benchmark-profile artifact, and publishes aggregate/detail profile payloads under the separate benchmark-profiles/ Blob namespace when BLOB_READ_WRITE_TOKEN is configured.
cli (needs build) — Downloads pre-built binaries and runs CLI behavior smoke tests on all platforms via Bun: the shared test assertions, options across all apps, lexer numeric-separator rejection, parser error display, config-file loading, app-specific features including the runner's sandbox mode, capability flags, limits and permission defaults (scripts/test-cli-permissions.ts), embedded resources, Intl taint robustness, the differential suites, and coverage parity between execution modes. Windows runs additionally assert that GocciaRunner does not link OpenSSL DLLs (HTTPS must use the platform TLS stack statically).
artifacts (needs test + toml-compliance + json5-compliance + awfy + jetstream + web-tooling + benchmark + cli, main only) — Uploads production binaries after all checks pass, deriving the executable names from the source/app/*.dpr entrypoints. The rolling nightly prerelease is not a ci.yml job: .github/workflows/nightly.yml runs on a schedule, every three hours, and looks for the newest CI run that concluded success for a push to main, so a cancelled, failed, tag, or dispatched run is never published. It publishes when that run's commit is ahead of the commit the nightly tag points at (or has diverged from it, after a rewritten main), and does nothing when no newer commit has passed or the run's one-day artifacts have expired; a manual run on main publishes the newest passing build at once. It checks out the run's head_sha and downloads that run's gocciascript-<target> artifacts. If any of the six target artifacts or packaged archives is missing, the job fails without touching the existing release; otherwise it restores the executable bit on the binaries (artifacts do not keep file modes), packages the six targets with tests/, benchmarks/, and examples/, and deletes and recreates the nightly prerelease at that commit under the non-cancelling nightly-release concurrency group. A commit newer than that run's, still in CI or failed, does not hold the nightly back: the newest build that passed is what gets published. Keeping the job out of ci.yml means a cancelled main run can never stop between deleting and recreating the release. A scheduled workflow uses the default branch's copy of nightly.yml, so changes to it take effect only after merge.
release (needs test + toml-compliance + json5-compliance + awfy + jetstream + web-tooling + benchmark + cli, tags only) — Downloads all platform build artifacts, stages only the shipped binaries derived from the source/app/*.dpr entrypoints, bundles them with tests/, benchmarks/, and examples/ into per-platform archives (.tar.gz for Linux/macOS, .zip for Windows), generates categorized release notes via git-cliff (cliff.toml), and creates a GitHub release using softprops/action-gh-release.
pr.yml — Pull requests
Job graph: build (+ build-main) -> test / benchmark -> benchmark-comment / awfy-report -> awfy-comment / jetstream-workload -> jetstream-report -> jetstream-comment / web-tooling-workload -> web-tooling-report -> web-tooling-comment / test262 -> test262-merge -> test262-comment / boot-timing / cli. test, benchmark, and boot-timing feed timing-comment; docs and test-structure run on their own.
Runs on ubuntu-latest x64 only; workload suites may fan out through matrices on that platform.
build — Installs FPC, compiles all binaries with --prod, stages the source/app/*.dpr binaries plus Pascal test executables, and uploads that staged set as build-pr.
test (needs build) — Runs all JavaScript tests and Pascal unit tests.
benchmark (needs build + build-main) — Compares the PR against a main build measured on the same runner. A separate build-main job builds the benchmark runner from the PR's base commit (github.event.pull_request.base.sha); the benchmark job then warms up the runner (discarded) and benchmarks the main build and the PR build back-to-back, so deltas reflect the diff rather than cross-runner variance (issue #815, ADR 0076). It posts a collapsible comparison comment grouped by file: each file section shows the same-runner main and PR opsPerSec point estimates side by side, each carrying its min/max ops/sec range in brackets. Classification (in scripts/benchmark-compare.js, unit-tested by scripts/test-benchmark-compare.ts) uses range overlap: fully above the main range is an improvement, fully below is a regression, and overlapping ranges are treated as unchanged noise. The comment also reports the measured median per-run variance as a noise floor. Percentage deltas are secondary context, files with significant changes are auto-expanded, and the comparison is advisory (comment-only, never merge-blocking). The deterministic profile diff (opcode/allocation counts) is regenerated from the same main build. If no main build is available, shows results without comparison.
test262 (needs build, non-blocking) — Checks out tc39/test262 at the pinned SHA and runs a matrix of native GocciaTest262Runner shards. GitHub's strategy index and total define each shard, and scripts/run_test262_suite.ts --merge-shards validates and merges every downloaded shard report. Failing tests do not fail the job. The downstream test262-comment job (if: always()) restores the most recent test262-baseline- cache entry from main, then bun scripts/run_test262_suite.ts --comment test262-results.json <baseline> builds the markdown body and the workflow posts/updates a comment using marker <!-- test262-results -->. The comment shows a per-category breakdown (built-ins, harness, intl402, language, staging) with Δ-vs-main columns when a baseline is cached, an "Areas closest to 100%" sub-table, and a collapsible per-test delta list. PR CI does not generate or upload the full-corpus test262 profile artifact.
awfy-report (needs build + build-main) — Runs the same ciReport AWFY set as full CI on the PR x64 build, using Goccia bytecode, QuickJS, and the latest Node Current release with five interleaved samples per engine. It uploads the normalized awfy-report JSON artifact. The downstream awfy-comment job posts or updates an AWFY Results comment with median timings and geomean ratios; min/max/CV and raw samples remain in the artifact.
jetstream-plan / jetstream-workload / jetstream-report (needs build + build-main) — Runs each frozen JetStream workload in parallel under the main and PR Goccia bytecode loaders, QuickJS, and Node Current. Engine samples remain interleaved within a workload and each shard keeps five repetitions; the merge job restores manifest order, validates compatible metadata, recomputes geomean ratios, and uploads the normalized report. The downstream jetstream-comment job posts or updates a JetStream 3 Performance Barometer comment with separate QuickJS and Node.js reference ratios.
web-tooling-workload / web-tooling-report (needs build) — Runs the same direct-invocation workload matrix as full CI on the PR x64 build, using Goccia bytecode only, then validates and merges the shards into the normalized web-tooling-report JSON artifact. The downstream web-tooling-comment job posts or updates a Web Tooling Benchmark comment with per-workload build/execution status and Goccia runs/s where available; full stdout/stderr for failures and min/max/CV remain in the artifact.
cli (needs build) — Runs CLI behavior smoke tests via Bun (scripts/test-cli-assertions.ts, scripts/test-cli.ts, scripts/test-cli-lexer.ts, scripts/test-cli-parser.ts, scripts/test-cli-config.ts, scripts/test-cli-apps.ts, scripts/test-cli-permissions.ts, scripts/test-cli-embedded-resources.ts, scripts/test-cli-intl-taint.ts, scripts/test-cli-differential.ts). test-cli-permissions.ts covers the capability flags, limits, and permission defaults. test-cli-apps.ts includes GocciaScriptLoaderBare coverage for stdin, -, input files, CLI-local print, module source type, absence of the loader runtime profile, and --mode=interpreted|bytecode (both values plus invalid-value rejection), plus GocciaRunner sandbox-mode coverage for --copy / --copy-rw inputs, the sandbox config section, virtual fs, $, shared and child-sandbox runScript / shell goccia, bytecode mode, diff output, the engine resource and fetch-policy options, and the runScript failure kinds for every guest-reachable failure and every host-set ceiling.
FPC is only installed once per platform in the build job. In ci.yml, the test, AWFY, JetStream, Web Tooling, benchmark, cli, TOML, JSON5, and test262 conformance jobs reuse the pre-built binaries and artifacts from that job; in pr.yml, the test, AWFY, JetStream, Web Tooling, benchmark, test262, and cli jobs do the same.
Report publication uses .github/scripts/publish-website-report.sh for the optional Blob token check and one website dependency installation per CI job. The workflow retains report arguments, timestamps, artifact conditions, and profile archive creation. Checked-out PR report jobs share .github/scripts/upsert-pr-comment.js; report builders retain their markdown and comment markers.
Changelog
The project maintains a CHANGELOG.md generated from git history using git-cliff. The configuration lives in cliff.toml at the project root.
How It Works
Commits are categorized by their leading verb into groups:
| Verb prefix | Category |
|---|---|
Add, Implement, Support, Allow, Create, Enable | 🚀 Added |
Fix, Fixes, Route, Thread, Harden, Handle | 🐛 Fixed |
Improve, Include, Make | ✨ Improved |
Replace, Optimize, Promote, Eliminate, Bypass, Inline | ⚡ Performance |
Remove | 🗑️ Removed |
Refactor, Extract, Update, Strip | 🏗️ Internal |
Commits whose changed paths are at least half under website/ | 🌐 Website |
This table shows common prefixes. See cliff.toml for the complete list of patterns, which includes additional verb-specific and phrase-specific matchers. Website commits are detected by changed path before prefix grouping so site changes stay separate from language/runtime changes. The path check requires the website to be the predominant surface — at least half the commit's changed paths — so a cross-stack change that happens to touch one file under website/ still lands in its prefix group rather than 🌐 Website. Release sections are generated only for semver-style tags such as 0.7.0 or v0.7.0; moving tags such as nightly are ignored.
Generating the Changelog
# Install git-cliffbrew install git-cliff # macOScargo install git-cliff # Any platform with Rust# Generate full changeloggit-cliff -o CHANGELOG.md# Preview unreleased changesgit-cliff --unreleased# Preview the latest release notesgit-cliff --latest --strip header
CI Integration
Releases are cut with the /create-release flow, and publishing is owned by CI. /create-release's job ends at pushing the tag: the changelog and version bump land on a release/<version> branch through a squash-merged release PR before the tag is created, so the tag always contains its own changelog (the release commit is chore(release): <version>, which cliff.toml skips). Do not run /create-release's own publish step (gh release create) for this repo — pushing the tag triggers the ci.yml release job, which is the only place the built platform archives exist. That tag-triggered job only publishes: it renders the latest release section with orhun/git-cliff-action (--latest) and creates the GitHub release with the archives via softprops/action-gh-release. It does not regenerate or commit the changelog — that already happened before the tag, which keeps each release self-contained. GitHub's auto-generated release notes (.github/release.yml) provide an additional PR-label-based view.
PR Labels
PRs should be labeled for the GitHub release notes categorization:
| Label | Category |
|---|---|
new feature | 🚀 New Features |
spec compliance | 📐 Compliance |
bug, os specific | 🐛 Bug Fixes |
performance | ⚡ Performance |
documentation | 📖 Documentation |
internal | 🏗️ Internal |
Auto-Formatter
The project includes ./format.pas, an instantfpc script that auto-fixes Pascal source files. It enforces uses clause ordering, PascalCase function names, parameter A prefix naming, and stray space removal (spurious spaces before ;, ), ,). No build step is needed — it runs directly via the instantfpc shebang.
./format.pas # Format all project Pascal files./format.pas --check # Check only (exit 1 if changes needed)./format.pas file.pas # Format specific files
Pre-Commit Hook
The formatter runs automatically on staged .pas/.dpr files before each commit via Lefthook. To enable this after cloning:
# Install Lefthook (see CONTRIBUTING.md for all platforms)brew install lefthook # macOSsudo snap install lefthook # Linuxscoop install lefthook # Windows# Register git hookslefthook install
The hook configuration is in lefthook.yml. It auto-stages formatting fixes (stage_fixed: true) so you don't need to git add after the hook reformats files.
Direct FPC Compilation
If you need to bypass the build script:
fpc @config.cfg -vw-n-h-i-l-d-u-t-p-c-x- source/app/GocciaREPL.dprfpc @config.cfg -vw-n-h-i-l-d-u-t-p-c-x- source/app/GocciaRunner.dprfpc @config.cfg -vw-n-h-i-l-d-u-t-p-c-x- source/app/GocciaTestRunner.dprfpc @config.cfg -vw-n-h-i-l-d-u-t-p-c-x- source/app/GocciaBenchmarkRunner.dprfpc @config.cfg -vw-n-h-i-l-d-u-t-p-c-x- source/app/GocciaBundler.dpr
The -v flags suppress verbose output to keep the build clean. Use fpc @config.cfg source/app/GocciaREPL.dpr for full verbose output during debugging.
Build System Design Decision
The build script (build.pas) is a FreePascal script executed via instantfpc — a cross-platform, out-of-the-box solution within the FreePascal ecosystem that requires no external build tools.