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
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 andbranches(...)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_ITERATIONScaps each helper call. The current exported value is128, and the tests import the constant directly so the guide stays aligned if that limit changes.and(...)andor(...)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 anydefaultarm must come last.- Build-time helpers return new
ExprNodetrees. 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