Browse documentation

Precompiled bytecode modules

Executive Summary

  • Compile module libraries once and load verified artifacts from memory.
  • Register buffers under import addresses; absent disk companions can fall back to source.
  • Keep compilation and execution profiles identical; mismatches fail before execution.
  • Bytecode imports retain the existing module linker and its live bindings and cycles.

API and ownership

Compile libraries once with the same language options used by the consuming engine. GocciaBundler library.mjs produces an importable library.gbc. In-process hosts can parse with TGocciaSourcePipeline.Parse and call Engine.CompileImportableModule(Parsed.ProgramNode). The engine retains the compiled module; serialize it before freeing the engine. Raw TGocciaCompiler callers must explicitly assign Module.CompileProfile using BuildBytecodeCompileProfile from Goccia.Bytecode.Profile.Options with the actual parse options and settled goal. The compiler cannot infer parser options from an AST; serialization refuses an unstamped module.

Goccia.Bytecode.Binary provides:

APIOwnership and validation
SaveModuleToBuffer(Module): TBytesReturns an owned serialized artifact
LoadModuleFromBuffer(Bytes): TGocciaBytecodeModuleVerifies the artifact; caller frees the returned module
LoadModuleFromBuffer(Data, Size): TGocciaBytecodeModuleBorrows a pointer for the call only; rejects invalid sizes and retains no pointer into the input
LoadModuleFromFile(Path)Reads a bounded buffer and uses the same verification path

To supply imported libraries from a cache, register their serialized buffers under the addresses the imports resolve to:

Engine.RegisterBytecodeModule('virtual:library', LibraryBytes);
// Source can now import from "virtual:library".
// The engine owns a copy; LibraryBytes may be changed or released.
Result := Engine.Execute;

RegisterBytecodeModule(Address, Bytes, BaseAddress) also accepts a base address for relative registrations. It uses the existing virtual module registry's normalization, alias and replacement rules. The registered address governs relative imports and import.meta, so an artifact can be relocated without trusting its original compiler filename. A custom content provider with its own path namespace overrides NormalizeEntryAddress; the sandbox provider normalizes buffered entries without requiring their source files to exist.

A caller-owned module loader may outlive its engine. Engine teardown detaches its bytecode callbacks and removes its borrowed decoded templates. Registered buffers remain owned by the loader and can be copied into another engine with ModuleLoader.CopyVirtualModulesFrom. Prepared entries registered through RunModuleForSourceType serialize lazily when copied, so copy those while their owning engine is alive; copying an expired prepared entry raises an error and leaves the destination registry unchanged. A successful registry copy discards the destination's decoded registration cache so subsequent imports use the copied bytes.

Engine.RunBytecodeBuffer(Bytes, Address) runs a compiled entry with normal module linking and runtime-idle handling. It returns the entry's completion value. Registered modules and ordinary source modules can coexist in the same graph. No lexing, parsing or compilation of an artifact's body occurs on load. Deferred imports, cycles, live bindings and top-level await use the same linker as source modules. A source-phase import still needs the target's source text.

For file-based caches, set Engine.ModuleLoader.PreferBytecode := True. An import of library.mjs then selects library.gbc when present and falls back to source when absent. An explicit .gbc import loads bytecode directly. Extensionless and directory-index imports also find companions after source removal. The stored source extension supplies the logical runtime address; imports selecting the same companion share one module record. A present artifact that is invalid or has the wrong profile raises an error; it never silently falls back to source. Content-provider integrity checks and module read capabilities still apply.

The artifact records the effective compatibility switches, preprocessors, strict-types setting, unsupported-feature warning recovery and script/module goal. Execution refuses a profile mismatch before the module body runs. With stDetect, each entry uses the recorded goal without changing Engine.SourceType; an explicit conflicting goal is an error. Use Engine.ValidateBytecodeModule(Module) before retaining a low-level loaded module or running host setup that depends on its acceptance. It returns the settled goal without changing engine settings.

The older RunModule and RunModuleInScope APIs also check profiles for loaded artifacts. They accept standalone bytecode only. Use RunModuleForSourceType(Module, Address) or RunBytecodeBuffer for artifacts with module-linking metadata so imports and declarations are initialized.

Artifacts are versioned and must be regenerated after incompatible format changes. See Bytecode VM — Binary Format and ADR 0153.

Command line

./build/GocciaBundler library.mjs
./build/GocciaRunner --prefer-bytecode app.mjs

The bundler emits library.gbc beside the source. --prefer-bytecode selects it for an import of library.mjs, even if the source has been removed. The option also accepts the prefer-bytecode config key. Without the option, ordinary imports continue selecting source; an explicit .gbc import selects bytecode directly.

A compiled entry uses its recorded source extension in the runtime directory, so a dependency importing the original entry name reaches the same module. Buffer APIs instead use the explicit address supplied by the embedder.

Pass the same compatibility and strict-types options when compiling and running. A library with no static import/export declaration can be compiled with --source-type=module. A .gbc entry adopts the recorded goal by default; an explicit conflicting --source-type is rejected.

Verification and measurement

bun scripts/test-cli-aot.ts compares source and artifact execution for module graphs, smol-toml and a generated large library. It removes compiled source files before running bytecode. The CLI application suite invokes it in both PR and platform CI.

The native loading benchmark measures parse/compile against buffered loading, verification and declaration-plan materialization, with serialization and execution excluded from both paths.