@endo/patternsPattern matching and validation for passable data, with copy-collections and interface guards.
The @endo/patterns package provides the M namespace for creating pattern
matchers that validate passable data and describe behavioral contracts.
This is the validation layer above @endo/pass-style,
enabling you to check that data matches expected shapes before using it.
Patterns enable:
import { M, mustMatch } from '@endo/patterns';
const specimen = harden({ foo: 3, bar: 4 });
const pattern = M.splitRecord(
{ foo: M.number() }, // required properties
{ bar: M.string(), baz: M.number() } // optional properties
);
mustMatch(specimen, pattern);
// throws: 'bar?: number 4 - Must be a string'
For best rendering, use the Endo reference docs site.
The M object provides methods for creating pattern matchers organized into
several categories:
Match specific JavaScript types:
M.any() // Matches any passable
M.undefined() // Matches undefined
M.null() // Matches null
M.boolean() // Matches true or false
M.number() // Matches any number (including NaN, Infinity)
M.bigint() // Matches any bigint
M.string() // Matches any string
M.symbol() // Matches registered/well-known symbols
// Constrained primitives
M.nat() // Non-negative bigint
M.gte(5) // Number >= 5
M.lte(100) // Number <= 100
Match copyArray, copyRecord, and other structures:
M.array() // Any CopyArray
M.record() // Any CopyRecord
M.set() // Any CopySet
M.bag() // Any CopyBag
M.map() // Any CopyMap
// With constraints
M.array({ maxSize: 10 }) // Array with at most 10 elements
M.string({ maxSize: 100 }) // String with at most 100 characters
// Structured content
M.arrayOf(M.number()) // Array of numbers only
M.recordOf(M.string(), M.number()) // Record with string keys, number values
M.setOf(M.string()) // Set of strings only
Match specific shapes:
// Split patterns: required, optional, rest
M.splitArray(
[M.string(), M.number()], // required elements
[M.boolean()], // optional elements
M.any() // rest elements
)
M.splitRecord(
{ name: M.string() }, // required properties
{ age: M.number() }, // optional properties
M.any() // rest properties
)
// Partial matches
M.partial({ name: M.string() }) // Has at least 'name' property
// Split auto-detects array vs record
M.split({ x: M.number() }, M.any())
Combine matchers:
M.and(M.number(), M.gte(0), M.lte(100)) // 0 <= n <= 100
M.or(M.string(), M.number()) // String or number
M.not(M.undefined()) // Anything except undefined
M.opt(M.string()) // undefined or string (optional)
Match values relative to a key:
M.eq('hello') // Equal to 'hello'
M.neq(0) // Not equal to 0
M.lt(10) // Less than 10
M.lte(100) // Less than or equal to 100
M.gte(0) // Greater than or equal to 0
M.gt(-1) // Greater than -1
M.remotable() // Any remotable object
M.remotable('Counter') // Remotable with specific label
M.error() // Any error
M.promise() // Any promise
M.eref(M.number()) // Number or promise for number (eventual reference)
M.kind('copyArray') // Specific pass style
M.pattern() // Any valid pattern
M.key() // Any valid Key
M.scalar() // Any primitive or remotable
Returns true if the specimen matches the pattern, false otherwise:
import { M, matches } from '@endo/patterns';
matches(42, M.number()); // true
matches('hello', M.number()); // false
matches([1, 2, 3], M.arrayOf(M.number())); // true
Throws with a descriptive error if the specimen doesn't match:
import { mustMatch } from '@endo/patterns';
mustMatch(42, M.string());
// throws: "number 42 - Must be a string"
mustMatch(-5, M.and(M.number(), M.gte(0)), 'count');
// throws: "count: number -5 - Must be >= 0"
The error messages are designed to help you understand exactly what was wrong with the data.
Patterns introduces three passable collection types built on makeTagged():
A set of unique Keys (primitives or remotables):
import { makeCopySet } from '@endo/patterns';
const colors = makeCopySet(['red', 'blue', 'green']);
// Elements are sorted in rank order
// Duplicates are removed
// Can be passed between vats
// Pattern for sets
const ColorSet = M.setOf(M.string());
mustMatch(colors, ColorSet); // passes
Why not use JavaScript Set?
JavaScript Sets aren't passable.
CopySet is frozen, comparable via keyEQ, and can be efficiently serialized.
A multiset (elements with counts):
import { makeCopyBag } from '@endo/patterns';
const inventory = makeCopyBag([
['apples', 5n],
['oranges', 3n],
['apples', 2n] // counts are combined
]);
// Result: [['apples', 7n], ['oranges', 3n]]
const InventoryPattern = M.bagOf(M.string(), M.bigint());
mustMatch(inventory, InventoryPattern);
A map from Keys to Passable values:
import { makeCopyMap } from '@endo/patterns';
const balances = makeCopyMap([
['alice', 100],
['bob', 50]
]);
// Keys are sorted in rank order
// Can use any Key as a key (not just strings!)
const remotableKey = Far('Key', {});
const map = makeCopyMap([[remotableKey, 'value']]);
const BalancesPattern = M.mapOf(M.string(), M.number());
mustMatch(balances, BalancesPattern);
Why not use plain objects? CopyMap supports:
compareKeys()InterfaceGuards describe behavioral contracts for objects, particularly useful with @endo/exo:
import { M } from '@endo/patterns';
const CounterI = M.interface('Counter', {
// Synchronous method
increment: M.call(M.number()).returns(M.number()),
// Method with optional arguments
reset: M.call().optional(M.number()).returns(),
// Method with rest arguments
add: M.call(M.number()).rest(M.number()).returns(M.number()),
// Async method (awaits arguments)
asyncOp: M.callWhen(M.string()).returns(M.string())
});
// Basic call: call(required args...)
M.call(M.string(), M.number())
// With optional args
M.call(M.string()).optional(M.number())
// With rest args
M.call(M.string()).rest(M.any())
// Specify return type
M.call(M.string()).returns(M.number())
// Async method (awaits promise args)
M.callWhen(M.remotable()).returns(M.string())
InterfaceGuards are enforced automatically by exos:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
const CounterI = M.interface('Counter', {
increment: M.call(M.number()).returns(M.number())
});
const counter = makeExo('Counter', CounterI, {
increment(n) {
// n is guaranteed to be a number by the guard
return count += n;
}
});
counter.increment(5); // OK
counter.increment('5'); // throws: Must be a number
This is the foundation of defensive programming in Endo: guards validate inputs automatically, so your methods can focus on business logic.
Keys can be compared for equality and ordering:
Tests if two Keys are equal using distributed equality semantics:
import { keyEQ } from '@endo/patterns';
keyEQ('hello', 'hello'); // true
keyEQ(42, 42); // true
keyEQ([1, 2], [1, 2]); // true (compares content)
const r1 = Far('Obj', {});
const r2 = Far('Obj', {});
keyEQ(r1, r1); // true (same remotable)
keyEQ(r1, r2); // false (different remotables)
Returns a comparison result implementing a partial order:
0: Keys are equal-1: key1 < key21: key1 > key2NaN: Keys are incomparableimport { compareKeys, keyLT, keyGT } from '@endo/patterns';
compareKeys('a', 'b'); // -1
compareKeys(5, 5); // 0
compareKeys(10, 3); // 1
// Convenience functions
keyLT('a', 'b'); // true
keyGT(10, 3); // true
// Incomparable keys
const r1 = Far('A', {});
const r2 = Far('B', {});
compareKeys(r1, r2); // NaN (different remotables)
Why partial order? Not all Keys can be compared. For example, different remotables have no defined ordering, and CopySets use subset relationships.
Understanding the type hierarchy:
Passable (everything that can pass)
├── Error
├── Promise
├── Key (stable, comparable)
│ ├── Primitives (null, undefined, boolean, number, bigint, string, symbol)
│ ├── Remotable
│ ├── CopyArray<Key>
│ ├── CopyRecord<Key>
│ ├── CopySet<Key>
│ ├── CopyBag<Key>
│ └── CopyMap<Key, Passable>
└── Pattern (describes a set of Passables)
├── Key (matches itself)
└── Key-like with Matcher leaves
Complete Tutorial: See Message Passing for a comprehensive guide showing how patterns work with pass-style, exo, and eventual-send.
For implementation details: