VibeLang

Warning: VibeLang is v0. The language will change in major, breaking ways. It is not suitable for production use.

Control operations and handlers

A control operation lets a handler decide how the caller continues. Use one when a dependency must pause a program, answer a request, or end the program early.

Declare and perform an operation

VibeLang
// approval.vibe
// run: Console.log(handle(() => release("v1"), Approval, { approve: (_request, k) => resume(k, true) }))
export error ReviewBusy { request: string }

export control Approval {
  approve(request: string): boolean!ReviewBusy
}

export function release(version: string) {
  if (!Approval.approve(`deploy ${version} to staging`)) return "staging declined"
  if (!Approval.approve(`deploy ${version} to production`)) return "production declined"
  return `released ${version}`
}
// string!ReviewBusy!Approval
// Output: released v1

Types in this example

error
class ReviewBusy
ReviewBusy
class ReviewBusy
request
(property) request: string
control
abstract class Approval
Approval
abstract class Approval
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!
request
(parameter) request: string
release
function release(version: string): string!ReviewBusy!Approval
version
(parameter) version: string
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!Runs here and returns boolean.

Approval.approve returns a boolean at the call. Approval stays in the program's requirements until a handler answers it. Each abstract operation states all three channels of its contract.

Control, handle, resume, resumeWith, and abandon are globals in .vibe files.

Run one program with two policies

VibeLang
// policies.vibe
// run: Console.log(JSON.stringify(comparePolicies()))
import { Approval, release } from "./approval.vibe"

export function autoApprove(version: string) {
  try {
    return release(version)
  } handle Approval.approve(_request) {
    resume(true)
  }
}

export function scriptedReview(version: string, decisions: ReadonlyArray<boolean>) {
  let next = 0
  try {
    return release(version)
  } handle Approval.approve(_request) {
    const approved = decisions[next] ?? false
    next++
    resume(approved)
  }
}

export function comparePolicies() {
  return [autoApprove("v1"), scriptedReview("v1", [true, false])]
  // ["released v1", "production declined"]
}
// Output: ["released v1","production declined"]

Types in this example

Approval
(alias) abstract class Approval
release
(alias) function release(version: string): string!ReviewBusy!Approval
autoApprove
function autoApprove(version: string): string!ReviewBusy!
version
(parameter) version: string
release
(alias) function release(version: string): string!ReviewBusy!ApprovalRuns here and returns string.
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!
_request
const _request: string
resume
const resume: <boolean, ReviewBusy, never>(k: Continuation<boolean, ReviewBusy, never>, value: boolean) => !!Runs here and returns never.Continues the program with value at the perform site; returns what the handle then produces.
scriptedReview
function scriptedReview(version: string, decisions: ReadonlyArray<boolean>): string!ReviewBusy!
decisions
(parameter) decisions: readonly boolean[]
ReadonlyArray
interface ReadonlyArray<T>
next
let next: number
approved
const approved: boolean
comparePolicies
function comparePolicies(): string[]!ReviewBusy!
autoApprove
function autoApprove(version: string): string!ReviewBusy!Runs here and returns string.
scriptedReview
function scriptedReview(version: string, decisions: ReadonlyArray<boolean>): string!ReviewBusy!Runs here and returns string.

The business program stays the same. A handle clause after a try block names an operation, Approval.approve, and parameters that take the operation's parameter types. Each time the try block performs the operation, the clause runs.

resume(true) continues the try block with true as the operation's result. The handler is deep: the next approval request reaches the same clause. A clause that ends with resume runs in constant stack space, however many times the block performs the operation.

A return in the try block or in a clause returns from the enclosing function, and break and continue leave its loops. A statement answers every operation of each control it names: give each operation a clause (EK2108). One statement can handle several controls.

A clause can work after resume. The try block runs first, until it finishes. Then the clause goes on, and when the clause ends, the statement ends the way the try block did: a return in the try block takes effect after the clause, as a pending return waits for a finally block.

A continuation can be consumed once. Call resume, resumeWith, and abandon in the clause's own code, not in a function nested inside it. The runtime rejects a second resume.

Stop and clean up

VibeLang
// stopping.vibe
// run: Console.log(stopDeployment())
import { Approval } from "./approval.vibe"

function deployment() {
  try {
    Approval.approve("deploy to production")
    return "deployed"
  } finally {
    Console.log("deployment hold released")
  }
}

export function stopDeployment() {
  try {
    return deployment()
  } handle Approval.approve(_request) {
    abandon()
    Console.log("cleanup finished")
    return "cancelled"
  }
}
// Output: deployment hold released
// Output: cleanup finished
// Output: cancelled

Types in this example

Approval
(alias) abstract class Approval
deployment
function deployment(): string!ReviewBusy!Approval
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!Runs here and returns boolean.
Console
(alias) module "effect/Console"
log
const log: (...args: ReadonlyArray<any>) => void!!Runs here and returns void.Logs a general-purpose message to the console.
stopDeployment
function stopDeployment(): string!ReviewBusy!
deployment
function deployment(): string!ReviewBusy!ApprovalRuns here and returns string.
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!
_request
const _request: string
abandon
const abandon: <boolean, ReviewBusy, never>(k: Continuation<boolean, ReviewBusy, never>) => void!!Runs here and returns void.Stops the program: its cleanup runs, its catch blocks do not. Fails if cleanup fails.

abandon() runs the try block's cleanup before the clause continues. It skips the block's catch blocks and runs its finally blocks and resource releases. Cleanup failures still fail the operation.

A clause that ends without resuming also stops the try block and cleans it up first. The statement then completes, and the function continues after it. Use abandon() when the clause needs cleanup to finish before its next statement.

Resume with an expected failure

VibeLang
// busy.vibe
// run: Console.log(busyReviewer())
import { Approval, ReviewBusy } from "./approval.vibe"

export function busyReviewer() {
  try {
    return Approval.approve("deploy now") ? "approved" : "declined"
  } handle Approval.approve(request) {
    resumeWith(() => { throw new ReviewBusy({ request }) })
  } catch (error) {
    return `review unavailable: ${error.request}`
  }
}
// Output: review unavailable: deploy now

Types in this example

Approval
(alias) abstract class Approval
ReviewBusy
(alias) class ReviewBusy
busyReviewer
function busyReviewer(): string!!
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!Runs here and returns boolean.
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!
request
const request: string
resumeWith
const resumeWith: <boolean, ReviewBusy, never>(k: Continuation<boolean, ReviewBusy, never>, program: boolean!NoInfer<ReviewBusy>!) => !!Runs here and returns never.Continues the program by running program at the perform site (D17): it may fail only with the operation's declared errors, and needs nothing (EK2005).
ReviewBusy
(alias) constructor ReviewBusy<{ readonly request: string; }>(args: { readonly request: string; }): ReviewBusy
request
(property) request: string
error
var error: ReviewBusy

resumeWith runs its program at the original operation call, inside the try block. This program can raise only errors declared by the operation, and it must need no services. Read services in the clause first, then capture their values in the program passed to resumeWith.

Catch clauses and a finally block may follow the handle clauses. They cover the try block and the clauses, so here the catch clause sees the ReviewBusy that the operation call raised.

Use handle as an expression

The statement is short for the handle function, which takes a program and returns its result:

VibeLang
// expression.vibe
// run: Console.log(countReleased(["v1", "v2"]))
import { Approval, release } from "./approval.vibe"

export function countReleased(versions: ReadonlyArray<string>) {
  let released = 0
  for (const version of versions) {
    const outcome = handle(() => release(version), Approval, {
      approve(_request, k) {
        return resume(k, true)
      }
    })
    if (outcome.startsWith("released")) released++
  }
  return released
}
// Output: 2

Types in this example

Approval
(alias) abstract class Approval
release
(alias) function release(version: string): string!ReviewBusy!Approval
countReleased
function countReleased(versions: ReadonlyArray<string>): number!ReviewBusy!
versions
(parameter) versions: readonly string[]
ReadonlyArray
interface ReadonlyArray<T>
released
let released: number
version
const version: string
outcome
const outcome: string
handle
function handle<string, ReviewBusy, Approval, Approval, Approval, { approve(_request: string, k: Continuation<boolean, ReviewBusy, string>): string!!; }>(program: string!ReviewBusy!Approval, control: Key<Approval, Approval>, clauses: { approve(_request: string, k: Continuation<boolean, ReviewBusy, string>): string!!; }): ...!!Runs here and returns string.handle(program, Control, clauses, onReturn?): runs program with a deep handler for Control's operations. Each clause receives the operation's arguments and k, the rest of the program. The result is what the program returns (through onReturn when given) or what a clause returns without resuming. Inside a clause, resume(k, v) has the program's own result type; with onReturn it has type never, so a clause's tail resume adds nothing to the result (D97).
release
(alias) function release(version: string): string!ReviewBusy!ApprovalRuns here and returns string.
approve
(method) approve(_request: string, k: Continuation<boolean, ReviewBusy, string>): string!!
_request
(parameter) _request: string
k
(parameter) k: Continuation<boolean, ReviewBusy, string>
resume
const resume: <boolean, ReviewBusy, string>(k: Continuation<boolean, ReviewBusy, string>, value: boolean) => string!!Runs here and returns string.Continues the program with value at the perform site; returns what the handle then produces.
startsWith
(method) String.startsWith(searchString: string, position?: number): booleanReturns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.

handle(program, Control, clauses, onReturn?) takes a program, the control class, and a clause for every operation. A clause receives the operation's arguments followed by k, the continuation, which resume(k, value), resumeWith(k, program), and abandon(k) take. resume returns the resumed program's result. The optional fourth argument of handle transforms normal returns. In this preview, resume is typed with the program's original result, or never when that fourth argument is supplied; avoid inspecting the resumed result when clauses return different types. Keep k inside its clause; never save it or return it.

Keep handlers on the executing fiber

VibeLang
// concurrent.vibe
// run: Console.log(JSON.stringify(approveAll(["staging", "production"])))
import { Approval } from "./approval.vibe"

export function approveAll(requests: ReadonlyArray<string>) {
  return forEach(requests, (request) => {
    try {
      return Approval.approve(request)
    } handle Approval.approve(_request) {
      resume(true)
    }
  }, { concurrency: 4 })
}
// Output: [true,true]

Types in this example

Approval
(alias) abstract class Approval
approveAll
function approveAll(requests: ReadonlyArray<string>): boolean[]!ReviewBusy!
requests
(parameter) requests: readonly string[]
ReadonlyArray
interface ReadonlyArray<T>
forEach
const forEach: <boolean, ReviewBusy, never, readonly string[], false>(self: readonly string[], f: (a: string, i: number) => boolean!ReviewBusy!, options?: { readonly concurrency?: Concurrency | undefined; readonly discard?: false | undefined; } | undefined) => boolean[]!ReviewBusy!Runs here and returns boolean[].Executes an effectful operation for each element in an Iterable.
request
(parameter) request: string
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!Runs here and returns boolean.
approve
(method) Approval.approve(request: string): boolean!ReviewBusy!
_request
const _request: string
resume
const resume: <boolean, ReviewBusy, never>(k: Continuation<boolean, ReviewBusy, never>, value: boolean) => !!Runs here and returns never.Continues the program with value at the perform site; returns what the handle then produces.
concurrency
(property) concurrency?: Concurrency | undefined

Handle the operation inside each concurrent branch. A handler on the parent fiber cannot answer a child fiber's control operations.

DiagnosticMeaningFix
EK2005A resumeWith program needs a service or raises an undeclared errorRead the service in the clause; raise only the operation's declared errors
EK2107A handle clause names an operation of something that is not a control, such as a serviceProvide a service with a layer or a provide block
EK2108A handle statement leaves an operation of its control without a clause, or gives one two clausesAdd the missing clause, or remove the second
EK4201Work sent to another fiber still requires a control operationHandle it inside the branch, or use sequential execution where the combinator supports it
EK4202A TypeScript function's declared requirements include a control operationWrite it in VibeLang or handle the operation inside the TypeScript implementation

Stream operators run a producer on its own fiber too. Handle any control requirements inside the producer before passing it to a Stream operator. See Producers.