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.
The t namespace
Section titled “The t namespace”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 → objectconst Pair = t.tuple(t.address, t.uint256); // positional tuple → [address, bigint]const amount = 'uint128'; // raw strings work identicallyvoid [fees, Position, Pair, amount];Type aliases
Section titled “Type aliases”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 levelstype StringType = ScalarType | ArrayType; // every string-encoded typeinterface 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 TupleTypetype NumericType = UintType | IntType;type BitsType = UintType | BytesNType;type OrderedType = NumericType | 'address' | BytesNType;// the same-width bytesN ↔ uintN twins (`asUint()` / `asBytesN()`), through two lookup tablesinterface 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). |
Composite types (TupleType)
Section titled “Composite types (TupleType)”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.
Deriving a type from an ABI
Section titled “Deriving a type from an ABI”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 oft.fromOutputs(abi, name): a single output’s type, or aTupleTypestruct over several outputs (named, in ABI declaration order). An unknown name or a non-constABI widens toEvsType; an overloaded function is named by its canonical signature ('get(uint256)'), and an overloaded bare name throws at recording.AbiParamToEvsType<p>— the type oft.fromAbiParameter(p): aTupleTypefor atuple…parameter, else the scalar/array type string.
The component-level mirrors AbiParamToComponent and AbiParamsToComponents are exported too, for
building your own ABI→type glue.
ABI parameters as args
Section titled “ABI parameters as args”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 handleconst 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.
Literal coercion: LitOf and IntoExpr
Section titled “Literal coercion: LitOf and IntoExpr”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.
Arithmetic (checked)
Section titled “Arithmetic (checked)”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 |
Arithmetic (wrapping)
Section titled “Arithmetic (wrapping)”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 |
Full-precision arithmetic
Section titled “Full-precision arithmetic”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 |
Comparisons
Section titled “Comparisons”| 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) |
Boolean logic
Section titled “Boolean logic”| 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).
Bitwise and shifts
Section titled “Bitwise and shifts”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 |
Conversions and casts
Section titled “Conversions and casts”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 });});Dynamic values and arrays
Section titled “Dynamic values and arrays”| 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 |
Putting it together
Section titled “Putting it together”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 });});