Skip to main content
SauceScript’s expressions and statements are TypeScript’s, restricted to those with one fixed meaning onchain. Everything on this page works on EVM chains and on Solana unless it says otherwise.

Operators

operators.sauce.ts
  • On a Uint256, / rounds down. a / 0 and a % 0 revert.
  • && and || evaluate the right operand only when needed, and produce true or false, never one of the operands. That holds for a contract name too: UniswapV3.Factory || 0 is true where the chain has the factory, not its address.
  • The two arms of ? : must have the same type. flag ? amount : true is refused with “the two arms of ? : must have the same type — the then arm is a number, the else arm is a bool”.
  • On an Int256, the operators are signed. See Signed arithmetic.
  • An array literal indexed at a literal position (or at a const bound to one) does not run every element. With no helper call, contract call or token read in it, only the element taken runs: [1 / zero, 7][1] returns 7 for zero = 0. With one, every element up to and including the last call or token read runs, in order, and a write inside a call happens: [1 / zero, pot.USDC][1] and [f(1 / zero), 7][1] revert. Elements after the last call are still skipped: [pot.USDC, 1 / zero][0] returns the Pot’s USDC. A builtin such as Math.mulDiv is not a call: [Math.mulDiv(1, 1, zero), 7][1] returns 7. Simulated on a local fork of Base at block 53166500 with the current contracts. Put a check or write that must run in its own statement (Arrays).

Precedence

Operators bind and associate as in TypeScript. Probed with constant expressions: 2 + 3 * 4 is 14, 1 << 2 + 1 is 8, 10 - 4 - 3 is 3, 2 ** 3 ** 2 is 512, and 1 | 2 ^ 3 & 6 is 1. As in TypeScript, -2 ** 2 is a syntax error (“Unexpected token”). Parenthesize it, in a function that returns an Int256:
negative-power.sauce.ts
In a function that returns a Uint256, negating a constant is refused with “negating this constant Uint256 value would produce a negative result; convert the operand with Int256(value) before negating it”. Parenthesize where the reading is not obvious.

Signed arithmetic

On Int256 values, / truncates toward zero, % takes the sign of the dividend, >> copies the sign bit, and the comparisons order negative values below positive ones:
signed-ops.sauce.ts
Simulated on a local fork of Base at block 52965000 with the current contracts: On Solana, the sauce command’s local copy (LiteSVM) of the cluster at slot 460264608 gives the first row’s values too.
  • / and % are checked as on Uint256, and one more division overflows: the minimum, −2²⁵⁵, divided by -1. Int256.fromBits(word) / divisor with word equal to 2²⁵⁵ and divisor = -1 reverts on the Base fork, and the engine refuses it with Overflow on Solana (slot 460264648). Divided by 2, the same value gives −2²⁵⁴.
  • Arithmetic takes two operands of the same kind: amount + delta, with amount a Uint256 and delta an Int256, is refused until one operand is converted with Int256(...) or Uint256(...).
  • A comparison takes both kinds and compares the numeric values: with b = -1, b < a is true for a = 5 and for a = 2²⁵⁶ − 1 on the same fork.
Types covers the conversions, Int256.fromBits and signed values at a contract boundary.

Assignment

= assigns. The compound forms +=, -=, *=, /=, %=, **=, &=, |=, ^=, <<= and >>= work on plain variables. x++, x--, ++x and --x work as statements only.
assignment.sauce.ts
These assignment forms are not supported: A const cannot be reassigned: k = 2 after const k = 1 is refused with “k is declared const and cannot be reassigned”. A variable keeps the type of its first value: x = true after let x = 1 is refused with “x cannot be reassigned a value of a different type — expected a number, found a bool”.

Conditionals

if, else if and else take a condition that is a bool or a number; a nonzero number is true.
tiers.sauce.ts
There is no switch: write an if/else if chain, as tiers.sauce.ts does.

Loops

break and continue work in each, without labels.
loops.sauce.ts
for...of walks an array only; on a bytes value it is refused with “for…of can only walk an array — this is a bytes value”. Index a bytes value instead:
bytes-sum.sauce.ts
A do/while loop, written with while (true):
do-while.sauce.ts

What a loop costs

On an EVM chain the engine interprets the program step by step, so each iteration costs gas. On a local fork of Base at block 52965000 with the current contracts, the for loop below cost 50475 gas for the cook with n = 0 and 141487 with n = 100: about 910 gas per iteration. The same loop that also writes xs[i] in each iteration costs about 480 gas more per iteration than an empty loop, and one that adds xs[i] to the total about 320 more than this one.
loop-gas.sauce.ts
Bound every loop by its input, and read a value into a local once rather than in each iteration. cook() covers the rest of a cook’s gas.

Destructuring

const [a, b] = … binds the fields of a record by position. A multi-output contract call, abi.decode and a record value each produce one. This example reads a Uniswap V3 pool, so it compiles for EVM chains only:
destructuring.sauce.ts
  • A hole skips a field: const [, tick] = UniswapV3.Pool(pool).slot0();. Taking fewer names than fields takes the leading ones.
  • let [x, y] = … gives variables that can be reassigned.
  • An array is indexed, not destructured: const [a, b] = xs is refused with “a destructuring binding needs an initializer that produces a tuple — this one does not, so bind it to one name instead”. Write xs[0] and xs[1].
  • Object destructuring, const { fee } = order, is not supported: write const fee = order.fee;.

Methods and positions

A method is called on a value, and the value may be a literal:
literal-methods.sauce.ts
On a local fork of Base at block 52965000 with the current contracts, amount = 7 returns 0x6162000507: "ab", then 5 in two bytes, then 7 in one. A record’s field is read by name, order.amount, on every record. A constant position, order[0], reads a record that comes from a main parameter, an array element, a contract call, abi.decode or a function’s result. An object literal the function builds is read by name or destructured; a position on it is refused. Types shows both cases and their messages.

Constant folding

The compiler evaluates an expression whose operands are all known when it compiles: literals, module constants, and the names the chain supplies. The result is written into the bytecode as one value.
folded.sauce.ts
This compiles to the same bytes as (amount * 9970) / 10000. Folding follows the type: 0 - 1 is refused as a Uint256 and is -1 as an Int256 (Types). A constant expression that overflows, underflows or divides by zero is a compile error rather than a revert: with const SHARES = 0, 100 / SHARES is refused with “constant / expression divides by zero”.

Testing a name

A registry name the chain lacks is zero there, and a test of it is decided when the program compiles. A branch the test rules out is removed, with the names it uses, so the program compiles for a chain that lacks the name:
presence-test.sauce.ts
This compiles for Base, Ethereum and Solana. Without the if, it fails on Solana, where the registry records no Uniswap V3 factory, with “UniswapV3.Factory is not deployed on solana. …”. A test is the name under if, !, ? :, &&, ||, == 0 or != 0; a decision held in a variable is not one. Names covers which uses count as tests. A condition built from other known values is removed in the same way; Functions shows it.

Not supported

Next steps

  • Functions: helpers, return paths and call cost.
  • Errors and reverts: require, throw, try/catch and .catch.
  • Types: what each operator accepts.
  • Names: token symbols, namespaces, and testing whether the chain has a name.