Skip to main content
SauceScript is the language for writing Sauce programs: a TypeScript subset for onchain runtime scripts that run within a single transaction. The compiler parses TypeScript, gates it to the supported subset, lowers it to one intermediate representation, and emits Sauce bytecode for the selected target, EVM or SVM. Anything outside the subset is a compile error rather than a silent difference. This page condenses the compiler’s language and builtins references. Check compiler diagnostics and the compiler API when adapting a program.

Program structure

The entry point is function main(). Alongside it, a module’s top level may contain function declarations (including export function), import declarations, let and const module-scope bindings, and interface declarations. Any other top-level statement, including var, expression statements and classes, is a compile error.
Scripts. A module that declares no main and has at least one top-level statement is a script: its top-level statements are the entry point’s body, its top-level let and const are locals, and it returns nothing.

Import kinds

The entry file’s directory is the module root. A library can ship per-target arms as sibling files, token.evm.ts and token.svm.ts, and import ... from "./token.ts" resolves the arm matching the compile target first.

Module-scope bindings

A module-scope const is a compile-time constant substituted at every use; its initializer may be any constant expression over the module’s own consts. A module-scope let is one 256-bit word of memory shared by every function in the module, re-initialized on every execution; it is not persistent state, so use evm.sstore or svm.sstore for that, and its initializer must be a literal. Neither may read a runtime value, a function call, or an imported name. A const whose initializer is an array or object of literals is a lookup table: TIERS[1] or FEES.high substitutes the element at compile time, the index must be a literal or a const bound to one, and an out-of-range index is always an error. A JSON data import is a lookup table too, and may nest to any depth. A JSON number is an IEEE double, so write a uint256 as a string ("1000000000000000000"); a string that is an integer written down reads as a number, otherwise as bytes. Ambient modules (ambient in the compiler options) broadcast a module’s exports into every module with no import line; a name the module already has wins silently. Defines (defines) inject compile-time constants into every module and, unlike ambient names, override the module’s own const of that name. Both are build inputs: keep them with the build script.

Functions

Functions are declared with function. Arrow functions and function expressions are not supported, except the zero-parameter arrow a contract call’s .catch() takes. Recursion is supported. Parameters must be plain identifiers, with no destructuring and no defaults, and may be any value type. A function that is not : void must return on every path; a function returning a non-scalar must annotate its return type unless it is main. main takes parameters: each is decoded at run time from the program input, as ABI-encoded words by default or in the compact format when compiled with compactArgs. On the SVM an Account parameter is routed to an attached account slot instead of a decoded word. An Account has .address on both targets, and on the SVM also .owner (the owning program) and .dataLen (the account’s data length), both read-only and unforgeable; check .owner first, then .dataLen, before reading a layout such as an SPL mint. Route bodies. The SDK’s Base(() => { ... }, options) form wraps a body in main for you and resolves token and protocol globals such as USDC.approve(Uniswap.UniversalRouter, amount) before compiling. Route bodies are parsed as JavaScript, so they take no type annotations and no main parameters; pass values through defines. Standalone programs compiled directly accept the full annotated language described here.

Variables and literals

  • let and const only; var is a compile error. Variables must be initialized at declaration. Object destructuring is not supported; array destructuring of a tuple result is (const [amount, gas] = q.quote(x);).
  • Assignment supports = and the compound operators +=, -=, *=, **=, /=, %=, &=, |=, ^=, <<=, >>=. ||=, &&=, ??= and >>>= are not supported. i++ and i-- are statements only, never values.
  • Numbers are 256-bit words, exact at every magnitude up to 2**256 - 1: 42, 0xff, 1000000000000000000, 1_000_000. An address is a hex literal. Fractions, exponents, binary, octal and leading-zero literals are compile errors. The n suffix is accepted and changes nothing.
  • Booleans: true is the word 1, false is 0.
  • Strings compile to their UTF-8 bytes. Hex byte literals use the hex tagged template, for example hex`a9059cbb` or hex “ for empty bytes, with no interpolation.

Operators

Control flow

if/else (with else if chains), while, C-style for, and for (const x of xs) over an array are supported, with unlabeled break and continue. With optimization on (the default), a condition known at compile time is folded away and the untaken arm is dropped, though it is still type-checked.

Contract interop

Register a contract by importing its ABI JSON artifact, a bare entry array or a build-tool object with an abi field. The compiler computes selectors, ABI-encodes arguments by position, issues the call, and decodes a single return value to its declared type. Struct arguments are object literals. Either name every field after a declared component, in which case order does not matter, or name none of them, in which case the keys are a mnemonic and values are read positionally. Naming some, or writing a different number of fields, is a compile error. A contract call that reverts normally aborts the program. Append .catch(() => { ... }) to an external call to handle the failure instead. Two forms exist, both EVM-only:
  • Captured result. For an ABI method returning one scalar, a handler containing exactly return CONSTANT; makes the call an expression: the successful result on success, the constant on failure. The constant must fit the result’s ABI width.
  • Statement handler. call.catch(() => { ok = 0n; }); discards the result and runs ordinary statements on failure. The handler may read and assign enclosing variables; break, continue and return cannot escape it.
The example uses IERC20.json beside the source. Only the external call’s failure is caught: malformed return data from a successful call still fails ABI decoding. Handlers are synchronous, take no parameters, and apply to ABI calls and raw evm.call, evm.static and evm.delegate only.

Type annotations

Annotations are optional but checked, and a recognized annotation is a constraint, not code. The types used across the compiler documentation include Uint256, Address, Account, bool, bigint, bytes, arrays such as Uint256[], tuples, void, and record types declared with a top-level interface. An annotation outside the compiler’s vocabulary fails the build; type aliases, enums, as and satisfies casts, arbitrary TypeScript generics and the non-null ! are rejected as out-of-subset.

Builtins

Ordinary arithmetic, comparison, boolean, bitwise and ternary operations use the native operators. Everything else is a builtin in one of six reserved namespaces, which may not be used as variable, parameter or import-alias names. ctx.msgSender() and ctx.self() are a 20-byte address on the EVM and a 32-byte pubkey on the SVM; balances are wei on the EVM and lamports on the SVM. ctx.msgData() returns the whole program input payload, [program | STOP | args], not Solidity’s msg.data.

Cross-target rules

Programs using only cross-target constructs can compile to either engine. Where a construct has the same meaning on both targets and only the encoding differs, the compiler reconciles it behind one builtin. Where a construct names a target-specific concept, it is exposed as two namespaced builtins (evm.sload and svm.sload) and the capability pass ensures each compiles only for its engine. The SVM engine omits CATCH, the CREATE* family, TLOAD/TSTORE, the ABI opcodes and DELEGATE/STATIC, so programs using them are EVM-only by construction.

Next steps