Skip to main content

Built-in Functions

Lambit supports deterministic control flow, but it is not a general-purpose runtime language. The important distinction is:

  • unrolled iteration happens at author time in JavaScript/TypeScript;
  • runtime branching happens in the emitted Script through cond(...), branches(...), or normal multi-method dispatch;
  • arbitrary runtime loops, mutable variables, and dynamic array growth are not part of the model.

Use this page when you need contracts that feel algorithmic without depending on unsupported runtime control flow.

import { contract, method, TypeTag, add, eq, fold, lit } from '@opcat-labs/lambit';

const SumThree = contract(
'SumThree',
{ expected: TypeTag.Int },
({ props }) => ({
unlock: method(
{ x: TypeTag.Int, y: TypeTag.Int, z: TypeTag.Int },
(x, y, z) => eq(fold([x, y, z], lit(0n), (acc, value) => add(acc, value)), props.expected),
),
}),
);

const sumThree = SumThree({ expected: 6n });
console.log(sumThree.artifact.contract); // SumThree
caution

If a loop needs to depend on witness data, state, or transaction context at spend time, it is not a supported loop in Lambit. Rewrite it as explicit branches or unroll it at author time.

Supported model

Three authoring patterns cover most control-flow work:

  • Compile-time array transforms: fold, map, every, some
  • Compile-time numeric ranges: range, mapRange, foldRange, everyRange, someRange
  • Runtime branching: cond(...) for expression-level choices and branches(...) for stateful methods that may continue state on one path and terminate on another

Multiple contract methods are also control flow: each method(...) compiles as a separate dispatch arm in the generated selector-last script.

Compile-time iteration

When you need repeated structure, build it before compilation. Array helpers operate on explicit JavaScript arrays; range helpers synthesize the numeric indices for you. Range callbacks get both the literal range value (indexExpr) and the zero-based JavaScript loop counter (iteration). In the count form (foldRange(values.length, ...)), indexExpr is 0..count-1; in the bounded form (foldRange(1, values.length + 1, ...)), it follows the authored half-open interval.

import {
add,
and,
cond,
contract,
eq,
everyRange,
foldRange,
gt,
lit,
method,
sub,
TypeTag,
} from '@opcat-labs/lambit';

const WindowedThreshold = contract(
'WindowedThreshold',
{ target: TypeTag.Int32, bias: TypeTag.Int32 },
({ props }) => ({
unlock: method(
{
a: TypeTag.Int32,
b: TypeTag.Int32,
c: TypeTag.Int32,
d: TypeTag.Int32,
useBias: TypeTag.Bool,
},
(a, b, c, d, useBias) => {
const values = [a, b, c, d];
const weighted = foldRange(values.length, lit(0n), (acc, indexExpr, iteration) =>
add(acc, add(values[iteration]!, indexExpr)),
);
const allPositive = everyRange(values.length, (_indexExpr, iteration) =>
gt(values[iteration]!, lit(0n)),
);
const selected = cond(
useBias,
add(weighted, props.bias),
sub(weighted, props.bias),
);

return and(allPositive, eq(selected, props.target));
},
),
}),
);

const policy = WindowedThreshold({ target: 12n, bias: 2n });
// => ['unlock', 'constructor']
policy.artifact.abi.map((entry) => entry.type === 'constructor' ? entry.type : entry.name);

Use indexExpr inside the returned IR tree and iteration for local JavaScript array indexing. That keeps the generated Script deterministic and avoids the old manual-index-array workaround.

If you need direct access to the literal indices themselves, use range(...) to materialize the half-open interval as an array of ExprNode integer literals, then feed that array into your own map(...), fold(...), or positional logic.

For fixed collections you already have in scope, the array helpers are the shortest form:

import {
add,
and,
contract,
eq,
fold,
gt,
lit,
method,
some,
TypeTag,
} from '@opcat-labs/lambit';

const SumAndPresence = contract(
'SumAndPresence',
{ target: TypeTag.Int32 },
({ props }) => ({
unlock: method(
{ x: TypeTag.Int32, y: TypeTag.Int32, z: TypeTag.Int32 },
(x, y, z) => {
const values = [x, y, z];
const total = fold(values, lit(0n), (acc, value) => add(acc, value));
const hasPositive = some(values, (value) => gt(value, lit(0n)));
return and(hasPositive, eq(total, props.target));
},
),
}),
);

const sumAndPresence = SumAndPresence({ target: 6n });

Runtime branching

Use cond(...) when both branches return plain expressions. Use branches(...) when a stateful method must choose between successor state and terminal outputs. The four-argument contract(name, props, state, body) form below is the same stateful builder surface described by stateful(...); the third positional argument is just the state schema.

import {
add,
branches,
contract,
method,
p2pkhOutput,
TypeTag,
} from '@opcat-labs/lambit';

const ScoreRouter = contract(
'ScoreRouter',
{ winner: TypeTag.Ripemd160 },
{ score: TypeTag.Int32 },
({ props, state }) => ({
step: method(
{ close: TypeTag.Bool, delta: TypeTag.Int32 },
(close, delta, ctx) =>
// The trailing ctx proxy exposes the spent value for terminal payouts.
branches(
{ when: close, do: { outputs: [p2pkhOutput(ctx.value, props.winner)] } },
{ default: { next: { score: add(state.score, delta) } } },
),
),
}),
);

const scoreRouter = ScoreRouter({ winner: '11'.repeat(20) });
const stepAbi = scoreRouter.artifact.abi.find(
(entry) => entry.type === 'function' && entry.name === 'step',
);
// => 'branches'
stepAbi?.returnShape;

Limits

  • Range helpers use half-open intervals: start <= i < end.
  • Signed bounds are allowed, but all bounds must be safe integers.
  • MAX_COMPILE_TIME_ITERATIONS caps each helper call. The current exported value is 128, and the tests import the constant directly so the guide stays aligned if that limit changes.
  • and(...) and or(...) require at least one argument. If a helper may be empty, use the helper forms that define empty behavior (every* => true, some* => false, fold* => init).
  • branches(...) requires at least one arm, and any default arm must come last.
  • Build-time helpers return new ExprNode trees. They do not create runtime arrays in Script.
import { contract as defineContract, method as defineMethod, TypeTag as Tags, add as plus, eq as equals, lit as literal } from '@opcat-labs/lambit';

const WeightRule = defineContract(
'WeightRule',
{ expected: Tags.Int },
({ props }) => ({
unlock: defineMethod({ x: Tags.Int }, (x) => {
// good, the repeat count is fixed while authoring the contract
const values = [x, literal(1n), literal(2n)];
return equals(values.reduce((acc, value) => plus(acc, value)), props.expected);

// invalid, runtime witness values cannot decide how many script branches are emitted
// return range(Number(x)).reduce((acc, value) => plus(acc, value));
}),
}),
);

const weightRule = WeightRule({ expected: 8n });
console.log(weightRule.artifact.contract); // WeightRule

Practical guidance

  • Prefer multiple explicit methods when your contract already has clear public entry points; that keeps dispatch shallow and ABI output understandable.
  • Prefer cond(...) for local expression choices inside one method.
  • Prefer branches(...) only when one stateful method truly has mixed successor-state and terminal-output paths.
  • Keep compile-time loops small and intentional. Even though the helpers are ergonomic, unrolling still increases script size linearly.

Range and array helpers emit deterministic Script because their iteration counts are known while the artifact is being built.1

What's Next

Footnotes

  1. Witness values can flow through the expressions returned by each callback, but they cannot decide how many callbacks run.