Skip to content

Values and types

Every runtime value in a script has an evs type — a Solidity-style type string like 'uint256', 'address', or 'uint256[]'. This page covers the type vocabulary, the Expr handles that represent runtime values, which plain JS literals you can pass where, and what happens when you accidentally treat a handle like a real value.

Kind Types Runtime representation
Word types uint8uint256 and int8int256 (multiples of 8), address, bool, bytes1bytes32 one canonical EVM word
Dynamic types string, bytes memref — a pointer to [length][payload]
Array types T[] where T is a word type, a dynamic type, a one-level array (T[][]), or a struct/tuple (tuple[]) memref — [length] then either one word per element (word element, inline) or one pointer per element (composite element)
Composite types structs and tuples (t.struct/t.tuple) memref — a pointer to a packed block, one word per member

The exported type aliases mirror this taxonomy: EvsType is the whole set; WordType, DynType, ArrayType, and TupleType are the kinds; and two cross-cutting unions matter for operations — NumericType (uintN and intN) and BitsType (uintN and bytesN). The full alias table is in the types reference.

A composite-element arraytuple[], a string array (string[]/bytes[]), or a one-level nested array (uint256[][]) — is an array of pointers: the [length] prefix is followed by one pointer word per element, each pointing at that element’s own block (a tuple block for tuple[], an inner array for T[][], a bytes block for string[]). This is exactly Solidity’s Struct[]/T[][]/string[] memory layout. You read and build it through the same array handle as a word array (.at(i), .length(), s.newArray), and it carries the same reference semantics — see composite arrays below.

Still rejected at recording (UNSUPPORTED_V0): two-level tuple arrays (tuple[][]), arrays nested deeper than [][], and fixed-size arrays T[N] (not modeled).

t is autocomplete sugar: every scalar property is its own name as a literal string (t.uint256 is exactly 'uint256'), t.array(elem) builds an array type, and t.struct/t.tuple build composite types. Raw type strings are accepted everywhere t.* is:

import { evscript, t } from '@maxencerb/evs';
const ex = evscript(
{ name: 'ex', args: [t.address, t.array(t.uint256), t.string] },
// owner amounts ('uint256[]') note (raw 'string' works too)
(s, owner, amounts, note) => s.return({ n: amounts.length() }),
);

An Expr<t> is a phantom-typed handle to a value that will exist at run time. It is not the value: you cannot read a number out of it, and you cannot construct one yourself. Handles are produced by the script’s positional args, s.lit, s.read, s.env, cell reads, and every operation — and consumed by other operations, sub-call parameters, and s.return.

The brand is nominal (a unique symbol), so nothing structurally fakes an Expr, and each handle carries a runtime-readable .type tag. Word-typed handles stand for single words; dynamic and array handles are memrefs — references to a memory buffer, with reference semantics where mutation is possible (see MutArray in control flow).

Dynamic and array handles expose two accessors:

import { evscript, t } from '@maxencerb/evs';
const inspect = evscript({ name: 'inspect', args: [t.array(t.uint128)] }, (s, xs) => {
// xs: Expr<'uint128[]'>
const n = xs.length(); // Expr<'uint256'>
const first = xs.at(0n); // Expr<'uint128'> — bounds-checked, Panic(0x32) if out of range
return s.return({ n, first });
});

The arithmetic, comparison, bitwise, and conversion methods on Expr are covered in arithmetic; the exhaustive method tables are in the types reference.

Solidity structs and tuples are first-class. Build a composite type with t.struct (named members — inferred as an object) or t.tuple (positional members — inferred as an array); both produce a TupleType descriptor that viem infers directly. A composite value is a memref to a packed block of one word per member (a static member inline, a dynamic/nested member as a pointer), and its handle is a Tuple — not an Expr — exposing a Field per member.

Three things you do with composites:

Decode a struct return. A 'tuple' output of s.read becomes a Tuple handle; read members by name with .field.get():

import { evscript, t } from '@maxencerb/evs';
const positionsAbi = [
{
type: 'function',
name: 'positions',
stateMutability: 'view',
inputs: [{ name: 'tokenId', type: 'uint256' }],
outputs: [
{
name: '',
type: 'tuple',
components: [
{ name: 'nonce', type: 'uint96' },
{ name: 'operator', type: 'address' },
{ name: 'liquidity', type: 'uint128' },
],
},
],
},
] as const;
const readPosition = evscript(
{ name: 'readPosition', args: [t.address, t.uint256] },
(s, manager, tokenId) => {
const pos = s.read({ address: manager, abi: positionsAbi, functionName: 'positions', args: [tokenId] });
// ^? Tuple<…> — the struct output, decoded
return s.return({
operator: pos.operator.get(), // Expr<'address'>, read by name
liquidity: pos.liquidity.get(), // Expr<'uint128'>
position: pos, // a Tuple handle is returnable directly — the whole struct flows out as an object
});
},
);

A Tuple handle is returnable directlys.return({ position: pos }) hands the whole struct back, abitype-typed. pos.expr() does the same thing and is still valid when you want the bare memref Expr (to pass on as a call argument, say).

Construct a tuple with s.tuple. Allocate a struct/tuple in memory and fill members — by an init record, or by Field.set afterwards:

import { evscript, t } from '@maxencerb/evs';
const Position = t.struct({ liquidity: t.uint128, owner: t.address });
const buildPosition = evscript({ name: 'buildPosition', args: [t.address] }, (s, owner) => {
const pos = s.tuple(Position, { liquidity: 1000n, owner }); // alloc + zero-fill + set members
pos.liquidity.set(pos.liquidity.get().add(7n)); // read-modify-write a field
return s.return({ position: pos }); // a Tuple is returnable directly → returns { liquidity, owner }
});

Pass a struct to a call. A 'tuple' argument accepts a Tuple handle, a s.tuple(...) result, or a plain literal object:

import { evscript } from '@maxencerb/evs';
// A raw `as const` descriptor matches the ABI's component order exactly — `s.tuple` and a raw
// `readonly AbiParameter[]` are accepted anywhere a tuple type is expected.
const QuoteParams = {
type: 'tuple',
components: [
{ name: 'tokenIn', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'amountIn', type: 'uint256' },
],
} as const;
const quoteAbi = [
{
type: 'function',
name: 'quote',
stateMutability: 'view',
inputs: [{ name: 'p', ...QuoteParams }],
outputs: [{ name: 'amountOut', type: 'uint256' }],
},
] as const;
const getQuote = evscript(
{ name: 'getQuote', args: ['address', 'address', 'uint24', 'uint256'] },
(s, quoter, tokenIn, fee, amountIn) => {
const params = s.tuple(QuoteParams, { tokenIn, fee, amountIn });
const amountOut = s.read({ address: quoter, abi: quoteAbi, functionName: 'quote', args: [params] });
return s.return({ amountOut });
},
);

A TupleType arg works the same way: declare it on the header (args: [QuoteParams]) and the body receives a Tuple handle instead of an Expr. Reference semantics apply throughout — a Tuple handle is a pointer, so passing it (or aliasing it) shares the same block and a later .set() is visible everywhere. The full Tuple/Field/s.tuple surface is in the builder reference.

When a type already exists in an ABI, you do not have to retype it. t.fromOutputs(abi, name) derives an evs type straight from a function’s ABI outputs, and t.fromAbiParameter(param) does the same for a single ABI parameter:

  • t.fromOutputs(abi, name) — a function with one output returns that output’s type (a TupleType for a tuple output, else the scalar or array type string); a function with several outputs returns a TupleType struct over them, with components in ABI declaration order.
  • t.fromAbiParameter(param) — one ABI parameter → its evs type (a tuple… param becomes a TupleType, anything else its type string).
import {
function evscript<const name extends string, const args extends ArgsInput = readonly [], ret extends Record<string, ReturnValue> = Record<string, ReturnValue>>(def: {
name: name;
args?: args;
}, body: (s: ScriptBuilder, ...args: ArgHandles<NormalizeArgs<args>>) => ScriptReturn<ret>, opts?: {
locations?: boolean;
}): EvsScript<name, NormalizeArgs<args>, ret>
evscript
,
const t: TypeNamespace
t
} from '@maxencerb/evs';
const
const poolAbi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}]
poolAbi
= [
{
type: "function"
type
: 'function',
name: "slot0"
name
: 'slot0',
stateMutability: "view"
stateMutability
: 'view',
inputs: readonly []
inputs
: [],
outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}]
outputs
: [
{
name: "sqrtPriceX96"
name
: 'sqrtPriceX96',
type: "uint160"
type
: 'uint160' },
{
name: "tick"
name
: 'tick',
type: "int24"
type
: 'int24' },
{
name: "unlocked"
name
: 'unlocked',
type: "bool"
type
: 'bool' },
],
},
{
type: "function"
type
: 'function',
name: "fee"
name
: 'fee',
stateMutability: "view"
stateMutability
: 'view',
inputs: readonly []
inputs
: [],
outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}]
outputs
: [{
name: ""
name
: '',
type: "uint24"
type
: 'uint24' }],
},
] as
type const = readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}]
const
;
// Several named outputs → a named struct type, in ABI declaration order.
const Slot0 =
const t: TypeNamespace
t
.
fromOutputs<readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}], "slot0">(abi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}], name: "slot0"): {
...;
}
fromOutputs
(
const poolAbi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}]
poolAbi
, 'slot0');
const Slot0: {
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}
// A single output → that output's type directly.
const
const Fee: "uint24"
Fee
=
const t: TypeNamespace
t
.
fromOutputs<readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}], "fee">(abi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}], name: "fee"): "uint24"
fromOutputs
(
const poolAbi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}]
poolAbi
, 'fee'); // 'uint24'
// One ABI parameter → its evs type.
const
const Tick: "int24"
Tick
=
const t: TypeNamespace
t
.
fromAbiParameter<{
readonly name: "tick";
readonly type: "int24";
}>(param: {
readonly name: "tick";
readonly type: "int24";
}): "int24"
fromAbiParameter
({
name: "tick"
name
: 'tick',
type: "int24"
type
: 'int24' }); // 'int24'
const
const readPool: EvsScript<"readPool", readonly [ArgSpec<"", "address">], {
readonly tick: Expr<"int24">;
readonly slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>;
}>
readPool
=
evscript<"readPool", readonly ["address"], {
readonly tick: Expr<"int24">;
readonly slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>;
}>(def: {
name: "readPool";
args?: readonly ["address"] | undefined;
}, body: (s: ScriptBuilder, arg0: Expr<"address">) => ScriptReturn<{
readonly tick: Expr<"int24">;
readonly slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>;
}>, opts?: {
locations?: boolean;
}): EvsScript<...>
evscript
({
name: "readPool"
name
: 'readPool',
args?: readonly ["address"] | undefined
args
: [
const t: TypeNamespace
t
.
address: "address"
address
] }, (
s: ScriptBuilder
s
,
pool: Expr<"address">
pool
) => {
// `struct: true` decodes the multi-output slot0() into one Slot0-shaped Tuple, so the derived
// type round-trips with the decode (same fields, same ABI order). See calls — struct: true.
const
const slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>
slot0
=
s: ScriptBuilder
s
.
ScriptBuilder.read: SubcallVerb
<readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}], "slot0">(p: SubcallParams<readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}], "slot0", ViewMutability> & {
...;
}) => Tuple<...> (+2 overloads)
read
({
SubcallParams<readonly [{ readonly type: "function"; readonly name: "slot0"; readonly stateMutability: "view"; readonly inputs: readonly []; readonly outputs: readonly [{ readonly name: "sqrtPriceX96"; readonly type: "uint160"; }, { ...; }, { ...; }]; }, { ...; }], "slot0", ViewMutability>.address: IntoExpr<"address">
address
:
pool: Expr<"address">
pool
,
abi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}]
abi
:
const poolAbi: readonly [{
readonly type: "function";
readonly name: "slot0";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}, {
readonly type: "function";
readonly name: "fee";
readonly stateMutability: "view";
readonly inputs: readonly [];
readonly outputs: readonly [{
readonly name: "";
readonly type: "uint24";
}];
}]
poolAbi
,
SubcallParams<readonly [{ readonly type: "function"; readonly name: "slot0"; readonly stateMutability: "view"; readonly inputs: readonly []; readonly outputs: readonly [{ readonly name: "sqrtPriceX96"; readonly type: "uint160"; }, { ...; }, { ...; }]; }, { ...; }], "slot0", ViewMutability>.functionName: "slot0" | "fee"
functionName
: 'slot0',
struct: true
struct
: true });
return
s: ScriptBuilder
s
.
ScriptBuilder.return<{
readonly tick: Expr<"int24">;
readonly slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>;
}>(values: {
readonly tick: Expr<"int24">;
readonly slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>;
}): ScriptReturn<...>
return
({
tick: Expr<"int24">
tick
:
const slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>
slot0
.
tick: Field<"int24">
tick
.
Field<"int24">.get(): Expr<"int24">
get
(), // Expr<'int24'>, read by name
slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>
slot0
, // the whole struct flows out, typed as Slot0
});
});
void [
const Slot0: {
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}
Slot0
,
const Fee: "uint24"
Fee
,
const Tick: "int24"
Tick
,
const readPool: EvsScript<"readPool", readonly [ArgSpec<"", "address">], {
readonly tick: Expr<"int24">;
readonly slot0: Tuple<{
readonly type: "tuple";
readonly components: readonly [{
readonly name: "sqrtPriceX96";
readonly type: "uint160";
}, {
readonly name: "tick";
readonly type: "int24";
}, {
readonly name: "unlocked";
readonly type: "bool";
}];
}>;
}>
readPool
];

A derived type flows everywhere a hand-written t.struct/t.tuple does — as a script arg, a s.read argument or output, an s.newArray element, or an s.return value. Because the components come from the ABI in declaration order, a t.fromOutputs(abi, name) type is interchangeable with a t.struct written in the same order and with a s.read({ …, struct: true }) decode of that same function. An overloaded or unknown function name throws EvsTypeError at the call site (a non-as const ABI degrades to EvsType rather than erroring, mirroring s.read). The exact return types (FromAbiOutputs, AbiParamToEvsType) are in the types reference.

Composite arrays: tuple[], T[][], string[]

Section titled “Composite arrays: tuple[], T[][], string[]”

Arrays whose element is itself composite — tuple[] (arrays of structs), one-level nested arrays (uint256[][]), and dynamic-leaf arrays (string[], bytes[]) — are first-class. They share the word-array handle surface (.length(), .at(i), s.newArray); the only difference is in memory each slot holds a pointer to the element’s own block instead of an inline value (an array-of-pointers, exactly Solidity Struct[]/T[][]/string[] memory). Reference semantics carry over: an array handle is a pointer, so the element handles you read from it alias the array’s slots.

Read a tuple[] output. A 'tuple[]' output of s.read is an array handle; .at(i) returns the i-th element as a typed Tuple — read its fields by name:

import { evscript, t } from '@maxencerb/evs';
const registryAbi = [
{
type: 'function',
name: 'allPositions',
stateMutability: 'view',
inputs: [],
outputs: [
{
name: '',
type: 'tuple[]',
components: [
{ name: 'liquidity', type: 'uint128' },
{ name: 'owner', type: 'address' },
],
},
],
},
] as const;
const readPositions = evscript({ name: 'readPositions', args: [t.address] }, (s, registry) => {
const positions = s.read({ address: registry, abi: registryAbi, functionName: 'allPositions' });
// ^? Expr<'tuple[]'> — the array handle
const first = positions.at(0n); // Tuple element — bounds-checked, Panic(0x32) if out of range
return s.return({
firstOwner: first.owner.get(), // Expr<'address'>, by name
positions, // the whole tuple[] flows out, abitype-typed → readonly { liquidity; owner }[]
});
});

A tuple[] output unwraps to readonly Struct[] at the viem call site; uint256[][]readonly (readonly bigint[])[]; string[]readonly string[].

Construct a tuple[] with s.newArray. s.newArray admits composite elements (a struct/tuple, a one-level T[], string/bytes). arr.get(i) returns the element handle (a Tuple for a struct element) — a reference into the array’s slot, so filling its fields writes through:

import { evscript, t } from '@maxencerb/evs';
const Position = t.struct({ liquidity: t.uint128, owner: t.address });
const buildPositions = evscript({ name: 'buildPositions', args: [t.address, t.uint256] }, (s, owner, n) => {
const arr = s.newArray(Position, n); // zero-filled tuple[n] — an array of fresh element blocks
s.for({ type: t.uint256, from: 0n, until: n }, (i) => {
const el = arr.get(i); // Tuple handle into slot i
el.liquidity.set(i.toUint(t.uint128));
el.owner.set(owner);
});
return s.return({ positions: arr.expr() }); // returnable as readonly { liquidity; owner }[]
});

A composite-array literal (a plain JS array of struct objects, of inner arrays, or of strings) is accepted wherever a value type is coerced — a call argument, an s.return value, or a tuple/array member slot (s.tuple / Field.set / MutArray.set) — and is built at record time into a fresh [length][p0…] block with reference semantics (not a flat data segment). The string-typed shapes (uint256[][], string[]) can also be materialized directly with s.lit, because their type is a plain string; a tuple[] literal cannot — s.lit only accepts string-encoded types and rejects the t.array(t.struct(...)) object with EvsTypeError (TYPE_MISMATCH), so build a tuple[] with s.newArray (above) or pass it through a coerced position (a call arg, below):

import { evscript, t } from '@maxencerb/evs';
const literals = evscript({ name: 'literals', args: [] }, (s) => {
const grid = s.lit(t.array(t.array(t.uint256)), [[1n, 2n], [3n]]); // uint256[][] — a string type
const tags = s.lit(t.array(t.string), ['alpha', 'beta']); // string[] — a string type
return s.return({ grid, tags });
});

Pass a tuple[] to a call. A 'tuple[]' argument accepts an array handle (arr.expr()) or a plain readonly Struct[] literal:

import { evscript, t } from '@maxencerb/evs';
const sinkAbi = [
{
type: 'function',
name: 'totalLiquidity',
stateMutability: 'view',
inputs: [
{
name: 'ps',
type: 'tuple[]',
components: [
{ name: 'liquidity', type: 'uint128' },
{ name: 'owner', type: 'address' },
],
},
],
outputs: [{ name: '', type: 'uint256' }],
},
] as const;
const sumLiquidity = evscript({ name: 'sumLiquidity', args: [t.address] }, (s, sink) => {
const total = s.read({
address: sink,
abi: sinkAbi,
functionName: 'totalLiquidity',
args: [[{ liquidity: 1000n, owner: '0x0000000000000000000000000000000000000001' }]],
});
return s.return({ total });
});

Still deferred (UNSUPPORTED_V0 at recording): two-level tuple arrays (tuple[][]), arrays nested deeper than [][], and fixed-size T[N].

Most operand positions are typed IntoExpr<t> — either an Expr<t> or a plain JS literal of the matching shape (LitOf<t>). Literals are validated at recording time, with the call site’s location attached to any EvsTypeError:

evs type Accepted JS literal Validation rule
uintN / intN bigint or number numbers must be safe integers; range-checked against N; negative bigints two’s-complemented for intN
bool boolean
address 0x string exactly 20 bytes; checksum not enforced (viem-permissive)
bytesN 0x string exactly N bytes
bytes 0x string any even-length hex
string string UTF-8 encoded
T[] readonly array of T literals element-wise rules of T
t.struct/t.tuple object (named) or array (positional) of member literals member-wise rules; omitted members default to zero

Word literals canonicalize at recording. Dynamic literals and word-array literals become bytecode data segments, materialized by CODECOPY the first time the script uses them. A composite-array literal (a tuple[]/T[][]/string[] JS array) is instead built at record time into a fresh array-of-pointers block (see composite arrays), so it has reference semantics like a constructed array. A T[][]/string[] literal can also be materialized directly with s.lit (its type is a plain string), but a tuple[] literal is built only when it flows through a coerced value position (a call arg, an s.return slot, or a member) — s.lit requires a string type and rejects the t.array(t.struct(...)) object.

import { evscript, t } from '@maxencerb/evs';
const coerce = evscript({ name: 'coerce', args: [t.uint256] }, (s, x) => {
const a = x.add(1n); // bigint → uint256 literal
const b = x.add(250); // number → uint256 (safe integer, range-checked)
const fees = s.lit(t.array(t.uint24), [100n, 500n, 3000n]); // array literal → data segment
const dai = s.lit(t.address, '0x6B175474E89094C44Da98b954EedeAC495271d0F');
const greeting = s.lit(t.string, 'hello');
return s.return({ sum: a.add(b), n: fees.length(), dai, greeting });
});

Two rules round this out:

  • s.lit(type, value) is the explicit constructor for when inference has no expected type to coerce against — a standalone constant, or the first operand of a free function.
  • Free functions need at least one Expr operand. s.add(1n, 2n) has no type to infer the operation from, so it throws EvsTypeError at recording with the fix in the message: type one operand with s.lit.

Out-of-range literals are recording-time errors, not runtime reverts: s.lit(t.uint8, 300) throws EvsTypeError immediately.

Staging misuse: treating a handle like a value

Section titled “Staging misuse: treating a handle like a value”

Because handles are plain objects at run time of your program, JS will happily let you pass them places they make no sense. evs traps the common cases: every handle installs throwing valueOf, toString, toJSON, and Symbol.toPrimitive, so arithmetic, template strings, loose equality, and JSON.stringify all throw EvsStagingError at the offending line — citing both the misuse site and where the handle was recorded.

The following does not compile (and the parts TS cannot reject throw at recording time):

import { evscript, t } from '@maxencerb/evs';
const bad = evscript({ name: 'bad', args: [t.uint256] }, (s, x) => {
const doubled = x * 2; // TS error — and EvsStagingError if it ever ran
const text = `x is ${x}`; // TS allows this — EvsStagingError at recording
return s.return({ x });
});

console.log(handle) is fine — printing is debugging, not misuse, so handles render a non-throwing description instead.

if (x) never calls a coercion hook, so it cannot throw. This script compiles and records without any error — and it is wrong:

import { evscript, t } from '@maxencerb/evs';
const wrong = evscript({ name: 'wrong', args: [t.bool] }, (s, flag) => {
let fee = 30n;
if (flag) {
// ALWAYS taken at recording time: an Expr handle is an object, and objects are truthy
fee = 5n;
}
return s.return({ fee: s.lit(t.uint256, fee) });
});

The recorded script always returns 5n, regardless of flag.

  • Arithmetic — the checked operation surface on Expr.
  • Types reference — every alias, method, and coercion rule in table form.