Kalandra

Learn

Parallel execution

A functional language for parallel and distributed programming. The next chapter of Clio.

Parallel calls use module-level functions as worker targets. Put vertical bars around the function name to select its parallel form. Calls return promises; await a single result, or use the mapped await-all form shown below.

Map typed work to workers

The params annotation establishes a concrete Number parameter. The compiler infers the numeric result. The mapped call sends each value to double, and [await] joins all results while preserving their input order.

main.kalandra

kalandra
@params Number
fn double n: n * 2

export fn main:
  [1.5 2.5 3.5] -> * [await] |double|

Expected result:

text
[3,5,7]

Try in playground

What crosses the boundary

The compiler generates the Sia schemas for the arguments and resolved result. Unknown or unsupported values fail at the parallel call with KAL4202, even without strict-name checking. There is no dynamic serialization fallback.

Tasks receive data snapshots. A worker cannot mutate the caller's copy of an argument or capture. Module-level targets can schedule nested tasks through the same bounded pool; the runtime admits children centrally so one worker can still make progress.

Await deliberately

An ordinary await on an array does not await its promise elements. Use mapped await-all when all worker results are needed. Do not apply await-all directly to a single worker promise: the preserved behavior fails at runtime because that promise is not iterable.

Bounds and hosts

The Bun runner uses subprocesses; the browser adapter uses Web Workers. The playground owns two parallel workers, limits queued work, and stops the whole run after ten seconds. Stop ends running work; it cannot undo side effects already completed.

Only statically selected module-level targets are supported. Arbitrary function-local closures and external JavaScript functions cannot become worker targets. An experimental Bun host API supports typed remote imports from local client contracts. Endpoint credentials stay in the host; the browser playground cannot make network calls. Remote record contracts preserve constructors, inheritance and nominal identity. Explicitly typed service captures with pure initializers are supported. Local workers can call remote services through the parent host. Effectful service setup and outbound calls from remote service hosts remain unsupported.

Try nested parallel work

Try typed captures

Current limits and port status

Hosting and running remote programs

Build selected services with bun run cli build server/main.kalandra -o out --service calculate=calculate. Set KALANDRA_REMOTE_TOKEN in the environment, then run bun run cli serve out/kalandra.deployment.json --port 3000. The token must contain 32–256 ASCII letters, digits, underscores or hyphens. The listener binds only to 127.0.0.1; production TLS deployment remains separate work.

For a client importing remote:./math.client.json, supply an endpoint file when running:

json
{\n  "version": 1,\n  "endpoints": {\n    "math.client.json": {\n      "url": "http://127.0.0.1:3000",\n      "tokenEnv": "MATH_TOKEN"\n    }\n  }\n}

Set MATH_TOKEN to the service token in the client environment, then use bun run cli run client/main.kalandra --bindings endpoints.json. Contract keys are relative to the source root. Tokens stay outside source, generated manifests and command arguments. Missing or unused bindings fail before program execution. Local parallel tasks and remote calls can run together.

On SIGINT or SIGTERM, the client cancels submitted calls and waits for acknowledgement and worker cleanup before exiting. Program failures also cancel outstanding work. An unconfirmed cancellation is reported as an unknown execution outcome. The service waits for cleanup when stopped.

Captured service data

Remote services may capture explicitly typed module data initialized from literals, pure numeric expressions, typed arrays, record constructors and other eligible typed declarations.

kalandra
Number multiplier = 3

@params Number
export fn multiply n:
  n * multiplier

Build this export as a service and host it with the commands above. The server initializes multiplier once before listening. Each request gets a fresh copy of its captures; record mutations inside a request cannot change another request or the startup baseline. Nested local tasks receive the calling request's current copy.

Clients supply only the explicit arguments. Capture values stay out of the client contract. Any and dynamic data remain compile errors. Modules supplying captures cannot perform top-level setup calls or later writes; initialization must use the supported pure subset. The compiler never evaluates initializers, and hosting does not call main or import the application entry. Initialization has a bounded subprocess lifetime and is cancelled on shutdown.

Remote calls from a local worker

A local worker can use checked remote imports. The parent host owns the endpoint bindings and performs the network requests.

kalandra
from "remote:./math.client.json" import calculate

@params Number
fn work n: await calculate n

export fn main: await |work| 2

Use run with the usual --bindings endpoints.json option. Remote children share the task tree's depth and outstanding-task limits. A local deadline, failure or cancellation also cancels remote descendants and waits for cleanup acknowledgement. Endpoint credentials are not sent with tasks; the CLI removes the token variables named in endpoint bindings from local worker environments.

Import aliases, namespace calls and calls through local helpers work. Dynamic arguments still fail compilation. Local worker signatures can use imported remote record types. Local types can also extend those records. Remote service hosts cannot yet make outbound service calls or export services whose schemas depend on imported record declarations.

Remote record types in worker signatures

Import a record constructor from a trusted client contract and use it in params and returns annotations. This example assumes the contract exports Point and echoPoint:

kalandra
from "remote:./math.client.json" import Point echoPoint

@params Point
@returns Point
fn work point: await echoPoint point

export fn main: await |work| (Point 1.5 2.5)

The record crosses the local worker boundary, the remote service boundary, and both return paths with its constructor identity intact. Named aliases, local re-exports and immutable namespace imports refer to the same constructor. Worker code can construct these records too. Typed lists, containing records and typed local captures use the same rules.

Records with matching fields but different types or deployment revisions are rejected. Plain objects, extra fields and accessor properties cannot stand in for a declared record. The existing check decorator still checks an async function's immediate Promise result; this change does not alter that tracked behavior.

Extending an imported record

A local type can extend an imported record and travel between local workers. Constructor arguments start with the local fields, followed by the parent's arguments. This example assumes Point has Number fields z and x, in that order:

kalandra
from "remote:./math.client.json" import Point

export type WeightedPoint is Point:
  Number weight

@params WeightedPoint
@returns WeightedPoint
@check
fn identity point: point

export fn main: await |identity| (WeightedPoint 7 1.5 2.5)

The returned value keeps its WeightedPoint identity and Point inheritance, including across nested worker calls, containing records and typed captures. Its nominal type is still WeightedPoint. A service expecting Point will reject it; explicitly construct Point value.z value.x when that is the value you intend to send. Extra fields are never silently stripped to fit a parent schema.

Mapping remote calls

A service build registers statically known array-map forms, so an independent client does not need matching map calls in the server source:

kalandra
from "remote:./math.client.json" import calculate

export fn main: [1.5 2.5 3.5] -> * [await] calculate

The callback receives the item, any explicit arguments, the index and the source array. Results retain input order. Typed records and services with typed captures are supported, including calls from local workers. Every mapped request gets its own server capture snapshot. Unknown types, direct Range mapping and call shapes absent from the contract still fail compilation.

A callback using the check decorator must annotate the index and collection too. The preserved decorator checks every argument; an annotation for only the item fails when map metadata arrives. For a numeric callback, declare a Numbers list and use params Number Number Numbers. This behavior is tracked for the later language-design phase.