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 { DynType, WordType } from '@maxencerb/evs';
declare const t: { readonly [k in WordType | DynType]: k } & {
array<const e extends WordType>(elem: e): `${e}[]`;
};

t is a frozen object whose every property is its own literal type string — pure autocomplete sugar. Raw type strings are accepted everywhere t.* is.

MembersValueNotes
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)`${elem}[]`function: builds a dynamic-array type; elem must be a word type

t.array validates its element at the call site — a non-word element (so no string[], no nested arrays) throws EvsTypeError.

import { arg, t } from '@maxencerb/evs';
const fees = arg('fees', t.array(t.uint24)); // ArgSpec<'fees', 'uint24[]'>
const amount = arg('amount', 'uint128'); // raw strings work identically

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 ArrayType = `${WordType}[]`;
type EvsType = WordType | DynType | ArrayType;
type ArgType = EvsType;
type NumericType = UintType | IntType;
type BitsType = UintType | BytesNType;
AliasMeaning
HexA 0x-prefixed hex string.
AddressThe address string type, re-exported from abitype (the same type viem uses).
UintBitsThe 32 legal integer widths: every multiple of 8 from 8 to 256.
BytesSizeThe legal bytesN sizes: 1 through 32.
UintTypeuint8uint256 as a template-literal union.
IntTypeint8int256.
BytesNTypebytes1bytes32.
WordTypeEvery type that fits one EVM word: unsigned and signed integers, address, bool, bytesN.
DynTypeThe dynamic byte-string types: string and bytes.
ArrayTypeDynamic arrays of word types — uint256[], address[], and so on. v0 has no nested arrays.
EvsTypeEvery v0 value type: word, dynamic, or array.
ArgTypeAlias of EvsType — the valid script-argument types.
NumericTypeThe arithmetic domain: uintN or intN.
BitsTypeThe bitwise/shift domain: uintN or bytesN.
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).

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

import type { BytesNType, EvsType, Expr, NumericType, WordType } from '@maxencerb/evs';
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 `${infer e extends WordType}[]`
? readonly LitOf<e>[]
: never;
type IntoExpr<t extends EvsType> = Expr<t> | LitOf<t>;

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

LiteralRule
number for uintN or intNmust be a safe integer; range-checked against N
bigint for uintN or intNrange-checked; negatives two’s-complemented for intN
booleanonly for bool
0x string for addressexactly 20 bytes; checksum NOT enforced (viem-permissive)
0x string for bytesNexactly N bytes
0x string for bytesany even-length hex
string for stringUTF-8 encoded
JS array for T[]element-wise rules of T

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). All arithmetic is checked, matching solc 0.8 semantics — see arithmetic.

MethodSignatureChecked semantics
addadd(rhs: IntoExpr<t>): Expr<t>Panic 0x11 on overflow
subsub(rhs: IntoExpr<t>): Expr<t>Panic 0x11 on underflow
mulmul(rhs: IntoExpr<t>): Expr<t>Panic 0x11 on overflow
divdiv(rhs: IntoExpr<t>): Expr<t>Panic 0x12 on zero divisor; Panic 0x11 on signed minN / -1
modmod(rhs: IntoExpr<t>): Expr<t>Panic 0x12 on zero divisor
MethodSignatureNotes
lt, gt, lte, gtelt(rhs: IntoExpr<t>): Expr<'bool'>Numeric types only; LT/GT vs SLT/SGT chosen from the static signedness
eq, neqeq(rhs: IntoExpr<t>): Expr<'bool'>Any word type; no string/bytes/array equality in v0
MethodSignatureNotes
andand(rhs: IntoExpr<'bool'>): Expr<'bool'>Eager, NOT short-circuiting — both sides always execute
oror(rhs: IntoExpr<'bool'>): Expr<'bool'>Eager, NOT short-circuiting
notnot(): Expr<'bool'>Logical negation

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

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

MethodSignatureNotes
bitAnd, bitOr, bitXorbitAnd(rhs: IntoExpr<t>): Expr<t>Bitwise within the type’s lane
bitNotbitNot(): Expr<t>Complement, re-masked to the type’s width
shl, shrshl(bits: IntoExpr<'uint256'>): Expr<t>Shift amount is always uint256

Widening is free; narrowing is checked (Panic 0x11 on out-of-range values).

MethodSignatureAvailable onSemantics
toUinttoUint(target: u): Expr<u>any numeric Exprchecked against the target range
toInttoInt(target: i): Expr<i>any numeric Exprchecked against the target range
asAddressasAddress(): Expr<'address'>Expr<'uint256'> or Expr<'bytes32'>checked: panics unless the top 96 bits are zero
asUint256asUint256(): Expr<'uint256'>Expr<'bytes32'>free reinterpret
asBytes32asBytes32(): Expr<'bytes32'>Expr<'uint256'>free reinterpret
import { arg, evscript, t } from '@maxencerb/evs';
const casts = evscript({ name: 'casts', args: [arg('raw', t.bytes32)] }, (s) => {
const word = s.args.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 = s.args.raw.asAddress(); // checked: top 96 bits must be zero
return s.return({ small, wide, addr });
});
MethodSignatureAvailable onSemantics
lengthlength(): Expr<'uint256'>string, bytes, any T[]byte length for string/bytes; element count for arrays
atat(i: IntoExpr<'uint256'>): Expr<elem>any T[]bounds-checked — Panic 0x32 on an out-of-range index
import { arg, evscript, t } from '@maxencerb/evs';
const flags = evscript(
{ name: 'flags', args: [arg('mask', t.bytes4), arg('n', t.int128)] },
(s) => {
const isNeg = s.args.n.lt(0n); // SLT — signedness comes from the static type
const top = s.args.mask.shr(16n); // re-masked to the bytes4 lane
const both = isNeg.and(s.args.mask.neq('0x00000000')); // eager bool logic
return s.return({ isNeg, top, both });
},
);