Skip to main content
@eco-incorp/sauce-compiler compiles SauceScript to Sauce bytecode. It is the Rust compiler built to WebAssembly, with no file system access: you pass a synchronous resolve function that returns each module’s bytes. @eco-incorp/sauce pins it at exactly 2.6.1, the version this page documents, and re-exports it from @eco-incorp/sauce/compiler. Import the compiler from there.
Compiler output runs on the engine releases recorded for its exact compiler version, and @eco-incorp/sauce pins the compiler with that release set. A program compiled by any other compiler version may not run on the deployed engines. Do not install @eco-incorp/sauce-compiler separately for programs you send. The pairing is listed in release sets and compiler pairing.

What @eco-incorp/sauce/compiler exports

The subpath has no argument codec: every program takes ABI-encoded arguments (see Arguments).

Compile a program in Node.js

Prerequisites: Node.js 24 or newer, and @eco-incorp/sauce installed in an ES module project. Save the program as src/main.sauce.ts:
src/main.sauce.ts
Then compile it:
compile.ts
bytecode is the program without its arguments. main’s arguments are ABI-encoded from entryParams and appended to it. To compile a program for a chain by name, with its token symbols, protocol namespaces and @sauce/token in scope, use routes.compileSauceProgram instead.

compile

Compiles a SauceScript program to Sauce bytecode. It is synchronous. Validation, compile and resolve failures throw an error with diagnostics.

CompileOptions

CompileResult

Arguments

Every program takes ABI-encoded arguments (entryEncoding: "abi-v1"). Encode the values with viem’s encodeAbiParameters(result.entryParams, values) and append them to bytecode; that payload is the program of Pot.cook(engine, program).
arguments.ts
A top-level tuple of fixed size is marked indirect: true: encode it as a dynamic parameter, with its head word holding the byte offset of its encoding. With no indirect parameter, encodeAbiParameters(entryParams, values) is the payload as it stands. A named type the entry module imports is laid out from its fields’ types alone, so its Address and Account fields read as uint256 and its string fields as bytes. routes.encodeEntryArguments(program, values) from @eco-incorp/sauce checks values against main’s declared types and encodes them for a program compiled with routes.compileSauceProgram; see routes.

Closure entries and bound arguments

entryKind: "closure" reads the entry as one function expression, exactly as Function.prototype.toString prints it: () => { … }, () => expr or function () { … }. Its parameters and return annotation are main’s, its block is main’s body, and a concise body is main’s return (a concise body with no value makes main void). A function declared at the top of its block is a module function. Diagnostics are located in the closure text. An async or generator closure, text that is more than one function expression, and a named function expression that calls itself are refused. Imported modules are always modules. The compiler reads the text; it runs no JavaScript and captures no host variables. args compiles main for given argument values: each parameter becomes a const of its value, so a branch on it compiles only its taken arm, calls passing it are specialized, and nothing is decoded from the program’s input. Bound parameters disappear from entryParams and stay in entrySignature. Each value is one word: a non-negative bigint below 2^256, a boolean, or a decimal or 0x integer string. Pass one value per parameter (except an SVM Account, which stays an attached account). Bytes, arrays and records cannot be bound. For a negative Int256, pass its two’s-complement word: (1n << 256n) - 10n is -10.
bound.ts

Programs built in a program

Inside SauceScript, program(closure, ...args) compiles the closure as a separate program and returns { code, args }. Each argument binds one of the closure’s parameters; an optional final literal { chain, target } selects its profile and target. CompileResult.programs reports each one: id (unique in the compile), path (the ids of the programs it is nested in, outermost first), target, chain, bytecode, manifest, and captures, the names of the values it reads from the program around it, in the order their 32-byte words appear in args. The builtins reference covers program in SauceScript.
programs.ts
The bound argument 1000 folds into the inner program’s code; fee, a run-time value of the outer program, is captured as a word.

Resolving modules

ResolveModule

Returns the bytes at path (a Node.js Buffer is a Uint8Array). undefined or null means nothing is at that path, and the compiler falls back: after token.evm.ts misses, it asks for token.ts. A throw means the path names something that could not be read; it propagates and the compile fails, so a miss must return undefined rather than throw. Any other return value is a resolve error. Serve every .json path a program names: the compiler asks for one either as a contract ABI or as data imported with { type: "json" }.

ResolvePackage

Maps a bare specifier to a path resolve accepts. importer is the resolved path of the importing module. undefined or null declines, and the specifier is used as a path; a throw means the package is not installed. resolveSdkPackage and resolveSdkModule serve the SDK’s SauceScript libraries (@sauce/token, @sauce/router) and ABIs to a compile (see SDK module resolvers).

Errors

compile throws an Error that carries diagnostics. The compiler declares it as CompileError; @eco-incorp/sauce/compiler does not export that type, so declare the shape where you need it:
message is the rendered report: the rule broken, the source line and a caret under the span. diagnostics places each source error, in source order. code names the error ("UnknownIdentifier", "UnknownFunction", "TypeMismatch") and is stable across releases, so a host can tell refusals apart without matching message; it is absent for a parse or subset error. path is the module as resolve was asked for it, so an error in an imported module names that module. line and column are 1-based; offset and column count UTF-16 code units, so source.slice(start.offset, end.offset) is the span. The --> path:line:col header inside message counts characters instead, which differs on a line with a character outside the Basic Multilingual Plane. An error with no source location, such as an invalid option, a resolver failure or a builtin a chain profile refuses, has an empty diagnostics array. A message thrown by your resolve is preserved.
errors.ts

Chain profiles

ChainProfile is not exported from @eco-incorp/sauce/compiler; it is the value type of CompileOptions["chainProfiles"]. A target is a VM, and two EVM chains can run the same engine while a builtin behaves differently on them. A profile tells the compiler what a chain does with each target-gated builtin, keyed by its source spelling ("ctx.prevrandao", "evm.create2"):
  • "unsupported": a program that uses the builtin fails to compile for that chain, instead of reverting there.
  • { constant: "0" }: the chain answers with this value on every call, so it compiles to the literal and constant folding applies. Decimal or 0x hex, as a string. Only for a builtin that takes no arguments.
A builtin the profile does not name compiles as it does without a profile. A name that is not a configurable builtin, such as a typo or ctx.timestamp, is a compile error. name is used in the error message; without it the message uses the key.
profiles.ts

Solana account manifest

On target: "svm", CompileResult.manifest lists the accounts a caller attaches, by index.
A slot is an Account parameter of main: the engine reads it at index, name is the parameter’s name, and requires is the role the attached account must have. The roles are Solana’s four AccountRole values, so an SDK maps one onto an AccountMeta directly; a bare Account is "readonly". A role is the program author’s assertion, as an ABI declaration is: the compiler does not infer what a program called by CPI does with an account. remaining marks where the caller’s own variable-length accounts begin (from is the index of the first). It appears only when the program reads them (svm.remainingAccount or svm.remainingAccounts), and it is always last.

Function cache

With cache: true (the default), the compiler keeps two stores global to the WebAssembly instance. The lowering cache stores each declared function’s lowering, keyed on the function’s source text, the target, and every external binding it consulted: defines, imported constants and JSON tables, contract ABIs, callee signatures and its chain profile’s setting of each builtin it reads. Recompiling a program with different defines reuses every function a changed define does not affect. The module cache keeps each parsed module by its path and exact text, and each contract ABI by its name, path and exact JSON, bounded at 64 modules and 64 MiB of text, and 4,096 ABIs and 16 MiB of JSON. A program compiles to the same bytes either way. Each store empties itself when full. cache: false on a compile forces a fresh one.
clearFunctionCache() empties both stores and resets the lowering cache’s counters; functionCacheStats() reads them. Neither touches a cache your resolver keeps. @eco-incorp/sauce/compiler does not re-export them. They belong to the pinned compiler package, which @eco-incorp/sauce installs under the alias sauce-compiler. npm places that alias in your project’s node_modules, so the import below resolves there; a package manager that does not expose a dependency’s dependencies, such as pnpm, does not resolve it. It is the same pinned compiler and the same WebAssembly instance the SDK compiles with, not a separate install.
cache.ts

In a browser

The browser build cannot load its WebAssembly synchronously. Call await ready() once before the first compile; until it resolves, every compile in a browser throws. In Node.js it resolves at once and compile works without it. Call it as often as you like: it initializes once, concurrent calls share one attempt, and a failed attempt is retried by the next call. With no source, the browser build fetches the WebAssembly from beside the compiler’s own module, the URL a bundler rewrites to an asset. Pass the bytes (an ArrayBuffer or typed array) or a URL where that does not apply. The type accepts a URL through { href: string }. Use Node.js or a bundler that supports package exports subpaths. The same ready is exported from @eco-incorp/sauce.
ready.ts

Next steps