Skip to content

Types and Expr

This page is the reference for the type vocabulary and the Expr value handle. For the narrative version (and the staging-misuse failure modes), see values and types.

import type { ArgsInput, EvsErrorType, EvsType, StringType, TupleType } from '@maxencerb/evs';
import type { Abi, AbiParameter } from 'viem';
declare const t: { readonly [k in Exclude<StringType, `${string}[]`>]: k } & {
array(elem: StringType | TupleType): StringType | TupleType;
struct(spec: Record<string, EvsType>): TupleType;
tuple(...items: readonly EvsType[]): TupleType;
error(name: string, params?: ArgsInput): EvsErrorType; // declare a custom error
fromOutputs(abi: Abi, name: string): EvsType; // derive a type from a function's outputs
fromAbiParameter(param: AbiParameter): EvsType; // derive a type from one ABI parameter
};

t is a frozen object whose every scalar property is its own literal type string — pure autocomplete sugar — plus six functions: three build composite types, t.error declares a custom error, and two derive a type from an ABI. Raw type strings (and raw readonly AbiParameter[] arrays, anywhere a tuple type is expected) are accepted everywhere t.* is.

Members Value Notes
t.address 'address' 160-bit word
t.bool 'bool' canonical 0 or 1
t.uint8 through t.uint256 'uint8' … 'uint256' one key per width in UintBits (every multiple of 8), 32 keys
t.int8 through t.int256 'int8' … 'int256' same 32 widths, signed (two’s complement)
t.bytes1 through t.bytes32 'bytes1' … 'bytes32' left-aligned fixed bytes, 32 keys
t.string 'string' dynamic, UTF-8
t.bytes 'bytes' dynamic byte string
t.array(elem, n?) `${elem}[]` / `${elem}[n]` / TupleType function: a dynamic array (t.array(t.uint256) → 'uint256[]') or, with a literal n ≥ 1, a fixed-size array (t.array(t.uint256, 2) → 'uint256[2]') of any element — a scalar, an array ('uint256[][]', 'string[][]', 'uint256[2][]'), or a tuple (tuple[], tuple[2], tuple[][]). Nesting deeper than four levels, or a static type of 2^32 bytes or more (t.array(t.uint256, 2 ** 27)), throws UNSUPPORTED_V0; an invalid n throws TYPE_MISMATCH
t.struct(spec) TupleType (named components) function: a struct — Object.keys(spec) order is the encode order; a field name must be an identifier other than __proto__ (TYPE_MISMATCH for a computed key or a literal key whose value is a type; a literal __proto__: key is always a type error, the only check for a primitive value, which JavaScript drops), and a name the Tuple handle already uses (at, expr, toString, …) is read by position
t.tuple(...items) TupleType (positional) function: an unnamed positional tuple
t.error(name, params?) EvsErrorType function: declare a custom error for a script’s errors: [...] and s.throw; params takes the same shorthand as script args (bare types, namedArg, or a mixed list). Invalid, reserved (Error, Panic, EvsDecodeError, EvsInvalidCalldata, empty, unknown, _) or duplicate names throw ERROR_DECL. See custom errors
t.fromOutputs(abi, name) EvsType (a TupleType for many/tuple outputs) function: derive a type from an as const ABI function’s outputs — one output → that output’s type, several → a named-component struct in ABI order (FromAbiOutputs); name may be an overload’s canonical signature ('get(uint256)'); an overloaded bare name or unknown name throws (a non-const ABI degrades to EvsType)
t.fromAbiParameter(param) EvsType function: derive a type from one ABI parameter (AbiParamToEvsType) — a tuple… param → a TupleType, else the type string

t.array validates its element at the call site. t.struct/t.tuple build a TupleType descriptor — an abitype AbiParameter-shaped object viem infers directly.

A hand-written tuple descriptor (or a raw AbiParameter[]) given to t.array, t.struct, t.tuple, t.fromOutputs or t.fromAbiParameter is checked member by member, at every nesting level: a tuple needs at least one member, every member type must be in the vocabulary, a tuple… member needs a well-formed tag (tuple, tuple[], tuple[2], …) and its own components, and no two named members of one tuple may share a name (viem decodes a named tuple into an object keyed by member name, so one value would be lost; unnamed members are positional and never clash). A violation throws TYPE_MISMATCH, naming the member by its path (t.struct() field "pos" component #1: …); arrays nested more than four levels deep, or a static size of 2^32 bytes or more, throw UNSUPPORTED_V0. The result is a frozen canonical copy: a member without a name gets name: '', and components on a non-tuple member is dropped. namedArg, evscript args, s.fn and t.error params and s.tuple apply the same limits, but take a descriptor only in its canonical form (every member carries a string name, '' when unnamed), the form every t.* constructor returns. s.lit, s.let, s.newArray, revertReturns and the params of a hand-built error in a def’s errors apply them too, so an empty tuple at any level throws while the script records, not at compile (ERROR_DECL for an error param). A descriptor given straight to s.lit, s.let, s.newArray or revertReturns is also held to the same distinct-names rule.

import { t } from '@maxencerb/evs';
const fees = t.array(t.uint24); // 'uint24[]'
const Position = t.struct({ liquidity: t.uint128, owner: t.address }); // named struct → object
const Pair = t.tuple(t.address, t.uint256); // positional tuple → [address, bigint]
const amount = 'uint128'; // raw strings work identically
void [fees, Position, Pair, amount];

The full exported vocabulary, exactly as defined:

type Hex = `0x${string}`;
type UintBits =
| 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128
| 136 | 144 | 152 | 160 | 168 | 176 | 184 | 192 | 200 | 208 | 216 | 224 | 232 | 240
| 248 | 256;
type BytesSize =
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16
| 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32;
type UintType = `uint${UintBits}`;
type IntType = `int${UintBits}`;
type BytesNType = `bytes${BytesSize}`;
type WordType = UintType | IntType | 'address' | 'bool' | BytesNType;
type DynType = 'string' | 'bytes';
type ScalarType = WordType | DynType;
type ArrayType =
| `${ScalarType}[]`
| `${ScalarType}[][]`
| `${ScalarType}[][][]`
| `${string}[${string}`; // catch-all: fixed sizes (`uint256[2]`) and four levels
type StringType = ScalarType | ArrayType; // every string-encoded type
interface NamedType {
readonly name: string; // '' for a positional tuple member
readonly type: string; // 'uint256', 'tuple', 'tuple[]', 'uint256[]', …
readonly components?: readonly NamedType[]; // present iff type starts with 'tuple'
}
interface TupleType {
readonly type: 'tuple' | `tuple[${string}`; // 'tuple', 'tuple[]', 'tuple[2]', 'tuple[][]', …
readonly components: readonly NamedType[];
}
type EvsType = WordType | DynType | ArrayType | TupleType;
type ArgType = EvsType; // script arg types — word, dynamic, string array, or a TupleType
type NumericType = UintType | IntType;
type BitsType = UintType | BytesNType;
type OrderedType = NumericType | 'address' | BytesNType;
// the same-width bytesN ↔ uintN twins (`asUint()` / `asBytesN()`), through two lookup tables
interface BytesSizeToBits {
1: 8; 2: 16; 3: 24; 4: 32; 5: 40; 6: 48; 7: 56; 8: 64; 9: 72; 10: 80; 11: 88; 12: 96;
13: 104; 14: 112; 15: 120; 16: 128; 17: 136; 18: 144; 19: 152; 20: 160; 21: 168; 22: 176;
23: 184; 24: 192; 25: 200; 26: 208; 27: 216; 28: 224; 29: 232; 30: 240; 31: 248; 32: 256;
}
interface BitsToBytesSize {
8: 1; 16: 2; 24: 3; 32: 4; 40: 5; 48: 6; 56: 7; 64: 8; 72: 9; 80: 10; 88: 11; 96: 12;
104: 13; 112: 14; 120: 15; 128: 16; 136: 17; 144: 18; 152: 19; 160: 20; 168: 21; 176: 22;
184: 23; 192: 24; 200: 25; 208: 26; 216: 27; 224: 28; 232: 29; 240: 30; 248: 31; 256: 32;
}
type UintOfBytesN<t extends EvsType> =
t extends `bytes${infer n extends keyof BytesSizeToBits}` ? `uint${BytesSizeToBits[n]}` : never;
type BytesNOfUint<t extends EvsType> =
t extends `uint${infer n extends keyof BitsToBytesSize}` ? `bytes${BitsToBytesSize[n]}` : never;
// the same-width intN ↔ uintN twins (`asUint()` on an intN / `asInt()`)
type UintOfInt<t extends EvsType> = t extends `int${infer n extends UintBits}` ? `uint${n}` : never;
type IntOfUint<t extends EvsType> = t extends `uint${infer n extends UintBits}` ? `int${n}` : never;
Alias Meaning
Hex A 0x-prefixed hex string.
Address The address string type: abitype’s, re-exported through viem, so it is viem’s Address (an abitype Register augmentation of addressType applies to both).
UintBits The 32 legal integer widths: every multiple of 8 from 8 to 256.
BytesSize The legal bytesN sizes: 1 through 32.
UintType uint8 … uint256 as a template-literal union.
IntType int8 … int256.
BytesNType bytes1 … bytes32.
WordType Every type that fits one EVM word: unsigned and signed integers, address, bool, bytesN.
DynType The dynamic byte-string types: string and bytes.
ScalarType WordType or DynType — every non-array, non-tuple type.
ArrayType String-encoded arrays of a scalar element, dynamic or fixed-size — uint256[], address[3], string[][], uint256[2][]. The dynamic forms up to three levels are exact literals; every other chain (a fixed size, four levels) is admitted by one catch-all pattern, so the leaf of such a string is checked at recording rather than statically. Recording rejects more than four levels (UNSUPPORTED_V0).
StringType Every string-encoded type: a scalar or a string array.
NamedType One tuple/struct member — an abitype AbiParameter-shaped descriptor (recursive).
TupleType A struct/tuple type — { type, components }. Built by t.struct/t.tuple; see Composite types.
EvsType Every value type: word, dynamic, string array, or a TupleType.
ArgType The script-argument value type — an alias for EvsType (word, dynamic, string array, or TupleType); types ScriptIr.args and ArgSpec.type. Named args and s.fn params accept the same types (see namedArg()).
EvsErrorType A declared custom error, built by t.error: { kind: 'error', name, params, abi } (params normalized to ArgSpecs).
EvsErrorAbiEntry The literal { type: 'error', name, inputs } ABI entry an EvsErrorType carries (its abi field), appended to the script ABI.
NumericType The arithmetic domain: uintN or intN.
BitsType The bitwise/shift domain: uintN or bytesN.
OrderedType The ordering domain of lt/gt/lte/gte: uintN, intN, address or bytesN.
UintOfBytesN<t> / BytesNOfUint<t> The same-width twin of a bytesN / uintN ('bytes4' ↔ 'uint32'): the result types of asUint() / asBytesN().
UintOfInt<t> / IntOfUint<t> The same-width twin of an intN / uintN ('int24' ↔ 'uint24'): the result types of asUint() on an intN / asInt().
LitOf<t> The host literal type accepted where a t is expected (table below).
IntoExpr<t> Expr of t, or a LitOf<t> literal.
Expr<t> The branded staged-value handle (below).

A TupleType is a struct or tuple type — an abitype AbiParameter-shaped descriptor that viem infers directly. Build one with t.struct (named members, inferred as an object) or t.tuple (positional members, inferred as an array); t.array lifts a tuple to a tuple[]. A raw readonly AbiParameter[] is accepted anywhere a TupleType is expected:

import { t } from '@maxencerb/evs';
import type { TupleType } from '@maxencerb/evs';
// t.struct → named components; runtime encode order is Object.keys insertion order.
const Position = t.struct({ liquidity: t.uint128, owner: t.address });
// ^? { type: 'tuple'; components: readonly [...] } (LitOf → { liquidity: bigint; owner: `0x${string}` })
// t.tuple → positional, unnamed components (LitOf → [bigint, `0x${string}`]).
const Pair = t.tuple(t.uint128, t.address);
// t.array of a tuple → a tuple[].
const positions = t.array(Position);
// nested struct: a member can itself be a TupleType.
const Outer = t.struct({ pos: Position, x: t.uint256 });
const types: readonly TupleType[] = [Position, Pair, positions, Outer];
void types;

LitOf<TupleType> delegates to abitype: an all-named tuple becomes an object, a positional tuple an array, recursing through nested components. TupleType flows everywhere a value type does — as a script arg (decoded into a Tuple handle), as a s.read argument or output, and as an s.return value. The handle surface — Tuple, Field, s.tuple — lives in the builder reference.

t.array(Position) lifts a struct to a tuple[] (a TupleType with type: 'tuple[]'); t.array(Position, 2) to a tuple[2] and t.array(t.array(Position)) to a tuple[][]. A composite-element array — tuple[], a nested array (uint256[][], tuple[][], string[][]), or a dynamic-leaf array (string[]/bytes[]) — is an array of pointers in memory: the [length] prefix is followed by one pointer word per element, each pointing at the element’s own block (a tuple block, an inner array, or a bytes block). It shares the word-array handle surface — .length(), .at(i), s.newArray — and the same reference semantics; an s.read tuple[] output is an array handle whose .at(i) yields a typed Tuple element. The build/read/pass examples live in composite arrays. A fixed-size array T[N] has the same memory layout as a T[] (its length word is always N); only its ABI encoding differs (no length word, inlined into the head when T is static). ArrayElemOf<t> peels the outermost suffix ('uint256[][2]' → 'uint256[]'), FixedLengthOf<t> reads its fixed length ('uint256[3]' → 3, null for []), and PeelArraySuffix<s> is the shared parser (it works on tuple tags too). Arrays nest at most four levels; a deeper type is rejected at recording with UNSUPPORTED_V0 (#4).

Several type-level helpers are exported for advanced inference: StructTypeOf, TupleTypeOf, TupleArrayOf, TypeToComponent, TupleLitOf, and TupleAsParam. You rarely name them — they power t.struct/t.tuple inference — but they are public for building your own type-level glue.

t.fromOutputs(abi, name) and t.fromAbiParameter(param) build a t.* type from existing ABI material instead of a hand-written t.struct/t.tuple — see deriving types from an ABI for the narrative and a runnable example. Their return types are exported:

  • FromAbiOutputs<abi, name> — the type of t.fromOutputs(abi, name): a single output’s type, or a TupleType struct over several outputs (named, in ABI declaration order). An unknown name or a non-const ABI widens to EvsType; an overloaded function is named by its canonical signature ('get(uint256)'), and an overloaded bare name throws at recording.
  • AbiParamToEvsType<p> — the type of t.fromAbiParameter(p): a TupleType for a tuple… parameter, else the scalar/array type string.

The component-level mirrors AbiParamToComponent and AbiParamsToComponents are exported too, for building your own ABI→type glue.

An ABI parameter can also be used directly as a declarator, wherever evscript args, s.fn params and t.error params are declared. All three sites follow the same rules, at run time and in the types. A scalar parameter ({ name: 'amount', type: 'uint256' }) has the namedArg shape. A struct parameter ({ name: 'p', type: 'tuple', components }, or a struct array typed 'tuple[]' / 'tuple[N]') is a TupleType that carries its own name. In both cases a non-empty name labels the arg, and name: '' keeps the positional arg{i} name. A function’s inputs therefore work as a script’s args:

import { evscript } from '@maxencerb/evs';
const quoterAbi = [
{
type: 'function',
name: 'quote',
stateMutability: 'view',
inputs: [
{
name: 'params',
type: 'tuple',
components: [
{ name: 'tokenIn', type: 'address' },
{ name: 'amountIn', type: 'uint256' },
],
},
{ name: 'fee', type: 'uint24' },
],
outputs: [{ name: '', type: 'uint256' }],
},
] as const;
// args labeled `params` / `fee`; `params` arrives as a Tuple handle
const echo = evscript({ name: 'echo', args: quoterAbi[0].inputs }, (s, params, fee) =>
s.return({ amountIn: params.amountIn.get(), fee }),
);
void echo;

Each tuple member needs a name (it may be ''). A parameter with a nameless member is rejected with TYPE_MISMATCH, and the message names the first such member (components[0] has no string `name` ). t.fromAbiParameter(param) fills the member names in, but it returns the bare type and drops the parameter’s own name, so wrap it to keep the label: namedArg(param.name, t.fromAbiParameter(param)). Without the wrapper the arg takes the positional arg{i} name.

Every builder position typed IntoExpr accepts either an Expr or a plain host literal:

type LitOf<t extends EvsType> = t extends NumericType
? bigint | number
: t extends 'address'
? `0x${string}`
: t extends 'bool'
? boolean
: t extends BytesNType
? `0x${string}`
: t extends 'string'
? string
: t extends 'bytes'
? `0x${string}`
: t extends TupleType
? TupleLitOf<t> // delegated to abitype: object (all-named) or array (positional)
: t extends `${infer e}[]`
? e extends StringType
? readonly (LitOf<e> | Expr<e>)[] // `T[]`; elements may be staged
: never
: t extends `${infer e}[${number}]`
? e extends StringType
? readonly (LitOf<e> | Expr<e>)[] // `T[N]` — N enforced at recording
: never
: t extends `${string}[${string}]`
? // `T[2][3]`, `T[][2]`, …: the element peeled by forward parsing
readonly (LitOf<NoInfer<ArrayElemOf<t>>> | Expr<NoInfer<ArrayElemOf<t>>>)[]
: never;
type IntoExpr<t extends EvsType> = Expr<t> | LitOf<t>;

Literals are validated at recording time, with the call-site location (EvsTypeError on violation):

Literal Rule
number for uintN or intN must be a safe integer; range-checked against N
bigint for uintN or intN range-checked; negatives two’s-complemented for intN
boolean only for bool
0x string for address exactly 20 bytes; checksum NOT enforced (viem-permissive)
0x string for bytesN exactly N bytes
0x string for bytes any even-length hex
string for string UTF-8 encoded
JS array for T[] element-wise rules of T; an element may also be an Expr of T
JS array for T[N] exactly N elements (TYPE_MISMATCH otherwise), element-wise

Word literals canonicalize at recording; dynamic literals and literal arrays become bytecode data segments. All-literal pure operations fold at recording, and a fold that would certainly panic throws EvsTypeError (CERTAIN_PANIC) at that line instead. When inference needs help, construct the literal explicitly with s.lit (builder reference).

import type { EvsType } from '@maxencerb/evs';
declare const exprBrand: unique symbol;
interface Expr<t extends EvsType = EvsType> {
readonly [exprBrand]: t; // nominal, covariant phantom — you never construct an Expr yourself
readonly type: t; // runtime-readable type tag, e.g. 'uint256'
}

(Abridged — the methods are tabulated below.) An Expr is a branded handle to a recorded program value. The brand makes it nominal: an Expr<'uint8'> is not assignable where Expr<'uint16'> is expected. The only runtime-readable member is type.

Each method uses a this-parameter to restrict itself to the types it is defined on — calling add on an Expr<'address'> is a TypeScript error, not a runtime surprise. Every binary method also exists as a free function on the builder (s.add(a, b), s.lt(a, b), …) for literal-left operands; see the builder reference.

Available on numeric Exprs (this: Expr of t where t is a NumericType). These methods are checked, matching solc 0.8 semantics — see arithmetic.

Method Signature Checked semantics
add add(rhs: IntoExpr<t>): Expr<t> Panic 0x11 on overflow
sub sub(rhs: IntoExpr<t>): Expr<t> Panic 0x11 on underflow
mul mul(rhs: IntoExpr<t>): Expr<t> Panic 0x11 on overflow
div div(rhs: IntoExpr<t>): Expr<t> Panic 0x12 on zero divisor; Panic 0x11 on signed minN / -1
mod mod(rhs: IntoExpr<t>): Expr<t> Panic 0x12 on zero divisor
pow pow(exponent: IntoExpr<'uint256'> | Expr<UintType>): Expr<t> Panic 0x11 when the power leaves t’s range; 0 ** 0 == 1; unsigned exponent

The explicit opt-out of the checks above, Solidity’s unchecked blocks: the result is the true result modulo 2^N of t (two’s complement for intN), and it never reverts. Same operand rules as the checked methods — see wrapping arithmetic.

Method Signature Semantics
wrappingAdd wrappingAdd(rhs: IntoExpr<t>): Expr<t> a + b modulo 2^N, no Panic
wrappingSub wrappingSub(rhs: IntoExpr<t>): Expr<t> a − b modulo 2^N, no Panic
wrappingMul wrappingMul(rhs: IntoExpr<t>): Expr<t> a · b modulo 2^N, no Panic

Available on Expr<'uint256'> only, like Solidity’s addmod / mulmod builtins and FullMath’s mulDiv. The intermediate sum or product is computed at full precision (512 bits): it never wraps.

Method Signature Checked semantics
addmod addmod(rhs: IntoExpr<'uint256'>, modulus: IntoExpr<'uint256'>): Expr<'uint256'> (a + b) % n; Panic 0x12 on n == 0
mulmod mulmod(rhs: IntoExpr<'uint256'>, modulus: IntoExpr<'uint256'>): Expr<'uint256'> (a * b) % n; Panic 0x12 on n == 0
mulDiv mulDiv(rhs: IntoExpr<'uint256'>, denominator: IntoExpr<'uint256'>): Expr<'uint256'> ⌊a · b / d⌋; Panic 0x12 on d == 0, Panic 0x11 when the quotient overflows
mulDivRoundingUp mulDivRoundingUp(rhs: IntoExpr<'uint256'>, denominator: IntoExpr<'uint256'>): Expr<'uint256'> ⌈a · b / d⌉; same Panics
Method Signature Notes
lt, gt, lte, gte lt(rhs: IntoExpr<t>): Expr<'bool'> OrderedTypes: numeric (LT/GT vs SLT/SGT chosen from the static signedness), plus address and bytesN as unsigned words (a bytesN orders byte by byte, like Solidity)
eq, neq eq(rhs: IntoExpr<t>): Expr<'bool'> Any type. Words compare directly; memrefs (string/bytes/T[]/structs) are hash equality — keccak256(a) == keccak256(b), byte-for-byte for string/bytes, element-wise via abi.encode for arrays/structs (two keccaks; one when the other side is a constant string/bytes or word-array literal, hashed at compile time)
Method Signature Notes
and and(rhs: IntoExpr<'bool'>): Expr<'bool'> Eager, NOT short-circuiting — both sides always execute
or or(rhs: IntoExpr<'bool'>): Expr<'bool'> Eager, NOT short-circuiting
not not(): Expr<'bool'> Logical negation

For conditional execution use s.if (control flow).

The bitwise methods are available on BitsType Exprs (uintN or bytesN); the shifts also on intN. Results are re-canonicalized to the operand’s width; shifts never panic — bits leaving the lane are dropped.

Method Signature Notes
bitAnd, bitOr, bitXor bitAnd(rhs: IntoExpr<t>): Expr<t> Bitwise within the type’s lane
bitNot bitNot(): Expr<t> Complement, re-masked to the type’s width
shl, shr shl(bits: IntoExpr<'uint256'>): Expr<t> Shift amount is always uint256; on intN, shr is arithmetic (SAR, rounds toward −∞) and shl re-sign-extends

Widening is free; narrowing is checked (Panic 0x11 on out-of-range values). Each method is typed for the receivers in its “Available on” column only: addr.toUint(t.uint160) or flag.toUint(t.uint8) is a compile error (and an EvsTypeError TYPE_MISMATCH at recording). On an address or a bytesN, both errors name the conversion to use first: asUint160() for an address, the same-width asUint() for a bytesN (addr.asUint160().toUint(t.uint256)). A bytes32 reaches the integers through asUint256() first. The wrapping conversions (wrappingToUint, wrappingToInt, asInt, asUint on an intN) never check: they keep the low N bits, as Solidity’s explicit conversions do (asInt() on an address, a bytesN or an intN is a compile error naming the route, like toInt’s). Like the arithmetic methods, toUint and toInt need a receiver whose type is numeric: a bare Expr (for example a widened-ABI result) is cast to its concrete type first (dec as Expr<'uint8'>).

Method Signature Available on Semantics
toUint toUint(target: u): Expr<u> any numeric Expr checked against the target range
toInt toInt(target: i): Expr<i> any numeric Expr checked against the target range
wrappingToUint wrappingToUint(target: u): Expr<u> any numeric Expr the low N bits (Solidity’s uint24(x)), never a Panic: one mask, or free when lossless
wrappingToInt wrappingToInt(target: i): Expr<i> any numeric Expr the low N bits as two’s complement (int24(uint24(x))), never a Panic: one SIGNEXTEND, or free when lossless
asAddress asAddress(): Expr<'address'> Expr<'uint256'> or Expr<'bytes32'> checked: panics unless the top 96 bits are zero
asAddress asAddress(): Expr<'address'> Expr<'uint160'> free (Solidity’s address(u160))
asUint160 asUint160(): Expr<'uint160'> Expr<'address'> free (Solidity’s uint160(a)); widen on with toUint
asUint256 asUint256(): Expr<'uint256'> Expr<'bytes32'> free reinterpret
asBytes32 asBytes32(): Expr<'bytes32'> Expr<'uint256'> free reinterpret
asUint asUint(): Expr<UintOfBytesN<t>> any bytesN free: the same-width uintN (bytes4 → uint32, Solidity’s uint32(b))
asUint asUint(): Expr<UintOfInt<t>> any intN the same-width two’s complement (int24 → uint24, Solidity’s uint24(i)): one mask, never a Panic
asBytesN asBytesN(): Expr<BytesNOfUint<t>> any uintN free: the same-width bytesN (uint32 → bytes4)
asInt asInt(): Expr<IntOfUint<t>> any uintN the same bits as two’s complement (uint24 → int24, Solidity’s int24(u)): one SIGNEXTEND, never a Panic
asBytes asBytes(): Expr<'bytes'> Expr<'string'> free reinterpret (the same memory)
asString asString(): Expr<'string'> Expr<'bytes'> free reinterpret (the same memory)
asString asString(): Expr<'string'> any bytesN a fresh string of its bytes, trailing zero bytes trimmed ('MKR\0…' → 'MKR')
import { evscript, t } from '@maxencerb/evs';
const casts = evscript({ name: 'casts', args: [t.bytes32] }, (s, raw) => {
const word = raw.asUint256(); // free reinterpret
const small = word.toUint(t.uint32); // checked narrowing — Panic 0x11 if out of range
const wide = small.toUint(t.uint256); // widening is free
const addr = raw.asAddress(); // checked: top 96 bits must be zero
return s.return({ small, wide, addr });
});
const ids = evscript({ name: 'ids', args: [t.address, t.bytes4, t.bytes32] }, (s, token, sel, raw) => {
const key = token.asUint160().toUint(t.uint256); // uint256(uint160(token))
const selector = sel.asUint(); // Expr<'uint32'> — uint32(sel)
const symbol = raw.asString(); // a legacy bytes32 symbol: 'MKR' + 29 zero bytes → 'MKR'
return s.return({ key, selector, symbol });
});
Method Signature Available on Semantics
length length(): Expr<'uint256'> string, bytes, any T[] byte length for string/bytes; element count for arrays
at at(i: IntoExpr<'uint256'>): Expr<elem> any T[] bounds-checked — Panic 0x32 on an out-of-range index
byteAt byteAt(i: IntoExpr<'uint256'>): Expr<'bytes1'> string, bytes the byte at i (Solidity’s b[i]) — Panic 0x32 past the end
slice slice(start: IntoExpr<'uint256'>, end?: IntoExpr<'uint256'>): Expr<t> string, bytes a fresh copy of bytes [start, end), end defaulting to the length (Solidity’s b[start:end]) — Panic 0x32 unless start ≤ end ≤ length
import { evscript, t } from '@maxencerb/evs';
const flags = evscript({ name: 'flags', args: [t.bytes4, t.int128] }, (s, mask, n) => {
const isNeg = n.lt(0n); // SLT — signedness comes from the static type
const top = mask.shr(16n); // re-masked to the bytes4 lane
const both = isNeg.and(mask.neq('0x00000000')); // eager bool logic
return s.return({ isNeg, top, both });
});