Effektive

Language
specification.

Language core 1. Design draft 0.63. October 24, 2026.

Read the reference
Chapters

Status and scope

Effektive is a small functional language designed to transpile to Effect-TS, the Effect library for TypeScript, version 4. Immutable values, ordinary functions, and explicit contracts define its source style.

The transpiler targets the effect core modules and selected modules from the Effect ecosystem. The section on typed IR and backends names each module.

This reference defines language core version 1. The documentation edition is draft 0.63, dated October 24, 2026.

Core 1 specifies additional source features in stages. The compiler implements the documented core 1 subset, including references, races, buffered and merged streams, source Layers, and unbounded traversal. The validation report identifies which examples run.

Draft 0.2 added three rules. A primitive type can occur only inside a type, nominal, or error declaration. A brand of a record type was a nominal record. Two values of one numeric brand accept comparisons, addition, and subtraction.

Draft 0.59 added the data declaration. A data type was a nominal record with a runtime tag. A match selected data types and errors by name. A data type could be recursive when every cycle had a base case.

Draft 0.60 replaces the data declaration and Brand<T> with the nominal declaration. The form nominal Name = type declares a nominal type. The word type keeps the structural form. A nominal body that is a record gives a nominal record with a runtime tag. Any other body gives a nominal type with no runtime marker.

Draft 0.60 also removes unbrand. A nominal value fits its base type, so no call is needed. A base value still needs the constructor. Arithmetic on a nominal operand widens to the base type, except + and - on one numeric nominal type. Two different nominal types never mix in an operator. The IR version goes from 2 to 3.

Draft 0.61 adds sealed nominal types and pure function types. Optional project import rules restrict dependency direction. Open nominal types and unmarked function types keep their existing behavior. The IR keeps version 3 and adds an optional sealed flag.

Draft 0.62 infers the omitted fails and uses clauses of a non-exported function. A written clause closes its own set. A result annotation alone no longer closes the sets. Exported functions keep closed contracts. The IR field contract becomes written, with one flag for each of result, fails, and uses. The IR keeps version 3.

Draft 0.62 also accepts an alias of errors in a fails clause. Branches of different nominal records or errors join to their union. A sinkForEach callback and the method records of a source Layer take expected types from their context.

Draft 0.63 adds bare single-parameter arrows, lazy Option fallback with ??, flat record bindings inside blocks, and using/finally resource expressions. Existing spellings remain valid, and exported contracts remain explicit. The IR version is 4.

This site contains a specification, teaching examples, and a TypeScript model of typed intermediate representation, or IR. The site does not run the Effektive compiler; a Deno transpiler and CLI exist in the effektive-lang repository.

Rules in this reference describe intended language behavior. The examples are manually authored. Effect-TS tabs illustrate the intended transpiler output; a transpiler did not generate them.

Effektive files use the .eff extension. Earlier .lft files remain provisional artifacts from the design discussion.

The core covers data, functions, services, failures, pipelines, retry, providers, named modules, and the core 1 extensions. Later sections identify deferred features.

Effektive uses familiar TypeScript shapes but adds language forms. A TypeScript parser cannot parse Effektive source directly.

A first example

A first example: log through a service
type Message = string

error ConsoleUnavailable = { message: string }

service Console {
  print(message: Message): void fails ConsoleUnavailable
}

export const log = (message: Message): void
  fails ConsoleUnavailable
  uses Console.print => Console.print(message)
import { Context, Effect, Schema } from "effect"

type Message = string

class ConsoleUnavailable extends Schema.TaggedError<ConsoleUnavailable>()(
  "ConsoleUnavailable",
  { message: Schema.String }
) {}

class Console extends Context.Service<Console, {
  readonly print: (message: Message) => Effect.Effect<
    void,
    ConsoleUnavailable
  >
}>()("Console") {}

const log = (message: Message): Effect.Effect<
  void,
  ConsoleUnavailable,
  Console
> => Console.use((console) => console.print(message))

An error declaration creates a nominal tagged error type and a constructor. Nominal identity means the declaration determines which error the value represents.

A service declaration names external operations. Each service method is a capability, which is a statically tracked requirement for calling that operation.

The log function returns void, can fail with ConsoleUnavailable, and requires Console.print. Its call runs in the current source execution context.

Service declarations use method notation. Function values and function type annotations use arrows. Methods have no implicit receiver or this binding.

The source return type names the successful value. It does not name an Effect object. The target backend supplies its own execution representation.

Source and lexical rules

Source text uses UTF-8. Source positions in IR count UTF-16 code units, with an exclusive end offset.

Identifiers start with an ASCII letter, underscore, or dollar sign. Later characters may also contain decimal digits. Names are case-sensitive.

Line comments start with //. Block comments use /* and */ and do not nest. Comments do not produce runtime values.

Strings use double quotes with JSON escape sequences. String interpolation, single-quoted strings, and template literals are outside the initial lexical core.

Numeric literals use decimal notation, with optional fraction and exponent parts. Numeric literal values must be finite. Integral literals infer int unless context requires number. The null keyword denotes the singleton null data value.

The parser recognizes |> before |. The first token denotes a pipeline; the second separates type alternatives and effect-set members.

A newline or semicolon separates declarations, service methods, and block items. Commas separate record fields, arguments, parameters, and match arms.

A newline continues an incomplete expression. It also continues a signature before fails, uses, or its final arrow, and an expression before |>.

The core does not apply general JavaScript semicolon insertion. Parentheses make grouping explicit when a multiline form would otherwise be unclear.

Data and types

Source typeMeaning
stringA string value.
booleantrue or false.
intA finite JavaScript safe integer.
numberA host numeric value; source numeric literals must be finite.
nullThe singleton null data value.
voidThe unit result of an operation.
neverNo successful value, as with fail.
unknownA value that needs a checked boundary before typed use.
T[]An immutable array of T values.
[A, B]An immutable tuple with the stated positions.
Option<T>Some(value) or None.
A | BA union of alternatives.
Declarations: aliases, record, literal union, nominal type, and optional field
type UserId = string
type Name = string
type User = { id: string, name: string, active: boolean }
type Status = "pending" | "paid"
nominal OrderId = string
type Profile = { name: string, nickname?: string }
import { Schema } from "effect"

type UserId = string
type Name = string

const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  active: Schema.Boolean
})
type User = typeof User.Type

const Status = Schema.Literals(["pending", "paid"])
type Status = typeof Status.Type

const OrderId = Schema.String.pipe(Schema.brand("OrderId"))
type OrderId = typeof OrderId.Type

const Profile = Schema.Struct({
  name: Schema.String,
  nickname: Schema.optionalKey(Schema.String)
})
type Profile = typeof Profile.Type

Type aliases use structural compatibility. Nominal types and errors retain declaration identity. The checker accepts one self-recursive data alias under the core 1 array, optional-key and terminal-tagged guards described below. Unions beneath an established guard retain it. Unguarded direct, mutual, and deferred-type recursion remain invalid for aliases. A nominal record type can also be recursive, as described below.

A primitive type is string, int, number, boolean, or a literal type such as "paid", 1, or null. A primitive type can occur only inside the body of a type, nominal, or error declaration. All other positions use declared type names. These positions include parameters, results, service methods, constant annotations, local annotations, and function types.

void, never, and unknown are not primitive types for this rule. Option<T>, T[], tuples, records, unions, function types, Effect, Stream, and Sink stay valid in any position when their parts are declared types. The rule also applies to a host interface file (.effi), which declares the services and providers of a host. The checker reports declaration/013 for each primitive type in another position.

Draft 0.46 adds null as a singleton data literal and type. Declare type Nil = null to use the type in annotations outside type, nominal, and error declarations. Null remains a singleton during inference and emits JavaScript null. It fits nullable unions and does not fit nonnullable values or void. Equality with a singleton null operand accepts a nullable opposite operand in either order; it adds no flow narrowing. Finite literal matches can include null. Null is not a numeric value or a grouping key. The version 2 IR represents a null literal type or pattern directly and adds a null expression node. The older literal expression with value null remains the unit marker that emits undefined. Independent validation distinguishes these nodes without trusting their annotations.

An alias such as type Name = string satisfies the rule. An alias adds a name but not identity. A nominal declaration adds identity.

Record fields are required unless the field name has ?. Reading an optional field returns Option<T>.

A required field of type Option<T> differs from an optional field of type T. A future codec backend must preserve that difference.

Record construction rejects missing required fields, duplicate fields, and unknown fields. For a record literal with an expected union type, field names and known literal values select compatible members before field checking. This includes aliases of literal tag types. Members with the same field names can therefore use different tag and payload types. Field expressions still evaluate once in source order.

Record updates construct a new value. One spread may appear first, followed by replacement fields. The original record remains unchanged.

A bracket expression constructs an immutable list. An expected tuple type requires the exact element count and checks each element against its position. Otherwise an expected array type checks every element against its element type. Without an expected type, a nonempty homogeneous list infers an array after literal widening. An empty list or elements without a common array element type require a contextual type. A trailing comma is allowed.

List elements evaluate once from left to right. A failed element stops evaluation. Their active failures and requirements combine in the list expression. The typed IR retains whether the expression is an array or tuple; the Effect backend emits a JavaScript array for either shape.

A pure function that copies a record
const activate = (user: User): User =>
  ({ ...user, active: true })
const activate = (user: User): User =>
  ({ ...user, active: true })

OrderId("o1") constructs a nominal value when OrderId has base type string. A nominal declaration adds identity, not an automatic validation predicate.

An open nominal value fits its base type. An OrderId fits a string position with no call. A base value does not fit a nominal type, so a string needs the constructor OrderId(s). The rule works in one direction only. It also holds inside arrays, options, records, tuples, and unions.

A nominal declaration has the form nominal Name = type. The body can be any type that has a schema. A body that is a record type, after aliases unfold, makes a nominal record. Any other body makes a nominal type over that base. The word nominal is a declaration word only at the start of a declaration, so nominal stays a legal variable name. A host interface file (.effi) can declare a nominal type too.

A nominal record has a runtime _tag. The declaration nominal Account = AccountFields names its fields once, in the alias. A body can also be a record literal. A field can be optional, and an empty body is legal.

A body that names a nominal record, or a union that has a record or a nominal record as a member, gives declaration/014. Declare each record alone.

Account({ ... }) constructs the value. The argument can be any expression that fits the base record, and the result has type Account. A record literal without that call does not fit Account. An Account value does not fit another nominal type with equal fields.

A field read reads the field of the base record. A record update keeps the nominal type, so { ...account, active: false } has type Account. An Account value fits its base record, so no second field list is needed. The call Twin(account) builds another nominal record from it. The generated code drops a foreign _tag when the argument is not a record literal.

The field name _tag is reserved in a nominal record and an error declaration (declaration/009). The rule also applies when the body names an alias of a record. A nominal body and an error field cannot hold a function type or a deferred type, also through an alias (declaration/015). Deferred types include Effect, Stream, and Ref. A nominal body and an error field cannot hold an opaque type, which has no schema (declaration/016). unknown stays legal.

The operation record of withResource has function fields. So it is a record type and not a nominal type.

The encoded form of a nominal record includes _tag. JSON without the tag does not decode into a nominal record. To read such JSON, decode it into a record type, then call the nominal constructor.

A nominal record can refer to itself, and nominal records can refer to each other. An alias can be part of the cycle. Every cycle needs a nominal record and a base case. The base case is a path through an Option, an array, an optional field, or a union that has a member outside the cycle. A cycle of required fields only gives declaration/007. A cycle of aliases only, a cycle through an error, and a cycle through a nominal type over a non-record body keep their rules.

A nominal record: construction, update, field read, and widening
type AccountFields = { id: UserId, name: Name, active: boolean }
nominal Account = AccountFields

const openAccount = (id: UserId, name: Name): Account =>
  Account({ id, name, active: true })

const closeAccount = (account: Account): Account =>
  ({ ...account, active: false })

const accountName = (account: Account): Name => account.name

const accountFields = (account: Account): AccountFields => account
import { Schema } from "effect"

const AccountFields = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  active: Schema.Boolean
})
type AccountFields = typeof AccountFields.Type

const Account = Schema.TaggedStruct("Account", {
  id: Schema.String,
  name: Schema.String,
  active: Schema.Boolean
})
type Account = typeof Account.Type

const openAccount = (id: UserId, name: Name): Account =>
  Account.make({ id, name, active: true })

const closeAccount = (account: Account): Account =>
  ({ ...account, active: false })

const accountName = (account: Account): Name => account.name

const accountFields = (account: Account): AccountFields => account

The declaration sealed nominal Name = T gives its module construction authority. Other modules can pass values, compare primitive values, and read record fields. Other modules cannot construct values, retain the type through record updates, perform arithmetic, or widen values to the base type. The declaring module can export explicit conversions.

Only the declaring module can derive a schema whose decoded type contains that sealed type. Other modules cannot decode or construct values through that schema. This restriction includes nested positions. Encoding remains legal. The owner can export an explicit decoder. Host interface files reject sealed declarations.

A Schema constructor cannot pass a foreign sealed decoded value into a callback. This restriction holds when the final decoded result is unsealed. The owner can export the complete transformer or an explicit decoder.

Codec construction also derives schemas and requires the same authority. Calls through owner-exported Codecs remain legal.

The domain owns construction and arithmetic
export type Int = int
export sealed nominal Positive = Int

export const parsePositive = (value: Int): Option<Positive> =>
  if (value > 0) { Some(Positive(value)) } else { None }

export const double = (value: Positive): Positive => value + value
export const toInt = (value: Positive): Int => value
import { Option, Schema } from "effect"

export type Int = number
export const Positive = Schema.Finite.check(
  Schema.makeFilter(
    (value) => Number.isSafeInteger(value) || "expected a safe integer"
  )
).pipe(Schema.brand("domain#Positive"))
export type Positive = typeof Positive.Type

export const parsePositive = (value: Int): Option.Option<Positive> =>
  value > 0 ? Option.some(Positive.make(value)) : Option.none()
export const double = (value: Positive): Positive => Positive.make(value + value)
export const toInt = (value: Positive): Int => value

The complete example adds a consumer and import rules. Its three runtime checks exercise the generated modules. Sealing constrains Effektive source; generated TypeScript remains a trusted boundary.

The checker treats int as a subtype of number. Division returns number. An int result outside the safe range is a runtime defect.

The following arithmetic rules apply to open nominal types and to sealed types inside their declaring module.

A numeric nominal type has the base type int or number, after aliases. The operators + and - accept two values of one numeric nominal type and produce that type. An int result outside the safe range is a defect, as for int.

Other arithmetic widens each nominal operand to its base type. So total * 2 has type int, and total / 2 has type number. A string nominal operand of + widens to string. A result of the nominal type needs the constructor, as in Cents(total * 2).

Two different nominal types never mix in an operator. The expression cents + meters is an error. A comparison needs compatible operands: one nominal type, or a nominal type and a type that it widens to. So id === "u1" and total > 0 are legal. The expressions userId === email and cents < meters are errors. The operators <, <=, >, and >= also need numeric operands.

Operators on a numeric nominal type
nominal Cents = int
type Exceeded = boolean
type Share = number
type Count = int

const sum = (a: Cents, b: Cents): Cents => a + b
const exceeds = (total: Cents, limit: Cents): Exceeded => total > limit
const positive = (total: Cents): Exceeded => total > 0
const scaled = (total: Cents, factor: Count): Cents => Cents(total * factor)
const half = (total: Cents): Share => total / 2
import { Schema } from "effect"

const Int = Schema.Finite.check(
  Schema.makeFilter(
    (value) => Number.isSafeInteger(value) || "expected a safe integer"
  )
)

const Cents = Int.pipe(Schema.brand("Cents"))
type Cents = typeof Cents.Type
type Exceeded = boolean
type Share = number
type Count = number

const sum = (a: Cents, b: Cents): Cents => Cents.make(a + b)
const exceeds = (total: Cents, limit: Cents): Exceeded => total > limit
const positive = (total: Cents): Exceeded => total > 0
const scaled = (total: Cents, factor: Count): Cents =>
  Cents.make(total * factor)
const half = (total: Cents): Share => total / 2

Equality compares compatible primitives, literal unions, and primitive nominal types. Comparing records, nominal records, or errors requires a named comparison function.

Conditions require boolean. Effektive does not use JavaScript truthiness. void cannot appear in a record field, array, or tuple.

Effect<A, E, R> is an immutable computation value. A is its successful result type. E is never or a union of nominal error names. R is never or a union of qualified service methods. Omitted E and R mean never. These channels describe deferred execution, not the effects of evaluating an expression that produces the value. Effect values may be bound, passed, and returned.

Effect value compatibility checks the successful result covariantly and permits source failure and requirement sets that are subsets of the expected sets. Nominal error and method identities are retained. A value with more possible failures or requirements cannot fit a narrower declared Effect type.

Functions and blocks

Typed functions and a deferred job
type Count = int

const add = (a: Count, b: Count): Count => a + b
const increment = (value: Count): Count => value + 1
const job = () => log("ready")
type Count = number

const add = (a: Count, b: Count): Count => a + b
const increment = (value: Count): Count => value + 1
const job = () => log("ready")

Every binding and aggregate value is immutable. Effektive has no assignment operator, mutable let binding, implicit receiver, or source class declaration.

Parameters and calls use ordinary argument lists. Effektive does not insert currying, partial application, or callback execution.

An arrow with one expression uses that expression as its result. A block uses its final expression as its result.

A block that ends with a binding, or contains no items, produces void. A final void operation also produces void.

Effektive has no return statement in version 0. Function blocks and other expression blocks use the same result rule.

A brace after an arrow begins a block. Parentheses distinguish a returned record, as in ({ ...user, active: true }).

A single unannotated parameter can omit parentheses: user => user.name. Annotations, multiple parameters, and written result contracts require parentheses. Context supplies the parameter type.

Inside a block, const { id, revision: expectedRevision } = article evaluates article once and reads fields in written order. A whole-subject annotation may follow the closing brace. Bindings must be flat and nonempty; duplicate fields or names, nesting, defaults, rest, tuples, and module-level destructuring are rejected.

Record bindings use ordinary field rules, including optional Option fields, nominal records, common union fields, own-field reads, and borrow restrictions. Renaming a shorthand local expands it to a field label and a new local name.

Contextual arrows, record bindings, and Option fallback
type Count = int
type Entry = { value: Count, alternative: Option<Count> }

export const totals = (entries: Entry[]): Count[] =>
  entries |> map(entry => {
    const { value, alternative } = entry
    value + (alternative ?? 0)
  })
import { Option } from "effect"

type Count = number
type Entry = { readonly value: Count; readonly alternative: Option.Option<Count> }

export const totals = (entries: ReadonlyArray<Entry>): ReadonlyArray<Count> =>
  entries.map(entry => {
    const { value, alternative } = entry
    return value + Option.getOrElse(alternative, () => 0)
  })

option ?? fallback returns a Some payload and evaluates fallback only for None. The subject runs once. Aliases, contextual types, branch joins, failures, and requirements follow ordinary match rules. Null and typed failures are not Options.

The ?? operator associates right, below logical operators and above pipelines. Mixing it with && or || requires parentheses around the logical operand.

using (job = openJob()) { job.execute() } finally { job.close() } returns the use-block result and requires a void finalizer. It has the same resource contract as withResource with three inline callbacks. Acquisition also accepts a block. The finalizer is mandatory and runs exactly once after successful acquisition, including failure, defect, and interruption.

The resource name is visible only in use and finalization. Use-block locals are unavailable to finalization. Borrow, capture, data-only result, failure, and requirement rules remain unchanged. Each region is an implicit callback; nested resources cannot capture an outer borrowed handle. Ordinary calls named using remain valid.

Creating an arrow evaluates no part of its body. Calling the arrow runs the body again. Captured immutable values retain ordinary lexical scope.

Exported functions require parameter and result annotations. Each annotation follows the rule on primitive types in Data and types. Local parameters require annotations unless a contextual function type supplies their types.

On a non-exported function, the compiler infers each omitted result type, fails clause, and uses clause from the body. This rule covers module-level functions and inline arrows. A written fails or uses clause closes its own set. A result annotation alone does not close the sets.

An exported function has a closed contract. Omitted fails means no expected failures. Omitted uses means no unprovided capability requirements.

A function type states its sets, so a function type in a binding annotation is closed. Omitted clauses in a function type mean empty sets.

Those omissions do not assert pure execution. The checker tracks execution mode separately, including effects whose providers are already supplied.

An unmarked function type defaults to effect execution. Imported interfaces retain their resolved execution modes.

A function type can declare pure execution with pure (value: T) => U. The contract requires empty failure and requirement sets. Inferred pure functions, imported pure functions, and pure inline callbacks can satisfy that contract. An effectful function cannot satisfy it, even when its effect sets are empty.

A parameter requires a pure callback
type Count = int

const apply = (values: Count[], callback: pure (value: Count) => Count): Count[] =>
  map(values, callback)
const increment = (value: Count): Count => value + 1
const updated = apply([1, 2], increment)
type Count = number

const apply = (
  values: ReadonlyArray<Count>,
  callback: (value: Count) => Count
): ReadonlyArray<Count> => values.map(callback)
const increment = (value: Count): Count => value + 1
const updated = apply([1, 2], increment)

An unmarked function type keeps the effect default. Direct pure-to-effect lifting keeps its existing rules. Executing run or provide remains effectful after all requirements disappear.

Services and contracts

Errors, a service, and a function that uses the service
error UserMissing = { id: string }
error StoreUnavailable = { message: string }

service Users {
  find(id: UserId): Option<User> fails StoreUnavailable
  save(user: User): void fails StoreUnavailable
  all(): User[] fails StoreUnavailable
}

export const getUser = (id: UserId): User
  fails UserMissing | StoreUnavailable
  uses Users.find =>
  match Users.find(id) {
    Some(user) => user,
    None => fail(UserMissing({ id }))
  }
import { Context, Effect, Option, Schema } from "effect"

class UserMissing extends Schema.TaggedError<UserMissing>()(
  "UserMissing",
  { id: Schema.String }
) {}

class StoreUnavailable extends Schema.TaggedError<StoreUnavailable>()(
  "StoreUnavailable",
  { message: Schema.String }
) {}

class Users extends Context.Service<Users, {
  readonly find: (id: UserId) => Effect.Effect<
    Option.Option<User>,
    StoreUnavailable
  >
  readonly save: (user: User) => Effect.Effect<
    void,
    StoreUnavailable
  >
  readonly all: () => Effect.Effect<
    ReadonlyArray<User>,
    StoreUnavailable
  >
}>()("Users") {}

export const getUser = (id: UserId): Effect.Effect<
  User,
  UserMissing | StoreUnavailable,
  Users
> =>
  Effect.flatMap(
    Users.use((users) => users.find(id)),
    (found) => Option.match(found, {
      onNone: () => Effect.fail(new UserMissing({ id })),
      onSome: (user) => Effect.succeed(user)
    })
  )

fails names a closed set of expected error identities. uses names a closed set of required service methods.

A fails clause can name a type alias whose body is an error or a union of errors, such as type PayFailure = OrderNotFound | PaymentDeclined. An alias of aliases also works. The checker expands the alias to error identities, so the IR keeps sets of errors. A member that is not an error gives failure-contract/004. A catch arm still names one error.

A whole service name expands to all methods in that service. Method requirements retain resolved identities across module boundaries.

A service method declaration has no uses clause. Its own invocation adds that method capability to the caller requirements.

A host provider captures implementation dependencies. The method interface describes only parameters, success, and expected failures.

The checker verifies that inferred body failures and requirements fit the declared sets. A body cannot silently widen an explicit contract.

A declared set may be wider than the body needs. Calls through that interface propagate the declared sets.

Error constructors produce tagged data. An error constructor takes one record literal argument; a computed record value or a spread-only record is rejected. The field name _tag is reserved for generated error tags.

The generated code checks nominal record values that a host provider returns. When a service method result contains a nominal record, each module wraps that provider once. Each result, and each element of a result Stream, goes through Schema.is of its type. An optional nominal record field can have the name of an Object.prototype member, such as toString. Then the check first copies the value without a prototype. A wrong value causes a defect that names the service and the method. A provider without nominal record results is not wrapped.

These positions stay trusted: Sink, Effect values, functions, Ref, fibers, opaque types, and typed errors. Arguments that the host passes to source callbacks stay trusted too.

Static capability tracking does not itself prompt for permission or sandbox a host adapter. The host supplies implementations that honor the declared contracts.

Execution order and mode

Ordinary calls evaluate the callee once, then arguments from left to right, then invoke the function. Fields and bindings evaluate from left to right.

A record update evaluates its base once before replacement fields. Only the selected if, match, or short-circuit branch executes.

The checker combines effects from all statically reachable paths. Source execution is sequential except for explicit concurrent operations. The traverse concurrency option states its limit and cancellation behavior.

Each expression has an execution mode: pure or effect. Each function type records the mode that applies when the function runs.

Nonempty failure or requirement sets require effect mode. Empty sets permit either mode. Closing a requirement set through provision does not remove execution effects.

A closure expression is pure even when its function type describes effectful invocation. Module initialization checks the expression mode, not the latent function mode.

Mode propagation uses logical OR over callee evaluation, arguments, invoked functions, and reachable branches.

An explicit contract with nonempty sets lifts a pure body into an effectful function interface. A pure function may lift into an effectful callback interface.

An effectful function cannot satisfy a pure callback requirement. Lifting wraps execution; it does not invoke a callback during construction.

A recursive function requires parameter and result annotations but no fails or uses clause. Only module-level functions may be recursive: a function bound inside a block cannot refer to itself. Inference starts pure, with empty effect sets. It propagates intrinsic effects and effectful calls until the recursive group reaches a fixed point. A group that has not settled after 32 rounds gives execution-mode/004.

defer(job) accepts a zero-argument function and constructs an Effect value from its invocation result, failure set, and requirement set. The job body does not run during construction. Evaluating the job expression may contribute its own active effects. run(value) accepts an Effect value and executes its deferred computation anew on every call. Its active effects combine evaluation of value with the value’s deferred failures and requirements. run has effect execution mode even when those sets are empty.

Expected failure stops the current normal path. Defects and cancellation use separate runtime outcomes and do not enter ordinary typed recovery.

Pipelines and collections

Collection pipelines with filter, map, and fold
type Name = string
type Amount = int

type User = { name: string, active: boolean }

export const activeNames = (users: User[]): Name[] =>
  users
    |> filter(user => user.active)
    |> map(user => user.name)

export const total = (amounts: Amount[]): Amount =>
  amounts |> fold(0, (sum, amount) => sum + amount)
import { Schema } from "effect"

type Name = string
type Amount = number

const User = Schema.Struct({
  name: Schema.String,
  active: Schema.Boolean
})
type User = typeof User.Type

const activeNames = (
  users: ReadonlyArray<User>
): ReadonlyArray<Name> =>
  users.filter((user) => user.active).map((user) => user.name)

const total = (amounts: ReadonlyArray<Amount>): Amount =>
  amounts.reduce((sum, amount) => sum + amount, 0)

A pipeline passes its left result as the first argument of the next stage. The stages associate from left to right.

The pipe syntax with and without arguments
value |> transform
value |> transform(a, b)
transform(value)
transform(value, a, b)

The first form invokes transform(value). The second invokes transform(value, a, b). Neither form creates a partially applied function.

Each stage first evaluates the left value once. It then evaluates the resolved callee and explicit arguments, and finally invokes the stage.

A temporary binding preserves this order during lowering. Direct textual substitution into an ordinary call could change the callee evaluation order.

Stage names may resolve to local, imported, or builtin functions. Dotted stages must identify declared service methods.

Version 0 excludes inline lambda stages, placeholders, spread arguments, and runtime record-member stages.

The pipeline operator has lower precedence than arithmetic, comparisons, and boolean operators. Parentheses may override grouping.

The prelude supplies map, filter, fold, and traverse. The callbacks of map, filter, and fold require pure execution mode and empty failure and requirement sets. The callback of traverse may be pure or effectful.

map transforms each element. filter selects elements through a boolean predicate. Both visit elements once from left to right and preserve order.

fold(items, initial, callback) invokes callback(state, item) from left to right. Its result retains the accumulator type. An empty array returns initial.

Empty arrays produce empty results for map and filter. A defect stops traversal.

traverse(items, callback) invokes the callback once per item from left to right and returns an array in input order. It is effectful even when the callback is pure. Its active failure and requirement sets include those of callback invocation. An empty array invokes no callbacks. Without options, the Effect backend uses sequential Effect.forEach, and a typed callback failure prevents later callbacks from starting. An optional third argument must be the literal record { concurrency: N } or { concurrency: "unbounded" }, where N is a positive safe integer. A numeric bound runs at most N callbacks simultaneously; "unbounded" starts every callback without a concurrency limit. Both modes return results in input order. A typed failure starts no queued callbacks in bounded mode, interrupts active siblings in either mode, and waits for their scoped finalizers; explicit interruption also waits for finalizers. The backend passes the literal concurrency option to Effect.forEach. A pure callback is lifted into an Effect callback. Dynamic concurrency values and other option fields are rejected.

all(effects, options?) executes a tuple, record, or array of Effect values and returns the same shape filled with their results. It unions their typed failures and service requirements. The default is sequential. An optional literal { concurrency: N } uses a positive safe integer limit; { concurrency: "unbounded" } has no limit. Concurrent success results retain input positions and record keys. A typed failure interrupts active siblings and waits for their finalizers. A heterogeneous bracket literal needs a contextual tuple type before passing to all. Other option fields and dynamic concurrency values are rejected. The backend uses Effect.all. A source binding named all shadows the prelude operation.

partitionAll(jobs, options?) accepts a homogeneous array of Effect<A,E,R> values. It runs every job and returns [A[], E[]], with successes and typed errors in separate input-order arrays. The operation has no typed failure channel and requires the jobs' combined services. A job defect or interruption still terminates the collection. The default is sequential; the optional concurrency record has the same literal rule as all. The backend uses Effect.partition. A source binding named partitionAll shadows the prelude operation.

validateAll(jobs, onErrors, options?) accepts a homogeneous array of Effect<A,E,R> values and a pure one-argument callback from E[] to a nominal error B. It evaluates every job. If all succeed, it returns A[] in input order. If any job fails, it discards successes and fails with the callback's B value; the callback receives every typed error in input order, and its array is nonempty at runtime. The source type remains E[] because core 1 has no nonempty-array type. The operation requires R and fails only with B; defects and interruption still terminate it. The default is sequential, and the optional literal concurrency record follows all. The backend uses Effect.validate followed by Effect.mapError. A source binding named validateAll shadows the prelude operation.

allResults(effects, options?) accepts an Effect tuple, record, or homogeneous array. It runs every member and preserves the input shape, position, or key. Each result is the tagged record union { _tag: "Success", success: A } | { _tag: "Failure", failure: E } for that member's A and E; an error in one member does not stop the others. The collection has no typed failure channel and requires the members' combined services. Defects and interruption still terminate it. The default is sequential; the optional concurrency record follows all. The backend uses Effect.all with mode: "result". A source binding named allResults shadows the prelude operation.

Stream<A,E,R> is a deferred producer and Sink<I,O,E,R,L> is a deferred consumer, with omitted E, R, and L defaulting to never. L is the leftover output type and follows all existing arguments. Their failure and requirement sets describe execution, not construction. unfold(seed, callback) takes an inline arrow returning Option<[A, State]>; None ends the stream. mapEffect(stream, callback) takes an inline effectful arrow and combines deferred channels. sinkForEach(callback) takes an inline effectful arrow and produces void. Its input needs an annotation, except when the call is the whole sink argument of runStream, transduceStream, aggregateStream, or aggregateWithinStream. Then the input takes the stream element type. A sink bound to a name keeps the annotation. These operations use direct call syntax. runStream(stream, sink) activates both values and combines their channels.

Default sequential stream operations use one-element pull demand without prefetch. The producer does not run until terminal consumption starts. The next pull follows consumer completion. Concurrent mapEffect and flatMapStreams may pull ahead within the target runtime’s buffering policy. A producer, transform, or consumer failure stops demand and waits for scoped finalizers; interruption also waits. The backend uses Stream.unfold, Stream.mapEffect, Sink.forEach, and Stream.run.

An unmarked callback function type defaults to effect mode. It cannot satisfy a pure callback requirement. Empty effect sets do not change that default. The pure modifier declares a pure function type explicitly.

The transpiler specializes builtin rules before IR serialization. Each distinct closed builtin type receives a unique synthetic ValueId and a closed prelude interface.

Pattern matching

Match on an option and choose with if
type Label = string

const label = (found: Option<User>): Label =>
  match found {
    Some(user) => user.name,
    None => "missing"
  }

const category = (user: User): Label =>
  if (user.active) { "active" } else { "inactive" }
import { Option } from "effect"

type Label = string

const label = (found: Option.Option<User>): Label =>
  Option.match(found, {
    onNone: () => "missing",
    onSome: (user) => user.name
  })

const category = (user: User): Label =>
  user.active ? "active" : "inactive"

if requires a boolean condition and an else branch. Branch results require a common result type. never fits every successful result type.

Without an expected type, branches of different nominal records or errors join to their union. This rule covers if, match, and try. Other types, such as structural records, form no union and need an annotation.

Match subjects evaluate once. The checker requires complete coverage and rejects unreachable arms and constructors that do not belong to the subject type.

Supported patterns include literals, _, Some(binding), None, error and nominal record bindings, and a string discriminator in a tagged record.

When the last arm of a match is not _, the generated code tests that arm too. Only unsound host code can supply a value outside the subject type. Such a value causes a defect with the message "a match received a value outside its type". When the last arm is _, that arm receives the value.

Matching an unbounded primitive domain requires a final wildcard. A record match also requires a final wildcard when a member lacks the selected tag field or makes that field optional. An open-ended tag also requires the wildcard. Enumerating the known tags does not cover those members. Nested patterns are outside version 0.

Match on a tagged record union
type Shape = { kind: "circle", radius: number } | { kind: "square", side: number }
type Length = number

const extent = (shape: Shape): Length =>
  match shape {
    { kind: "circle", ...circle } => circle.radius,
    { kind: "square", ...square } => square.side
  }
import { Schema } from "effect"

const Circle = Schema.Struct({
  kind: Schema.Literal("circle"),
  radius: Schema.Number
})

const Square = Schema.Struct({
  kind: Schema.Literal("square"),
  side: Schema.Number
})

const Shape = Schema.Union([Circle, Square])
type Shape = typeof Shape.Type
type Length = number

const extent = (shape: Shape): Length => {
  switch (shape.kind) {
    case "circle":
      return shape.radius
    case "square":
      return shape.side
  }
}

A spread binding in this pattern captures the whole narrowed record. It does not mean JavaScript rest destructuring. The binding retains every union member whose tag can equal the pattern at runtime, including overlapping open or optional fields. Primitive nominal types use their erased base values for this comparison, and an unknown tag may match any literal. These matching rules do not change nominal assignment or make unknown a top type.

Construct a tagged member and match an open record union
type Count = int
type Text = string
type Reading = { kind: "count", value: Count } | { kind: "text", value: Text }

const reading: Reading = { kind: "text", value: "ready" }

type Incoming = Reading | { kind: Text, value: Text } | { other: Text }

const read = (input: Incoming): Count | Text =>
  match input {
    { kind: "count", ...record } => record.value,
    _ => "unclassified"
  }
type Count = number
type Text = string
type Reading = { readonly kind: "count", readonly value: Count }
  | { readonly kind: "text", readonly value: Text }

const reading: Reading = { kind: "text", value: "ready" }

type Incoming = Reading
  | { readonly kind: Text, readonly value: Text }
  | { readonly other: Text }

const read = (input: Incoming): Count | Text =>
  "kind" in input && input.kind === "count"
    ? input.value
    : "unclassified"

The count arm can receive a record from the open Text-tagged member, whose value is Text. Its result is therefore Count | Text. The wildcard handles other tags and the member without kind.

A match over nominal records and errors selects an arm by name. Every member of the subject, after aliases, must be a nominal record or an error. The arm Circle(circle) binds a value of type Circle. Coverage needs one arm for each member, or a final wildcard. A name that is not a member of the subject is an error.

A record pattern on such a subject gives pattern/004. A subject that mixes nominal record or error types with other types also gives pattern/004. A constructor pattern that names a type alias or a nominal type with a non-record body gives pattern/004. A catch handler names an error only.

Match by name on nominal records and errors
type Length = number
type Label = string

nominal Circle = { radius: number }
nominal Square = { side: number }
error Missing = { reason: string }
type Shape = Circle | Square

const extent = (shape: Shape): Length =>
  match shape {
    Circle(circle) => circle.radius,
    Square(square) => square.side
  }

const fallback = (shape: Shape): Length =>
  match shape {
    Circle(circle) => circle.radius,
    _ => 0
  }

const label = (value: Circle | Missing): Label =>
  match value {
    Circle(circle) => "circle",
    Missing(missing) => missing.reason
  }
import { Schema } from "effect"

type Length = number
type Label = string

const Circle = Schema.TaggedStruct("Circle", { radius: Schema.Number })
type Circle = typeof Circle.Type

const Square = Schema.TaggedStruct("Square", { side: Schema.Number })
type Square = typeof Square.Type

class Missing extends Schema.TaggedError<Missing>()(
  "Missing",
  { reason: Schema.String }
) {}

const Shape = Schema.Union([Circle, Square])
type Shape = typeof Shape.Type

const extent = (shape: Shape): Length =>
  shape._tag === "Circle" ? shape.radius : shape.side

const fallback = (shape: Shape): Length =>
  shape._tag === "Circle" ? shape.radius : 0

const label = (value: Circle | Missing): Label =>
  value._tag === "Circle" ? "circle" : value.reason

A tagged pattern without a binding lowers with a null binding. Pattern-bound values remain immutable and local to their arm.

Failure and recovery

Recover from a typed failure with try and catch
export const displayName = (id: UserId): Name
  fails StoreUnavailable
  uses Users.find =>
  try {
    getUser(id).name
  } catch {
    UserMissing(error) => "Guest"
  }
export const displayName = (id: UserId): Effect.Effect<
  Name,
  StoreUnavailable,
  Users
> => Effect.map(getUser(id), (user) => user.name).pipe(
  Effect.catchTag("UserMissing", () => Effect.succeed("Guest"))
)

fail(error) produces no normal value. Its success type is never, and its failure set includes the nominal error identity.

catch installs handlers before evaluating the protected block. It removes handled tags and combines the handlers failures and requirements.

Successful and handled paths require a common result type, or they join as nominal records or errors. Unhandled tags continue to the caller.

The checker rejects duplicate handlers and handlers for tags absent from the protected failure set. It does not treat defects as typed failures.

A backend may use Result data or a typed failure channel. It must not implement domain failures as untyped JavaScript exceptions.

Retry and providers

This fragment assumes the payment module defines pay, id has type OrderId, and the host supplies AppLive. The fragment belongs inside a function.

Deferred control, using the payment example from the mini book
const paymentJob = () => pay(id)
retry(paymentJob, { times: 2 })
provide(AppLive, () => pay(id))
const paymentJob = () => pay(id)

Effect.retry(Effect.suspend(paymentJob), { times: 2 })
Effect.provide(Effect.suspend(() => pay(id)), AppLive)

These control functions require a zero-argument function. They receive work that has not run yet. Ordinary expected parameter types never insert suspension.

retry invokes its callback for the first attempt and again after each permitted typed failure. times counts retries after the first attempt.

Version 0 requires a finite nonnegative safe integer literal for times. Defects and cancellation do not trigger typed retry.

Retry retains the callback result type, failures, and requirements. The author must choose operations that tolerate repeated execution.

A provider value lists supplied capabilities, acquisition mode, acquisition failures, and acquisition dependencies. Host modules supply provider values in version 0.

provide installs the provider while invoking its callback. Provider acquisition runs before the callback and may contribute additional failures and requirements.

Provision removes supplied callback requirements and adds acquisition dependencies. It preserves typed failures from both acquisition and callback execution.

Provider lookup occurs at invocation time. Creating a closure does not freeze the current provider environment.

retry, provide, fail, and catch have effect execution mode in the core. A provider with empty acquisition sets can still require effectful acquisition.

withResource(acquire, use, release) takes three inline closures. Acquisition has no parameters and returns an operation record with required function fields. Use and release each take that record as one parameter. The use result and operation arguments and results are data only: recursively no function, Effect value, provider, or unknown. Release returns void. A scoped parameter may occur only as the direct receiver of an operation call, such as file.read(); it cannot escape through a return, alias, aggregate, nested closure, or host argument. A field cannot be read as a value. Invocation failures and requirements of all three callbacks combine; execution is effectful even with empty sets. Release runs after success, failure, defect, or interruption, and its own typed failure remains in the result channel. Host adapters create operation records and remain responsible for their functions' behavior. Core 1 defines general borrowed handles, source Layers, races, and scoped fibers below.

Modules and boundaries

Import a function and provide a provider
import { displayName, Name, StoreUnavailable } from "./users.eff"
import { UsersLive } from "host:providers"

export const main = (): Name
  fails StoreUnavailable =>
  provide(UsersLive, () => displayName("u1"))
import { UsersLive } from "./users-live.js"
import { displayName, Name, StoreUnavailable } from "./users.js"

const main = (): Effect.Effect<Name, StoreUnavailable> =>
  Effect.provide(
    Effect.suspend(() => displayName("u1")),
    UsersLive
  )

This boundary imports the supplied users module. UsersLive supplies Users with no acquisition failure or additional dependency.

Imports select named exports from relative files or registered host modules. Version 0 excludes default imports, namespaces, renaming, re-exports, and package resolution.

The compiler emits a companion module for each host interface file that a module imports: host.effi gives host.effi.ts. The companion module holds the types, errors, nominal types, and services of the interface, and exports each one. Each tag keeps the module name of the interface, as in host#Token. A source import of these declarations reads the companion module, so source code can construct an interface nominal record or error. The host module imports these declarations from the companion module and supplies only the opaque types and the providers.

Prefix export exposes a declaration by name. Interfaces also carry the complete supporting type graph, without importing private names into the local source scope.

The checker resolves declaration identities before checking bodies. Identical printed names from separate modules never share identity merely because their spellings match.

Module initialization may construct immutable data and closures. It cannot run service operations or other effectful initializers.

The optional imports array in effektive.json restricts direct imports. Each rule has a from pattern and an allow array. Patterns match normalized module names without file extensions. A standalone * matches one path segment, and ** matches zero or more segments. Wildcards occupy a complete segment.

The first matching source rule controls the import. An unmatched module remains unrestricted. Host targets match exact host: specifiers or host:*. A forbidden edge reports module/005 on the import specifier. The policy does not select a platform or enforce runtime authority.

The linker rejects module dependency cycles. Application execution begins when the host invokes an exported entry function with the required providers.

A host registry supplies interfaces for host: modules. This registry is host configuration, not arbitrary JavaScript import access inside Effektive.

Host adapters must validate boundary values and preserve immutable source data. Registered implementations must honor their declared errors, capabilities, and execution behavior.

Typed IR and backends

Transpiler design
Effektive source
  -> parse with source positions
  -> resolve declaration identities
  -> check types, contracts, patterns, and execution modes
  -> specialize builtins and lower pipelines
  -> validate typed IR
  -> emit Effect-TS code

The accompanying typed-ir.ts describes the current compiler payload model. The independent validator lives in the effektive-lang repository.

Core 1 requires light-functional-ir version 4. The compiler, validator, and linked model use version 4. Earlier versions are rejected without conversion. Effektive branding does not change the format marker.

Draft 0.62 replaces the function field contract with written. The field holds one flag each for result, fails, and uses. The version stays 3.

Draft 0.63 exports ClosureExpr and makes closure parameters own scoped identities. Resource acquisition, use, release, fiber use, and grouped-stream callbacks require closures. Read identities from use.parameters[0].id, release.parameters[0].id, and grouped use.parameters[1].id. The duplicate useBinding, releaseBinding, and groupBinding fields are removed. Runtime validation retains closure, arity, type, reference, and borrow checks.

Bare arrows keep the existing arrow tree. Option fallback lowers to match; record bindings lower to block bindings and field reads. using/finally lowers to with-resource with separate use and release parameters. No new runtime operations are needed.

The frontend package entry point provides checking, IR, and source editor queries without importing the TypeScript backend. A TypeScriptOutputs instance owns lazy text, maps, and span tables for one program. Source hover does not lower or emit; combined hover explicitly receives outputs.

Every expression records its type, active failure and requirement sets, execution mode, and source span. Function types retain invocation effects and mode.

References use resolved identities. Service operations use perform nodes; ordinary function invocations use call nodes.

Pipelines lower through fresh block bindings and existing calls. A generated binding uses the left expression span; calls retain the corresponding stage span.

Diagnostics refer to source expressions and hide temporary names. Source positions count UTF-16 units, with the end position excluded.

The compiler prints TypeScript with one fixed style and a target width of 80 columns. Formatting checks cover long identifiers in type arguments, fields, function declarations, member accesses, service bindings, and parameter annotations. An unbroken identifier can exceed the target width. The emitter records source mappings during layout.

Project validation binds each IR module to its lowered name. Imported declarations cannot claim the consumer module's identity. Supporting types can belong to other modules. The validator checks authority against provided declaration metadata. It does not compare imported metadata with separate producer IR.

The IR retains explicit retry, provide, catch, and nominal operations. A nominal definition can record sealed: true; an absent flag means open. A nominal construction is a nominal node, and a nominal pattern is a nominal pattern kind. A nominal definition has the kind nominal and a base type. A nominal value needs no node to read its base value. A backend must not replace deferred control with eager helper calls.

Pure exports return ordinary target values. Effectful exports return target computations, even when their error and requirement sets are both empty.

The Effect-TS backend uses Effect.Effect<A, E, R>, Context services, and typed failures. Its R parameter groups requirements by service rather than individual method.

The backend preserves method precision in checked IR or metadata. A coarser target type does not erase the source contract.

A backend must encode resolved declaration identities in error tags, nominal record tags, service keys, and brand tokens. Equal printed names from different modules must remain distinct.

Records, nominal types, and errors become Schema declarations. A nominal record becomes Schema.TaggedStruct, and its tag comes from its resolved identity. Any other nominal type becomes a Schema brand. Options become Option values. Providers become Layer values.

An Effect value maps to Effect.Effect<A, E, Rservice>, where Rservice is the union of service classes containing the required methods; checked IR keeps the precise method-level requirements. defer maps to Effect.suspend for an effectful callback, or a suspended pure-to-effect lift for a pure callback. run sequences the stored Effect value in the enclosing effect context. Neither operation supplies a provider.

The IR has distinct unfold, map-effect, sink-for-each, and run-stream expressions. Stream and Sink types carry deferred channels; run-stream activates them. The IR has a distinct with-resource expression carrying the three closures and the scoped parameter identities. Independent validation checks the operation-record shape, data-only types, scoped references, and invocation channels. The Effect-TS backend maps it to Effect.acquireUseRelease with suspended acquisition and Effect-returning use and release callbacks.

The backend targets the effect core modules and selected modules from the Effect ecosystem. The table names each module and its role.

Effect-TS moduleRole in transpiled code
effect: Effect, Context, Layer, Option, Schema, Stream, SinkComputations, services, providers, options, records, errors, brands, and incremental streams and sinks. The compiler emits these modules.
effect/http, FileSystem, and @effect/platform-node, -bun, or -browserHTTP and file services, and the runtime for each host.
effect/sql and a driver such as @effect/sql-pgDatabase services.
@effect/opentelemetryExport of traces, metrics, and logs.

Core 0 specifies the existing mapping. Core 1 specifies one file service through a typed host adapter. HTTP, SQL, and telemetry adapters need separate specifications.

The manual log fixture retains its original log.lft source identifier. Its semantic payload illustrates the same method declaration now shown in log.eff.

Equations and validation

F(expression) denotes active expected failures. R(expression) denotes active capability requirements. Both sets contain resolved identities.

Checking equations
closure construction: F = {}, R = {}, execution = pure
effect value construction: active sets from evaluating the job expression only;
                           deferred F and R come from job invocation
run effect value:         value evaluation sets union deferred F and R;
                           execution = effect
withResource:             callback construction sets union acquisition, use,
                           and release invocation F and R; execution = effect
stream construction:      active sets from evaluating children only; deferred
                           sets from producer and transform invocation
runStream:                active sets union stream and sink deferred sets;
                           execution = effect
function interface:   invocation mode and bounded invocation sets
ordinary call:        evaluated-child sets union callee invocation sets
service call:         evaluated-child sets union method failures,
                      with the service method added to requirements
sequence:             union over statically reachable steps
if / match:           condition or subject sets union branch sets
catch:                body failures minus handled tags, union handler failures
                      body requirements union handler requirements
retry:                callback construction sets union invocation sets
provide:              construction sets union provider acquisition sets,
                      union callback invocation failures;
                      callback invocation requirements minus supplied methods,
                      union acquisition and construction requirements

Execution mode combines the modes of evaluated children, branches, and invocations. Nonempty sets require effect mode. Empty sets do not establish purity.

A semantic validator must recompute annotations and reject disagreements. TypeScript checks payload shape but cannot prove the semantic equations.

The validator receives immutable source texts keyed by source identifiers. It uses those texts to check source positions.

Validation areaRequired check
IdentityReferences, imports, exports, and capabilities resolve in their scopes.
TypesArguments, fields, branches, and function results match their checked types.
ContractsA written failure or requirement set bounds the function body from above. An inferred set equals the body set.
ExecutionModes preserve invocation effects, closure deferral, and provider acquisition.
RecoveryHandlers select possible tags and return compatible results.
ControlRetry jobs and provider jobs are zero-argument functions. Counts satisfy the literal constraint.
InitializationModule constants have pure active execution and empty active sets.
SerializationSets are sorted and unique, including Schema decodeRequirements and encodeRequirements. The serializer and independent validator both check this ordering. Literals are finite. Source spans refer to supplied text.

Surface grammar

This grammar summarizes the named extensions. The lexical rules and type rules above constrain the terminal forms. It is not a generated parser.

Grammar summary
module       = (importDecl | declaration)*
importDecl   = "import" "{" Name ("," Name)* [","] "}" "from" String
declaration = ["export"] (typeDecl | nominalDecl | errorDecl | serviceDecl | binding)
typeDecl     = "type" Name "=" type
literal      = String | Number | "true" | "false" | "null"
nominalDecl  = ["sealed"] "nominal" Name "=" type
errorDecl    = "error" Name "=" recordType
serviceDecl  = "service" Name "{" method* "}"
method       = Name "(" parameters ")" ":" type ["fails" errorSet]
binding      = "const" Name [":" type] "=" expression
lambda       = (Name | "(" parameters ")" [":" type effects]) "=>" expression
functionType = ["pure"] "(" parameters ")" "=>" type effects
effects      = ["fails" errorSet] ["uses" requirementSet]
errorSet     = ErrorName ("|" ErrorName)*
requirementSet = QualifiedName ("|" QualifiedName)*
recordType   = "{" [field ("," field)* [","]] "}"
field        = Name ["?"] ":" type
type         = union of primitive, named, record, array, tuple,
               option, function, effect value, stream, and sink types
effectType   = "Effect" "<" type ["," (errorSet | "never")
               ["," (requirementSet | "never")]] ">"
recordBinding = "const" "{" bindingField ("," bindingField)* [","] "}" [":" type] "=" expression
bindingField = Name [":" Name]
block        = "{" (binding | recordBinding | expression)* "}"
listExpr     = "[" [expression ("," expression)* [","]] "]"
deferExpr    = "defer" "(" expression ")"
runExpr      = "run" "(" expression ")"
resourceExpr = "withResource" "(" arrow "," arrow "," arrow ")"
usingExpr    = "using" "(" Name "=" arrowBody ")" block "finally" block
fallbackExpr = logicalExpr ["??" fallbackExpr]
streamType   = "Stream" "<" type ["," (errorSet | "never") ["," (requirementSet | "never")]] ">"
sinkType     = "Sink" "<" type "," type ["," (errorSet | "never") ["," (requirementSet | "never") ["," type]]] ">"
matchExpr    = "match" expression "{" arm ("," arm)* [","] "}"
arm          = pattern "=>" expression
tryExpr      = "try" block "catch" "{" handler ("," handler)* [","] "}"
handler      = ErrorName "(" Name ")" "=>" expression
ifExpr       = "if" "(" expression ")" block "else" (block | ifExpr)
pipeExpr     = nonPipeExpr ("|>" pipeStage)*
pipeStage    = QualifiedName ["(" arguments ")"]

The checker recognizes only the stated core. Unlisted JavaScript features do not become valid merely because the file resembles TypeScript.

The grammar accepts a primitive type in every type position. The checker then applies the rule on primitive types in Data and types.

The core operators are +, -, *, /, ===, !==, <, <=, >, >=, &&, ||, and ??. Their operands follow the primitive typing rules and the nominal operator rules in Data and types.

Types use postfix [] for arrays and brackets for tuples. Option<T> and Effect<A, E, R> are builtin type applications.

Core 1 also reserves CloseableScope, Ref<A>, Fiber<A,E>, RetrySchedule<E>, Schedule<O,I,E,R>, ScheduleInput<I>, ScheduleMetadata<O,I>, ScheduleDecision<S,O>, Codec<Decoded,Encoded>, Schema<D,E,RD,RE>, and Layer<Services,E,R>. Their type arguments follow the same angle bracket and union rules. Schedule defaults omitted I, E, and R to unknown, never, and never.

Core 1 builtins are makeRef, getRef, setRef, updateRef, race, withFiber, joinFiber, interruptFiber, bufferStream, mergeStreams, retrySchedule, codec, decode, encode, layer, and mergeLayers. Draft 0.5 adds all for Effect collections. Draft 0.6 adds scoped and forkScoped. Draft 0.7 adds zipStreams. Draft 0.8 adds concatStreams. Draft 0.9 adds flatMapStreams. Draft 0.10 adds partitionAll. Draft 0.11 adds validateAll. Draft 0.12 adds allResults. Draft 0.13 adds takeStream. Draft 0.14 adds takeWhileStream. Draft 0.15 adds takeUntilStream. Draft 0.16 adds dropStream. Draft 0.17 adds dropWhileStream. Draft 0.18 adds filterStream. Draft 0.19 adds mapStream. Draft 0.20 adds scanStream. Draft 0.21 adds mapAccumStream. Draft 0.22 adds foldStream. Draft 0.23 adds foldEffectStream. Draft 0.24 adds collectStream. Draft 0.25 adds headStream. Draft 0.26 adds lastStream. Draft 0.27 adds drainStream. Draft 0.28 adds bounded and unbounded concurrency options to mapEffect and flatMapStreams. Draft 0.29 adds effectful filter and prefix predicates. Draft 0.30 adds switchMapStreams. Draft 0.31 adds catchStream. Draft 0.32 adds retryStream. Draft 0.33 adds debounceStream and throttleStream. Draft 0.34 adds CloseableScope, makeScope, provideScope, addScopeFinalizer, and closeScope. Draft 0.35 adds Sink leftovers, takeSink, and transduceStream. Draft 0.36 adds mapGroups. Draft 0.37 adds aggregateStream. Draft 0.38 adds scheduleFromStep, scheduleRecurs, scheduleSpaced, scheduleMap, and aggregateWithinStream. Draft 0.39 adds scheduleMax, scheduleMin, and scheduleConcat. Draft 0.40 adds first-class Schema composition and guarded array recursion. Draft 0.41 adds schemaOptionalKey, schemaUnion, schemaInvalid, schemaAt, and schemaBoth, plus exact optional fields, tagged finite recursion, and authored transformation issues. Draft 0.42 adds schemaOptionFromOptionalKey, schemaDefaultKey, schemaOptionalTransform, and SchemaEncodedOptional for side-specific optional fields. Draft 0.43 adds effectful optional getters and defaults with directional service requirements and bounded Schema issues. Draft 0.44 adds guarded required-encoded to optional-decoded struct fields. Draft 0.45 extends schemaUnion to general ordered branches. Draft 0.46 adds null data and schemaNullToMissingKey for explicit nullable field policies. Draft 0.47 adds explicit default encoding strategies. Draft 0.48 adds schemaDefaultNullKey for missing-or-null defaults. Draft 0.49 adds schemaDefaultEncodedKey for encoded-value defaults. Draft 0.50 adds schemaMake for type-side construction. Draft 0.51 adds schemaConstructorDefault and a fifth Schema make-input parameter. Draft 0.52 specifies make-input propagation through optional Schema adapters. Draft 0.53 carries distinct make inputs through guarded recursive Schemas. Draft 0.54 permits distinct recursive decoded and encoded representations. Draft 0.55 attaches constructor defaults to field descriptors while preserving their presence flags. Draft 0.56 specifies retained, target-selected and reset defaults through later field adapters. Draft 0.57 carries guarded recursive self through wire-default and optional-bridge adapters. Draft 0.58 removes takeWhileStream, takeUntilStream, dropWhileStream, filterStream, mapStream, schemaDefaultNullKey, and schemaDefaultEncodedKey in favor of the effect predicate forms, mapEffect, and the on option of schemaDefaultKey. These constructors use ordinary call syntax. Only all, retrySchedule, scoped, forkScoped, provideScope, addScopeFinalizer, and closeScope also accept a pipeline stage; the others reject one. joinFiber and interruptFiber take a borrowed fiber, which they accept only as a direct call argument.

The word nominal is a declaration word only at the start of a declaration. It stays a legal name elsewhere. A host interface file can declare a nominal type.

The borrow parameter qualifier applies only in a function parameter list. An opaque type declaration applies only in a host interface file.

A brace after an arrow, if condition, else, try, or catch enters its declared control context. Returned object literals require parentheses after an arrow.

The checker rejects duplicate declarations, fields, methods, match arms, and handler tags where their scopes require uniqueness.

Operator group, highest firstOperand and result rules
Calls and field readsA call checks parameter types. A field read uses the declared field type.
* and /Numeric operands produce a numeric result. Division produces number. A nominal operand widens to its base type.
+ and -Numeric operands produce a numeric result. Two values of one numeric nominal type produce that type. Other nominal operands widen to the base type. Addition also accepts two strings and produces string.
<, <=, >, >=Numeric operands produce boolean. A numeric nominal operand compares with its own type or its base type.
=== and !==Compatible primitives, literal unions, or a primitive nominal type with its own type or base type produce boolean. Two different nominal types are an error. A singleton null operand may also compare with a nullable opposite operand.
&&Boolean operands produce boolean, with short-circuit evaluation.
||Boolean operands produce boolean, with short-circuit evaluation.
??An Option returns its payload or lazily evaluates the fallback.
|>The stage receives the left result as its first argument.

Binary operators within each group associate from left to right, except right-associative ??. Numeric arithmetic preserves int when both operands are int, except for division.

Arithmetic with a number operand produces number. String addition performs no implicit conversion. Invalid int arithmetic produces a defect.

Diagnostics and conformance

Diagnostics identify a source span, a category, and a concrete mismatch. The categories are syntax, unknown-name, declaration, type-mismatch, pattern, failure-contract, requirement-contract, execution-mode, module, host-boundary, and validation.

The effektive-lang compiler reports each diagnostic with a code that joins a category and a number, such as type-mismatch/003. A primitive type outside a type or error declaration gives declaration/013.

An invalid program that the compiler rejects
export const broken = (id: UserId): User
  uses Users.find => getUser(id)

// Invalid: getUser can fail, but an exported function with no fails clause allows no failures.
export const broken = (id: UserId): Effect.Effect<User, never, Users> =>
  // @ts-expect-error Effect-TS rejects the same contract: getUser can fail.
  getUser(id)

Acceptance fixtures must check evaluation order, single evaluation, branch selection, failure propagation, explicit contracts, closure deferral, retry, and provider resolution. Effect value fixtures must check construction without execution, repeated run, transport through bindings and functions, and failure and requirement contracts at the run boundary.

Pipeline fixtures must verify argument placement, stage order, short-circuiting after failure, and source locations for generated bindings.

Collection fixtures must verify callback purity for map, filter, and fold; callback effects for traverse; sequential, bounded, and unbounded concurrency, order, fail-fast sibling interruption, finalizer completion, empty arrays, and distinct closed builtin identities. Effect collection fixtures must also verify tuple and record shape, unioned channels, and cancellation. Stream fixtures must verify deferred construction, one-element demand, channel activation, and cancellation cleanup.

Backend fixtures must compare values, typed failures, and operation traces. A future Effect target also requires strict TypeScript checking and runtime checks.

Delivered checks cover document consistency, the manual log IR fixture, and strict TypeScript checks of every illustrative Effect-TS tab. The compiler repository checks emitted TypeScript and runtime behavior; this site checks its illustrative tabs separately.

Core 1 extensions

Core 1 adds the following source contracts. The compiler supports the listed reference, race, scoped fiber, borrowed handle, stream, retry schedule, codec, and source Layer forms. Drafts 0.4 and 0.5 extend concurrency with unbounded traverse and heterogeneous Effect collection. Target checks and runtime tests cover each slice.

Sink leftovers and synchronous transduction

Sink<I,O,E,R,L> appends a covariant leftover type L, defaulting to never. Existing two-, three-, and four-argument applications retain their meaning. Input remains contravariant, output covariant, and failure/requirement sets use subset compatibility. The target type is Sink.Sink<O,I,L,E,R>. A no-leftover sink can fit a type permitting leftovers. Sink aliases remain runtime-only, and existing borrow restrictions apply.

takeSink(count) constructs Sink<I,I[],never,never,I> with a positive safe integer literal count. Zero, negative, fractional, dynamic, and non-finite counts are rejected. The input I comes from an expected Sink type in a binding, return, or argument position, or from the source of an inline transduceStream, aggregateStream, or aggregateWithinStream call. An inline runStream call also supplies the expected Sink input type from its source. Without that context, an annotation is required. Sink<I,I[]> cannot hide this constructor's produced leftovers. A fifth type argument requires explicit error and requirement placeholders.

transduceStream(source, sink) requires the source element and the sink leftover type each to fit the sink input type. Leftovers need not fit the narrower original source element type. It returns a Stream of sink results carrying the union of source and sink deferred failure/service channels. Construction evaluates arguments once, left to right, without pulling or running a sink transform; construction failures and requirements stay active. Services are supplied at terminal consumption. Each requested result invokes fresh sink state. Leftovers replay before a new upstream pull. No producer fiber, queue, chunk rewrite, or extra read-ahead is added. Existing upstream buffering and concurrency retain their behavior.

With takeSink(2), input [1,2,3,4,5] yields [[1,2],[3,4],[5]], exact input [1,2,3,4] yields [[1,2],[3,4],[]], and empty input yields [[]]. A fresh run can discover completion and emit an empty result; outputs are not filtered. After input 1,2,3 followed by typed source failure, the pinned target emits [1,2], then [3], then propagates the failure on further demand. Sink failure emits no invented incomplete result. Defects and interruption pass through without forcing a partial result. Already emitted outputs remain observable.

Sink transforms share the whole consumption scope. Finalizers registered in it remain until that consumption closes, including after individual sink results; downstream failure and interruption wait for started cleanup. A sink requiring earlier release must bracket it internally. Arbitrary host sinks may finish without consuming input or return all input as leftovers, so their type does not guarantee progress or bounded resource retention. They may emit indefinitely without discovering source completion; bounded consumers and cooperative host implementations remain responsible for progress. No watchdog or new error is inserted.

runStream still runs one sink, returns its output, discards leftovers of any type, and closes the producer continuation. Both Sink operations accept ordinary calls and may be shadowed by source bindings; neither is a pipeline stage. Custom source sink-building callbacks remain deferred. The version 2 IR adds an optional leftover field to Sink, absent for never, plus take-sink and transduce-stream nodes. Independent validation recomputes count constraints, constructor shape, replay compatibility, and active/deferred channels. Old default payloads and interface hashes remain unchanged. The backend emits typed Sink.take and Stream.transduce calls without unchecked casts.

Untimed async aggregation

aggregateStream(source, sink) accepts Stream<A,E1,R1> and Sink<I,O,E2,R2,L> when A and L each fit I. It returns Stream<O,E1 | E2,R1 | R2>. Source and Sink construction expressions evaluate once, left to right; their effects remain active. Pulls, Sink runs, and required services stay deferred until terminal consumption. An inline takeSink gets its input hint from the source. Calls may be shadowed by a source binding; pipeline syntax is not provided.

The backend emits a typed Stream.aggregate call. Each consumption starts fresh producer, handoff queue, Sink runs, and scope. The pinned target uses a capacity-zero queue and replays Sink leftovers before another upstream pull. It offers no configurable aggregation capacity or global memory bound. With takeSink(1), blocking the consumer after its first result leaves one upstream pull observed; buffered transduction can read ahead. Source and Sink failures retain their channels, and interruption waits for a pending source pull's cleanup.

For one Stream.fromArray chunk and takeSink(2), [1,2,3,4,5] produces [[1,2],[3,4],[5]], exact [1,2,3,4] produces [[1,2],[3,4]], and empty input produces []. A typed source failure can follow a partial Sink result. Aggregation may flush a partial Sink between chunks under the target's internal Schedule.forever handoff, so rechunking can change the partition. A host Sink that completes without input can repeatedly emit; callers must bound that consumption. The version 2 IR adds an aggregate-stream node, and independent validation checks its operand compatibility and active/deferred channels.

General Schedule values and timed aggregation

Schedule<O,I,E,R> is a reusable, deferred policy with output O, input I, typed failures E, and service requirements R. O, E, and R are covariant; I is contravariant. A Schedule with input unknown accepts any consumer input, an exception limited to this slot because ordinary source unknown is not a top type. RetrySchedule<E> remains a separate pure retry policy. General Effect retry and repeat consumers, manual Schedule stepping, and cron schedules are not in this draft.

scheduleFromStep(initial, decide) receives two inline callbacks. The nullary initial callback creates state S once per Schedule execution. The decision callback receives S and ScheduleInput<I>, with input, one-based attempt, start, now, elapsed, and elapsedSincePrevious fields. It returns ScheduleDecision<S,O>: a Continue record carries next state, output, and numeric delayMillis; a Done record carries the normal completion output. Both callbacks may fail or use services; their channels are unioned in the Schedule. A typed callback failure stays distinct from Done and from a defect. The backend maps this record to Schedule.fromStepWithMetadata, Duration.millis, and target Done. No target step or Cause escapes into source.

scheduleRecurs(times) and scheduleSpaced(delayMillis) make zero-based numeric output policies with unknown input. scheduleMap(schedule, callback) receives ScheduleMetadata<O,I>, adding output and durationMillis to the input metadata. It preserves input and delay, changes the output, and unions callback failures and requirements. The callback runs for continuing steps and for normal completion, where durationMillis is zero. Constructing a Schedule calls neither a factory nor a service. Each execution starts fresh state and resolves services under its current provider.

aggregateWithinStream(source, sink, schedule) accepts Stream<A,E1,R1>, Sink<I,O,E2,R2,L>, and Schedule<C,J,E3,R3> when A and L fit I and Option<O> fits J. It returns Stream<O,E1 | E2 | E3,R1 | R2 | R3>; C is policy output, not a stream element. It is a call-only, shadowable prelude operation. The checker and independent validator rederive input compatibility and active versus deferred channels. The backend emits a typed Stream.aggregateWithin call without erasing schedule channels.

Each terminal consumption starts fresh producer, queue, Sink, scope, and Schedule execution state. The policy sees the previous aggregation result, starting with None, when the current aggregation receives its first element. A timed recurrence may flush a partial Sink result. Normal Schedule end emits the current result and leftovers, then stops before another upstream pull. A typed Schedule failure fails the Stream. Failure or interruption awaits started source and Sink cleanup. Time between aggregations is excluded from the Schedule clock, so scheduleSpaced is the selected simple flush timer.

Binary Schedule composition

scheduleMax(left, right), scheduleMin(left, right), and scheduleConcat(left, right) accept two Schedule values whose input types are comparable: every value of one input type must fit the other, with unknown accepting any Schedule input. The narrower input becomes the result input. Incomparable inputs are rejected because the source language has no general intersection type. Each result unions the operands' typed failures and service requirements. Calls are shadowable and cannot be used as pipeline stages. RetrySchedule is not accepted.

scheduleMax recurs only while both policies recur and chooses the longer delay; scheduleMin recurs while either recurs and chooses the shorter active delay. Both output the selected delay in milliseconds as number, including zero on normal completion. The backend uses target Schedule.max or Schedule.min and maps Duration output through Duration.toMillis. scheduleConcat runs the left to completion, then starts the right on the same input that ended the left. It returns the union of both output types and completes with the right policy's Done output. The backend emits target Schedule.concat.

Arguments evaluate once in source order. Construction does not run either policy or resolve its services. Every execution gets fresh child state; a typed failure, defect, or interruption retains its target cause. The existing consumer owns cleanup. The version 2 IR adds schedule-max, schedule-min, and schedule-concat nodes, and independent validation rederives the input choice, output, deferred channels, and construction effects. General input intersections, policy arrays, manual stepping, and other Schedule consumers remain deferred.

References and concurrent effects

Ref<A> is a shared mutable cell inside an Effect runtime. makeRef(value), getRef(ref), setRef(ref, value), and updateRef(ref, callback) are effectful. The update callback is pure, and its replacement is atomic.

race(left, right) accepts two Effect values with compatible success types. It returns the first success, interrupts the loser, and waits for its finalizers. If both fail, the result has their union of typed failures.

withFiber(job, use) starts a supervised child from an Effect value. Its inline use callback receives a borrowed Fiber<A,E>. joinFiber exposes the child result and typed failures. interruptFiber stops it and waits for finalizers.

The child receives its service requirements when it starts. A child failure enters the parent contract when joined. The child is interrupted when use exits, even without a join.

forkScoped(job) starts an Effect value in the current scope and discards its Fiber handle. The job must have no typed failures. The call requires the reserved Scope capability and the job's service requirements. The child may outlive the spawning fiber; scope closure interrupts it and waits for finalizers. scoped(job) runs an Effect value in a fresh local scope, removes its Scope requirement, and closes the scope after success, failure, defect, or interruption. Its result and remaining channels come from the job. A source binding named scoped or forkScoped shadows the prelude operation. Explicit shared scope authority is specified below.

Explicit shared scopes

CloseableScope is an abstract runtime handle with no type arguments, fields, equality, or derived Schema. It is not data only, including inside an aggregate. Ordinary assignment, return, parameters, closures, arrays, records, Options, Effects, and Refs share the same scope identity and close authority. They do not move or consume an owner. Borrowed resource and Fiber restrictions remain unchanged; an explicit scope cannot retain an otherwise forbidden borrowed reference. Host interfaces may carry this type, but a raw target Scope.Closeable does not satisfy its wrapper representation.

makeScope() creates an independent sequential scope and shared completion cell. Creation needs no ambient Scope. Creator success, failure, or interruption does not close it. Cleanup is manual; dropping aliases performs no cleanup and the type does not enforce leak freedom. provideScope(owner, job) runs an Effect value with that selected Scope capability without closing it on return. It removes the reserved Scope requirement only from the latent job, preserving argument-construction failures and requirements and the job's other channels and success type. Ordinary provided work is not supervised by scope closure; forkScoped explicitly registers child interruption and cleanup. Provision after close may run ordinary work, but cannot leave a newly scoped child live.

addScopeFinalizer(owner, job) requires an Effect of void without typed failures or a latent reserved Scope requirement. It evaluates owner and job once, left to right, activates the job's service requirements, and captures their bindings at registration. While open, registration defers execution; finalizers run in reverse registration order. Captured services retain references, not their providers' resource lifetimes. A resource-backed provider needed beyond the creating root must remain live or be acquired inside the child. Registration after the underlying scope enters its closed state runs immediately and waits for its own cleanup, independently of the original close completion.

closeScope(owner) closes with success Exit.void. The first caller atomically claims one physical close; every alias waits uninterruptibly for the same cached outcome. Successful return means the original cleanup has completed. Cleanup defects remain defects and replay to later callers; remaining sequential finalizers are attempted. Pending caller interruption may be observed after cleanup without replacing the cached outcome. Late registration does not change that outcome or become part of its completion barrier. Cleanup that never finishes keeps close pending; waiting for close from a finalizer on that same owner creates such a dependency cycle. There is no timeout or static cycle detection. General supplied Exits, exit-aware finalizer callbacks, parallel strategies, and mandatory cleanup remain deferred.

All four operations force effect execution. makeScope has empty active channels; closeScope retains owner-expression channels. provideScope combines owner/job construction channels with latent job failures and requirements minus only the latent reserved Scope capability. addScopeFinalizer combines construction channels with the finalizer's latent requirements, captured for later execution; close therefore requires no services from registered finalizers. Source bindings may shadow the four operation names. The IR uses the closeable-scope type and make-scope, provide-scope, add-scope-finalizer, and close-scope nodes; the checker and independent validator enforce these equations and borrow/data-only exclusions. Emitted modules use a compatible structural wrapper containing the raw Scope, Deferred completion, and shared close-started state.

Source references and a race
type Count = int

const count = (): Count => {
  const ref = makeRef(0)
  updateRef(ref, (n) => n + 1)
  getRef(ref)
}

const choose = (): Count =>
  run(race(defer(() => 1), defer(() => 2)))
import { Effect, Ref } from "effect"

type Count = number

const count = (): Effect.Effect<Count> =>
  Effect.gen(function* () {
    const ref = yield* Ref.make(0)
    yield* Ref.update(ref, (n) => n + 1)
    return yield* Ref.get(ref)
  })

const choose = (): Effect.Effect<Count> =>
  Effect.race(Effect.succeed(1), Effect.succeed(2))

Borrowed resources

A host interface may declare an opaque type. Effektive may name that type but cannot inspect or construct its representation. Host type declarations must avoid import cycles with provider declarations.

withResource accepts an acquired handle as well as an operation record. Its use and release parameters are borrowed for that expression. They may pass the handle to a borrow-qualified parameter.

A host interface declares opaque type Name. A function parameter uses borrow name: Type to accept a scoped handle. Only host interfaces and functions whose bodies obey the borrow rule may declare such parameters.

A borrowed value cannot escape through a return, aggregate, alias, closure capture, or ordinary parameter. Borrow-qualified helpers cannot retain the value. Host methods promise the same rule at the trusted boundary.

Use and release results cannot contain the borrowed handle. Release runs after success, failure, defect, or interruption. Nested withResource calls compose scopes without exposing raw Scope values.

Buffered and merged streams

bufferStream(stream, { capacity: N }) creates a deferred stream with a positive safe integer literal N. It uses suspending backpressure. At most N elements wait in its queue, with one producer item in flight.

mergeStreams(left, right) accepts compatible element types. It starts both streams and ends after both finish. Each source keeps its own order; cross-source order is unspecified.

zipStreams(left, right) accepts any two Stream values and returns a Stream of ordered pairs [Left, Right]. It combines their deferred failure and service requirements. It starts production only on terminal consumption and ends when either input ends. An extra unpaired element is not emitted; closing the zipped stream interrupts the remaining producer and waits for its finalizers. A source binding named zipStreams shadows the prelude operation.

concatStreams(left, right) accepts two Stream values and returns a Stream whose element type is the union of their element types. It emits every left element before starting the right stream. The right stream is never started if the left stream fails. Construction is deferred until terminal consumption. Deferred failures and service requirements from both streams are combined. A source binding named concatStreams shadows the prelude operation.

mapEffect(stream, callback, options?) accepts a Stream and an inline callback from each source element to B. The callback may be pure, or may fail and use services. Without options it processes one element at a time in source order. The optional record is exactly { concurrency: N } for a positive safe integer literal N, or { concurrency: "unbounded" }. With concurrency greater than one, at most N callback effects run together; output remains in input order. The result is Stream<B,E | E2,R | R2>, with source and callback failures and requirements deferred until consumption. Construction invokes neither source nor callback. A callback failure stops new work, interrupts active siblings, and waits for finalizers. The backend uses Stream.mapEffect with its concurrency option and ordered output. A pure callback with empty failure and service sets and no options record compiles to Stream.map; the result type is the same, and B may differ from the element type of the source. The name mapEffect is reserved: a source declaration with that name gets declaration/002.

Live keyed groups

mapGroups(source, key, use, { bufferSize: N }) accepts a Stream<A,E1,R1>, an inline pure classifier from A to K, an inline effectful callback from (K, Stream<A>) to data-only B, and a positive safe integer literal N. The key type must be a homogeneous string, boolean, or int class, or one primitive nominal type. General numbers, structural keys, mixed classes, and effectful classification are rejected. The result is Stream<B,E1 | E2,R1 | (R2 minus Scope)>; source and callback channels stay deferred, while only the callback's Scope requirement is locally supplied. Construction performs no pulls or callback work. A binding named mapGroups shadows the prelude operation.

The callback's group parameter is borrowed. It may be left unused or referenced at most once as the direct first argument of collectStream, runStream, foldStream, foldEffectStream, headStream, lastStream, or drainStream. It cannot be stored, aliased, returned, transformed, passed to another function, or captured by a nested closure. The callback result must contain data only. The checker and independent IR validator enforce both rules; the map-groups node records the bound group identity and capacity.

Each terminal consumption starts one producer and one callback per discovered key. Group values keep source order, while completed callback results merge without a cross-group order guarantee. Consumers run concurrently without a fixed limit. N bounds each live key queue, not the total number of keys, active callbacks, pending offers, or results. A full live group queue backpressures the producer; a callback that finishes closes its group and later values for that key are discarded without reopening it. A callback-owned scope closes unused and partially consumed groups. Source or callback failure and downstream interruption close the producer and peer consumers and wait for their finalizers. The backend uses Stream.groupByKey and concurrent Stream.flatMap with a callback-owned pull scope and a one-result merge buffer.

flatMapStreams(source, callback, options?) accepts a Stream and an inline pure callback from each source element to a Stream. Without options it concatenates inner streams in source order, exhausting one before starting the next. The optional record is exactly { concurrency: N } for a positive safe integer literal N, or { concurrency: "unbounded" }. With concurrency greater than one, up to N inner streams may run together and their outputs merge as they arrive; cross-inner order is unspecified while each inner retains its own order. Construction does not pull either stream. The result carries the inner element type and the union of outer and inner deferred failure and service channels. A failure stops new inner streams, interrupts active siblings, and waits for their finalizers; interruption also closes active inner streams. The backend uses Stream.flatMap with its concurrency option and default buffering. A source binding named flatMapStreams shadows the prelude operation.

debounceStream(source, delayMs) accepts a Stream<A,E,R> and a nonnegative safe integer millisecond literal. It returns Stream<A,E,R>. Construction does not pull the source or start a timer. During terminal consumption, a new element replaces a pending element until the quiet interval elapses; then the latest pending element emits. A completed source emits its final pending element. A zero delay follows the target Stream.debounce behavior, including coalescing a synchronously produced burst. Source failures, requirements, and started finalizers remain deferred. The backend uses Stream.debounce with Duration.millis. A source binding named debounceStream shadows the prelude operation.

throttleStream(source, { units: U, durationMs: M, strategy: S }) accepts a Stream<A,E,R>, positive safe integer literals U and M, and the literal S equal to "shape" or "enforce". It returns Stream<A,E,R>. Each source element costs one token: the backend first rechunks to one element and then uses Stream.throttle with a token bucket of U units per M milliseconds and no extra burst. "shape" delays elements until capacity is available; "enforce" drops elements that cannot pass immediately. The source remains deferred, and each consumption starts a fresh bucket. Source failures and requirements propagate, and interruption closes active work. A source binding named throttleStream shadows the prelude operation.

retryStream(source, policy) accepts a Stream<A,E,R> and a pure RetrySchedule<P> value whose error type accepts every nominal member of E. It returns Stream<A,E,R>. Construction neither pulls the source nor consults the policy. On terminal consumption, a typed source failure consults the schedule, waits for its nonnegative millisecond delay when it permits another attempt, and restarts the entire source. Previously emitted elements remain and may be emitted again by the restarted source. The target Stream.retry resets the schedule after the first element of a new attempt passes through. If the schedule declines, the latest source failure propagates. Defects and interruption do not retry; cancellation closes active source work and waits for finalizers. Each consumption starts a fresh schedule. A source binding named retryStream shadows the prelude operation.

catchStream(source, recover) accepts a Stream<A,E,R> and an inline pure one-argument callback from the union of E to Stream<B,E2,R2>. It returns Stream<A | B,E2,R | R2>. The source typed failures are handled, so only recovery failures remain deferred. The callback may inspect the error and construct a replacement stream, but cannot perform active effects. Construction starts neither stream and does not invoke the callback. On terminal consumption, source values already emitted remain in order. At the first typed source failure, the source closes, recover runs once with that error, and the replacement stream emits its values; no further source values are pulled. A recovery failure propagates. Defects and interruption are not recovered. Each consumption starts afresh, and started finalizers complete on failure or interruption. The backend uses Stream.catch. A source binding named catchStream shadows the prelude operation.

switchMapStreams(source, callback) accepts a Stream<A,E,R> and an inline pure callback from A to Stream<B,E2,R2>. It returns Stream<B,E | E2,R | R2>. Construction starts neither source nor inner stream. On terminal consumption, each new source element interrupts the previously active inner stream and switches to the new inner. Values already emitted remain; a prior inner may emit no values if superseded before its first output. After the source completes, the latest inner may finish and emit. A source or inner failure propagates and closes started work; interruption waits for started finalizers. The backend uses Stream.switchMap with default options. A source binding named switchMapStreams shadows the prelude operation.

takeStream(stream, count) accepts a Stream<A,E,R> and a pure Int count. It returns Stream<A,E,R> and emits at most the first count elements. A count at or below zero produces an empty stream without pulling the source. Construction does not pull the source; consuming a positive prefix closes upstream work when the limit ends. The count expression is evaluated when takeStream is called. The backend uses Stream.take. A source binding named takeStream shadows the prelude operation.

dropStream(stream, count) accepts a Stream<A,E,R> and a pure Int count. It returns Stream<A,E,R>. A positive count consumes and discards up to that many source elements, then emits every remaining element. A zero or negative count emits the source unchanged. If the source ends before the count is reached, the result is empty. The count expression is evaluated when dropStream is called, but source pulls remain deferred until terminal consumption. A source failure in the discarded prefix still propagates. Started upstream work closes normally on completion, failure, or interruption. The backend uses Stream.drop. A source binding named dropStream shadows the prelude operation.

filterEffectStream(stream, predicate), takeWhileEffectStream(stream, predicate), takeUntilEffectStream(stream, predicate), and dropWhileEffectStream(stream, predicate) accept a Stream<A,E,R> and an inline one-argument callback from A to boolean. The callback may be pure, or may fail with E2 and require R2. Each returns Stream<A,E | E2,R | R2>. When the callback is pure with empty failure and service sets, the result is exactly the source stream type, so a stream alias is kept. Construction pulls no source element and runs no predicate. During terminal consumption, predicates run sequentially on pulled elements. Source and predicate failures stop later pulls and propagate; started upstream work closes on completion, failure, or interruption. Each consumption starts fresh. A source binding with any of these names shadows the prelude operation.

filterEffectStream tests each element and emits those whose predicate succeeds with true, in source order. takeWhileEffectStream emits the longest true prefix; it consumes but omits the first false element and stops pulling. takeUntilEffectStream emits through the first true element, including that element, then stops pulling. dropWhileEffectStream discards the longest true prefix, emits the first false element, and emits later elements without calling the predicate again. Empty sources emit nothing and call no predicate. The backend uses Stream.filterEffect, Stream.takeWhileEffect, Stream.takeUntilEffect, and Stream.dropWhileEffect, respectively, with no index parameter or extra options. When the callback is pure with empty failure and service sets, the backend uses Stream.filter without type refinement, Stream.takeWhile, Stream.takeUntil without excludeLast, and Stream.dropWhile instead, so a pure callback compiles to the pure stream operation.

scanStream(stream, seed, update) accepts a Stream<A,E,R>, a pure seed expression of type S, and an inline pure two-argument callback from (S,A) to S, with no failure or service channels. It returns Stream<S,E,R>. Construction does not evaluate the seed expression or pull the source. Each terminal consumption evaluates the seed once and emits it before any source pull. It then calls update once per source element and emits each new state in source order. Each run starts with a fresh scan state. An empty source emits only the seed. A source failure propagates after states already emitted, and started upstream work closes normally on completion, failure, or interruption. The backend uses Stream.scan with a lazy seed function. A source binding named scanStream shadows the prelude operation.

mapAccumStream(stream, seed, update) accepts a Stream<A,E,R>, a pure seed expression of type S, and an inline pure two-argument callback from (S,A) to [S,B[]], with no failure or service channels. The callback declares its tuple result type explicitly so B is known even when an input emits no values. It returns Stream<B,E,R>. Construction does not evaluate the seed or pull the source. Each terminal consumption evaluates the seed once, starts with fresh state, calls update once per source element, and emits the returned array in order before processing the next element. The seed itself is not emitted. An empty source produces no values. A source failure propagates after prior outputs; started upstream work closes on completion, failure, or interruption. The backend uses Stream.mapAccum with a lazy seed and no onHalt callback. A source binding named mapAccumStream shadows the prelude operation.

foldStream(stream, seed, update) accepts a Stream<A,E,R>, a pure seed expression of type S, and an inline pure two-argument callback from (S,A) to S, with no failure or service channels. It is a terminal effectful expression returning S, failing with E, and requiring R. Constructing its Effect does not evaluate the seed or pull the source. Each execution evaluates the seed once before the first pull, calls update once per source element in order, and returns the final state. An empty source returns the seed. Each execution starts with fresh state. A source failure propagates without returning a state, and started upstream work closes normally on completion, failure, or interruption. The backend uses Stream.runFold with a lazy seed function. A source binding named foldStream shadows the prelude operation.

foldEffectStream(stream, seed, update) accepts a Stream<A,E,R>, a pure seed expression of type S, and an inline two-argument callback from (S,A) to S that may fail with E2 and require R2. It is a terminal effectful expression returning S, failing with E | E2, and requiring R | R2. Constructing its Effect does not evaluate the seed or pull the source. Each execution evaluates the seed once before the first pull, calls update sequentially once per source element, and returns the final state. An empty source returns the seed without calling update. Each execution starts with fresh state. A source or update failure stops later pulls and propagates; started upstream work closes on completion, failure, or interruption. The backend uses Stream.runFoldEffect with a lazy seed function and an Effect-valued callback, lifting a pure callback when needed. A source binding named foldEffectStream shadows the prelude operation.

collectStream(stream) accepts a Stream<A,E,R> and is a terminal effectful expression returning A[], failing with E, and requiring R. Constructing its Effect does not pull the source. Each execution starts a fresh collection, pulls the source to completion, and returns its elements in source order. An empty source returns an empty array. A source failure propagates without returning a partial array; started upstream work closes on completion, failure, or interruption. The operation has no element bound, so a source that never completes does not return. The backend uses Stream.runCollect. A source binding named collectStream shadows the prelude operation.

headStream(stream) accepts a Stream<A,E,R> and is a terminal effectful expression returning Option<A>, failing with E, and requiring R. Constructing its Effect does not pull the source. Each execution pulls until the first element, returns Some(first), and closes started upstream work after waiting for finalizers. An empty source returns None. A source failure before the first element propagates; later elements are not requested. Each execution starts a fresh consumption. The backend uses Stream.runHead. A source binding named headStream shadows the prelude operation.

lastStream(stream) accepts a Stream<A,E,R> and is a terminal effectful expression returning Option<A>, failing with E, and requiring R. Constructing its Effect does not pull the source. Each execution pulls the source to completion and returns Some(last) if at least one element was emitted, or None for an empty source. A source failure propagates without returning a partial result; started upstream work closes on completion, failure, or interruption. A source that never completes does not return. Each execution starts a fresh consumption. The backend uses Stream.runLast. A source binding named lastStream shadows the prelude operation.

drainStream(stream) accepts a Stream<A,E,R> and is a terminal effectful expression returning void, failing with E, and requiring R. Constructing its Effect does not pull the source. Each execution pulls the source to completion and discards every emitted value. An empty source succeeds with void. A source failure propagates after any preceding pulls; started upstream work closes on completion, failure, or interruption. A source that never completes does not return. Each execution starts a fresh consumption. The backend uses Stream.runDrain. A source binding named drainStream shadows the prelude operation.

Failure interrupts the other producer and waits for its finalizers. Buffer and merge combine deferred failure and requirement channels. Neither operation starts a producer before terminal consumption.

Retry schedules

RetrySchedule<E> is a first-class retry policy for nominal failure type E. retrySchedule takes a pure callback receiving the latest error and attempt number. The first failure has attempt number 1.

The callback returns Option<DelayMs>, where DelayMs aliases int and measures milliseconds. Some permits another attempt after that delay. None propagates the latest failure.

A negative delay is a runtime defect. retry(job, { schedule: policy }) keeps the job result, failures, and requirements. The existing { times: N } form remains valid. Defects and interruption are not retried.

Source codecs

Codec<Decoded,Encoded> is a value with two pure transformations. codec(decode, encode) infers its types from annotated callback parameters and results. Both types must have compiler-derived schemas.

decode(codec, input) accepts unknown and returns Option<Decoded>. encode(codec, value) returns Option<Encoded>. Invalid values produce None. Codec construction and both transformations have no failure or service channels.

A required Option field remains distinct from an optional field during both directions.

First-class Schemas and guarded recursion

Schema<D,E,RD,RE,M> is a reusable data-only Schema value. E and M default to D; RD and RE default to never. M is the make-input type, independent of decoded D and encoded E. RD lists decode services and RE lists encode services. schema() derives an identity Schema only from an expected Schema<T,T> type with empty services. schemaArray preserves both directions and their service sets. schemaStruct accepts required Schema fields, exact SchemaOptional fields that are optional in both directions, SchemaEncodedOptional fields whose decoded key is required but encoded key is exact optional, and SchemaDecodedOptional fields whose decoded key is exact optional but encoded key is required. It also accepts SchemaConstructorDefault fields whose decoded and encoded keys stay required while the make input may omit the field. schemaOptionalKey wraps an ordinary Schema for the first descriptor. Missing exact optional keys remain absent unless a field builder supplies a decoded value; a present undefined value fails when the wrapped Schema rejects it. schemaUnion accepts an inline list of at least two ordinary Schemas, including unlike types, overlapping records, duplicate types, and transformations. It unions the branches’ decoded data, encoded data, and each direction’s service requirements. Struct-only field descriptors are rejected. These constructors run no parser or service during construction.

schemaOptionFromOptionalKey(Schema<D,E,RD,RE>) returns SchemaEncodedOptional<Option<D>,E,RD,RE>. A missing encoded key decodes to a required None, and a present valid key decodes to Some. Encoding None omits the key; Some encodes through the wrapped Schema. schemaDefaultKey(schema, zero-argument callback, options?) returns SchemaEncodedOptional<D,E,RD | RDefault,RE> and evaluates the callback once per decode with a missing key. The options record may hold on, with the literal "missing" (the default), "null", or "encoded", and encodingStrategy. The on literal selects the operation: "null" follows the missing-or-null rules of draft 0.48, and "encoded" follows the encoded-value rules of draft 0.49. A present key skips it. Encoding includes the field under the default passthrough strategy; an explicit omit strategy leaves it absent. schemaOptionalTransform(source SchemaOptional, target SchemaOptional, decode, encode) returns SchemaOptional with target decoded and source encoded types. Its inline callbacks map Option of the source decoded or target encoded field to the corresponding directional Option; None omits the key. The checker and independent validator preserve separate decoded and encoded optionality and union the field Schemas’ and callbacks’ direction-specific requirements. These descriptors only serve struct fields, not Schema runners.

See the Core 1 code showcase for paired Effektive and Effect-TS examples of scoped concurrency, scheduled streams, service-backed optional fields, guarded required-encoded fields, ordered union fallback, nullable field policies, default encoding strategies, missing-or-null defaults, encoded-value defaults, explicit construction factories, and reusable constructor metadata.

Draft 0.43 permits effectful optional getters with either no typed failure or exactly SchemaIssue. They receive and return a directional Option, including when the key is missing; callback service requirements join only the corresponding decode or encode direction. The target wraps authored issues as SchemaError at the field path, while defects and interruption remain distinct. schemaDefaultKey also accepts an effectful callback with decode services and either SchemaIssue or SchemaError as its sole typed failure. An authored SchemaIssue becomes SchemaError at the emitted boundary; an existing SchemaError passes through. The default runs for each missing-key decode, and encoding does not need its services. Pure callbacks keep their existing behavior. No other typed failure is converted.

Draft 0.44 adds schemaOptionalFromRequired(source Schema, target SchemaOptional, decode, encode), returning a struct-only SchemaDecodedOptional<D,E,RD,RE>. Its encoded field is required and its decoded field is exact optional. The decode callback receives a present source-decoded value and returns Option of the target-encoded value; the target Schema then decodes Some into the optional decoded field, while None omits it. On encode, the target Schema encodes an optional decoded value first. The encode callback receives that Option of the target-encoded value and returns a required source-decoded value, so encoding always writes the key. Either callback may be pure or effectful, with at most SchemaIssue as its typed failure and services confined to its direction. A missing encoded key fails with SchemaError at that field before the decode callback runs. This explicit guard is necessary because the pinned Effect 4.0.5 required-to-optional target combinator otherwise accepts a missing encoded key at runtime despite its required Encoded type. The descriptor cannot run as an ordinary Schema.

Draft 0.45 preserves source branch order when emitting Schema.Union. During decoding or encoding, an untagged union tries eligible members in order and returns the first success. A typed SchemaIssue in one member can allow a later member to succeed; if all candidates fail, the runner fails with SchemaError. An earlier overlapping record may drop fields before a longer record is tried. Tagged unions retain target discriminant dispatch, so a selected tag does not fall through to an unrelated tag. Defects and interruption remain in their own channels. Static service requirements include every branch in the corresponding direction, even when a run stops early. Construction evaluates each branch expression once and does not run parsing or services. Recursive schemas retain the existing array, optional-key, or tagged-terminal guard; a union that immediately tries its own self Schema is rejected. The existing version 2 union IR operation needs no new shape.

Draft 0.46 supports two nullable exact optional field policies. schemaOptionalKey(schemaUnion([textSchema, nullSchema])) preserves a missing key as absent and a present null as present. schemaNullToMissingKey(Schema<D,E,RD,RE>) returns a struct-only SchemaOptional<D,E | null,RD,RE> and requires D and E to exclude null. A missing or null encoded key becomes absent; a present valid value runs the wrapped decoder. Encoding an absent key omits it and a present value runs the wrapped encoder; it never writes null. Both policies reject a present undefined value, and missing or null values skip the wrapped nonnullable transformation. Directional services stay in the static type even when skipped. The null-to-missing constructor is call-only and shadowable, evaluates its argument once, and adds the null-to-missing-key Schema IR operation. schemaOptionFromOptionalKey over a nullable Schema keeps None distinct from Some(null). A missing-key default still runs only for an absent key, so present null bypasses its callback and encodes as a present field under passthrough. Undefined-tolerant fields remain separate work.

Draft 0.47 extends schemaDefaultKey with an optional third argument: an inline record containing exactly encodingStrategy with the literal value "omit" or "passthrough". Named records, dynamic values, shorthand fields, spreads, duplicate keys, and extra fields are rejected. Without the third argument, the existing passthrough behavior applies. Decoding is identical under both strategies: a missing key runs a fresh decoded default, present keys skip it, and explicit undefined fails. The decoded field remains required and the encoded field remains exact optional. Passthrough runs the wrapped encoder and writes its value. Omit drops every valid field value, including values different from the default and nullable values; it does not compare values or run the default callback during encoding. Present decoded values still validate before omission, while the wrapped encoder and its failures, defects, and service lookups are skipped. Static encode requirements remain RE under both strategies, matching the target type. Source schemaEncode continues to require the decoded record. The existing version 2 default-key Schema operation accepts two or three arguments; no new type or descriptor is needed.

Draft 0.48 adds schemaDefaultNullKey(Schema<D,E,RD,RE>, zero-argument callback, options?), returning a struct-only SchemaEncodedOptional<D,E | null,RD | RDefault,RE>. Both wrapped channels must exclude null. An absent or null encoded key runs a fresh decoded default and skips the wrapped decoder; a present valid E runs that decoder and skips the default. Present undefined and invalid values fail with SchemaError without calling the default. The decoded field stays required and nonnullable. Defaults bypass the wrapped transformation and validate against D. Inline callbacks may be pure or effectful, with no failure, exactly SchemaIssue, or exactly SchemaError; services join only RD. Authored issues retain the field path, defects keep their channel, and cancellation waits for callback cleanup. The optional third argument follows schemaDefaultKey’s exact literal encodingStrategy rule. Passthrough runs the wrapped encoder and writes E; omit drops every valid decoded value while retaining static RE. Encoding never calls the default or synthesizes null. Source schemaEncode requires the decoded record. The constructor is call-only and shadowable and evaluates the Schema argument once. Version 2 IR adds the default-null-key Schema operation with two or three arguments, using the existing encoded-only optional descriptor.

Draft 0.49 adds schemaDefaultEncodedKey(Schema<D,E,RD,RE>, zero-argument callback, options?), returning a struct-only SchemaEncodedOptional<D,E,RD | RDefault,RE>. The inline callback returns encoded E, with no failure, exactly SchemaIssue, or exactly SchemaError, and may use decode services. A missing key runs a fresh callback, validates its E, and runs the wrapped decoder to produce D. Present valid E skips the default and runs that decoder; present invalid E and explicit undefined fail without calling the default. A present null passes through the decoder if E admits it and otherwise fails. The decoded field stays required and the encoded field is exact optional. The third argument follows the existing literal encodingStrategy rule. Passthrough runs the wrapped encoder and writes E. Omit also runs the wrapped encoder and validates its E, then discards the result. Thus omitted encoded defaults retain encoder services, failures, defects, and interruption cleanup at runtime. This differs from decoded-default omission, which skips the encoder. Encoding never runs the default and requires only RE. Default services join RD; authored issues retain field paths, defects keep their channel, and interruption waits for callback or decoder cleanup. The constructor is call-only and shadowable and evaluates its Schema argument once. Version 2 IR adds default-encoded-key with two or three arguments, using the existing encoded-only optional descriptor. The draft 0.52 and 0.53 rules below specify optional-adapter and guarded recursive make inputs.

schemaTransform(source, target, decode, encode) composes a source Schema<A,E,RD1,RE1> and target Schema<B,C,RD2,RE2>. Each inline callback independently returns its directional Option value without typed failure, or returns the directional value with exactly SchemaIssue as its typed failure. The Option form maps None to a fixed InvalidValue. schemaInvalid(message), schemaAt(key, issue), and schemaBoth(subject, left, right) author InvalidValue, Pointer, and Composite issue trees. No other typed callback error is implicitly converted. The resulting Schema<B,E,RD,RE> unions decode requirements from source, target, and decode callback, and encode requirements from source, target, and encode callback.

Draft 0.50 initially adds schemaMake(Schema<D,E,RD,RE>, D), returning D with SchemaIssue failure and no wire service requirements. It calls the Schema’s makeEffect method to validate the decoded type without running either wire transformation. The Schema and input arguments evaluate once in source order, and their active failures and services still propagate. Field descriptors, unknown inputs, values that do not fit D, incomplete records, and extra options are rejected. The constructor is shadowable and call-only. Ordinary source factories declare their own input types, fill defaults, and call schemaMake with the complete D; this leaves decoded and encoded field presence unchanged. Nested factories can catch SchemaIssue and use schemaAt to add the parent path. SchemaIssue catch and error-match dispatch uses SchemaIssue.isIssue for the target’s multi-variant issue family, and one catch handler cannot recatch a failure raised by another handler in the same clause. Version 2 IR adds the schema-make runner with schema and input children. Draft 0.51 extends this runner to accept the make-input M. Explicit factories retain their existing evaluation rules. Validation-disabling options and synchronous throwing or Option constructors remain outside this slice.

Draft 0.51 extends Schema to Schema<D,E=D,RD=never,RE=never,M=D>. M is data-only and invariant, as are D and E; changed make-input information cannot be erased by a four-argument annotation. schemaMake(schema, M) constructs D with SchemaIssue and no wire service requirements. schemaConstructorDefault(schema, zero-argument callback) returns a struct-only SchemaConstructorDefault<D,E,RD,RE,M>. Its inline callback returns M, may be pure or effectful, has no services, and may fail only with SchemaIssue. The containing struct makes that field optional in M while leaving D, E and both wire service directions unchanged. A missing or raw undefined constructor input runs a fresh callback, then constructs and validates its M; a present value skips the default and null stays present. Exact source make-input types reject explicit undefined. Defaults never run during wire decode or encode. Nested defaults attach complete issue paths, defects retain their channel, and interruption awaits cleanup.

Structs combine field make inputs; decoded-optional and constructor-default fields may be absent in the struct’s M. Arrays retain element M, unions retain the ordered union of branch M types, optional keys retain nested M, and transformations take M from their decoded target. The new constructor is call-only and shadowable. Default descriptors cannot run as Schemas, serve as array or union members, be wrapped twice, or pass through an incompatible field adapter. Draft 0.55 permits decorating an existing descriptor, and draft 0.56 specifies supported adapter order. schema() cannot derive a changed M from data types alone. The draft 0.53 recursive rule below carries changed M through guarded self references. The draft 0.52 optional adapter rule below covers nullable, Option, decoding-default and optional transformation fields. Existing M=D forms stay supported. Version 2 IR adds optional makeInput and constructorDefault Schema type properties and the constructor-default operation. Missing makeInput means D. Imports and independent validation preserve these properties. Typed emission restores checked make views through public Schema.make and native withConstructorDefault, without casts. Host calls to one raw emitted Schema.makeEffect effect retain native replay aliasing; source functions keep their existing evaluation rules. The draft 0.54 extension below also separates recursive D and E.

Draft 0.52 permits changed make input M through every existing optional adapter. schemaNullToMissingKey, schemaDefaultKey, schemaDefaultNullKey, and schemaDefaultEncodedKey retain the wrapped M. schemaOptionalTransform and schemaOptionalFromRequired take M from the decoded target; source make inputs do not enter the result. The containing struct makes a field optional in M only when its decoded side is optional. A decoding default leaves its constructor field required, and its callback still returns complete D or E according to the existing default rule. Making a value skips all wire defaults and optional transformation getters and needs neither wire service direction. Present partial nested M runs nested constructor defaults. Null-as-missing remains an encoded policy, so it does not admit null in M. schemaOptionFromOptionalKey explicitly produces M=Option<D>: its containing constructor key is required and Some must contain complete D. The declared Option schema validates that contents without running nested constructor defaults. No new IR operations are needed; independent validation recomputes adapter make inputs, and typed emission restores changed make views through public Schema.make. The draft 0.55 rule also attaches constructor defaults to these field descriptors; the next rule covers guarded recursive make-input propagation.

schemaDecode(schema, unknown) runs Schema.decodeUnknownEffect, returns D, fails with reserved SchemaError, and requires only RD. schemaEncode(schema, D) runs Schema.encodeEffect, returns E, fails with SchemaError, and requires only RE. Source can catch SchemaError and read its message; it cannot construct the error. The target issue tree remains available to host code. Defects and interruption retain their channels.

Draft 0.53 extends schemaRecursive to Schema<D,D,RD,RE,M>. The pure inline callback takes and returns that same full Schema type; each self reference keeps M. D and M are data-only aliases, each obeying the existing array, optional-key, or tagged terminal recursion guard. The body is a struct or tagged union. Self-bearing paths use struct, union, array, optional-key and constructor-default builders. Constructor-default wrappers preserve a guard but do not create one, and their callbacks cannot capture or invoke self. Expressions without self remain ordinary checked Schema leaves. Thus a required children array in D can be optional in M when the body supplies an empty-array constructor default. Making nested partial inputs runs fresh defaults at each missing field without wire services; decoding and encoding keep complete D and their directional services. Recursive issues retain field and array-index paths, defects retain their channel, and interruption awaits default cleanup. Named aliases keep finite IR references, and the existing typed Schema.suspend retains M without new IR operations or emitter helpers. A finite data guard does not prove termination of user defaults that keep generating further input. Direct and mutual cycles, cyclic runtime object graphs, and arbitrary Schema AST construction remain outside this slice. The next extension permits distinct recursive D/E.

Draft 0.54 extends schemaRecursive to Schema<D,E,RD,RE,M>. D and E may be distinct data-only recursive aliases, each obeying its own existing guard; M retains its independent make-input shape and defaults to D. The expected Schema and pure inline callback keep the same full D/E/M and service axes at every self reference, and the body must fit all of them. Numeric decoded fields may therefore correspond to text wire fields throughout a recursive tree. Existing optional adapters may give a recursive field different decoded and encoded presence, while decoding defaults still do not supply constructor fields. Tagged recursion retains matching literal discriminators on both sides and a self-free terminal branch. Decode consumes unknown wire input, returns D, fails with SchemaError and requires RD; encode consumes D, returns E, fails with SchemaError and requires RE; make consumes M, returns D, fails with SchemaIssue and needs neither wire service direction. Nested wire errors keep field and array-index paths, defects keep their channel, and interruption awaits nested decoder or encoder cleanup. The existing Schema IR and typed Schema.suspend preserve these axes without new operations, casts, or emitter helpers. schema() still requires identity D=E and no services. The draft 0.57 rule below admits selected self-bearing field adapters. Arbitrary self-bearing ordinary transformations, transformed recursive discriminators, and mutual recursion remain outside this slice. The draft 0.55 rule below specifies constructor-default composition with field descriptors.

Draft 0.55 lets schemaConstructorDefault decorate SchemaOptional, SchemaEncodedOptional, and SchemaDecodedOptional fields as well as ordinary Schemas. The combined results are SchemaOptionalConstructorDefault, SchemaEncodedOptionalConstructorDefault, and SchemaDecodedOptionalConstructorDefault, all with arguments <D,E=D,RD=never,RE=never,M=D>. D/E/M, decoded and encoded presence, and directional wire services stay unchanged. The constructorDefault flag is independent of the three mutually exclusive presence flags; all flags and data axes remain invariant across annotations and imports. The containing struct permits that key to be absent in M. Optional decoded fields remain optional in D, even when construction supplies a value. The inline zero-argument callback returns wrapped M, has no services and may fail only with SchemaIssue. Option fields still require complete Option<D> contents. Construction runs a fresh callback for missing or raw undefined input, then constructs its M; present input skips the callback and null stays present. Source types still reject explicit undefined. Wire defaults and optional getters do not run during make, and constructor defaults do not run during decode or encode. Encoding strategies, services, issue paths, defects, and awaited cleanup retain their existing rules. Draft 0.56 permits the supported adapters after constructor defaults too. Duplicate retained defaults, descriptor runners, and incompatible input presence are rejected. No new IR fields or operations are needed: constructorDefault may coexist with one presence flag. The draft 0.56 emitter uses structural Codec metadata views, supporting either composition order without requiring adapter-specific helper properties. Runtime emission uses public Schema.make and withConstructorDefault without casts or private APIs. The next rule specifies field adapters after constructor defaults.

Draft 0.56 lets unary field builders consume SchemaConstructorDefault while keeping their existing input-presence restrictions. schemaOptionalKey, schemaNullToMissingKey, schemaDefaultKey, schemaDefaultNullKey and schemaDefaultEncodedKey retain the input’s root constructor default and make input. Optional bridge positions also accept SchemaOptionalConstructorDefault, and the required source of schemaOptionalFromRequired may carry a constructor default. schemaOptionalTransform and schemaOptionalFromRequired inherit only the decoded target’s M and constructor default; source defaults do not become result defaults. schemaOptionFromOptionalKey explicitly resets the root default and produces required Option<D> make input, with complete D inside Some. A missing constructor key fails after this reset; a new Option-valued default may then be attached. A second default remains an error when the preceding adapter retained the original. Assignment flags remain invariant, and annotations cannot discard them. Wire defaults and getters stay independent from construction, and wire services, encoding strategies, failures, defects and cleanup retain their existing rules. No new syntax or IR properties are needed. Emitted field types use structural Schema.Codec views with checked make input, decoded and encoded optionality, and constructor-default metadata, restored through public Schema.make from the composed AST. These views require no adapter-specific helper properties, casts or private APIs. Incompatible presence rewrites and descriptor runners remain rejected. The draft 0.57 rule below permits selected guarded self paths through field adapters.

Draft 0.57 extends guarded schemaRecursive bodies through schemaDefaultKey, schemaDefaultNullKey, schemaDefaultEncodedKey, schemaOptionalTransform and schemaOptionalFromRequired. The guard visits the wrapped Schema or both bridge Schema arguments. Default callbacks, both directional getters, and options cannot capture self, including discarded captures. Binding identity permits an unrelated local with the same name. Only calls that resolve to the actual builtin supply structural proof.

Wire defaults and bridges carry an existing guard; they do not supply one. schemaNullToMissingKey supplies an optional-key guard in both directions. Constructor wrappers remain transparent and require self-free callbacks.

Each recursive D/E/M data alias retains its independent guard. A union beneath an established array, optional-key or terminal-tagged guard carries that guard through every member. This permits exact optional nullable wire links such as next?: WireNode | null. An unguarded nested union remains invalid. Root unions still require distinct literal tags and a self-free terminal branch.

Construction retains target M and defaults, skips wire getters, and requires neither wire service direction. Wire services, encoding strategies, nested failure paths, defects and awaited cleanup keep their existing rules. No syntax, IR shape or emission path is added.

Recursive Option data, arbitrary self-bearing ordinary transformations and mutual recursion remain outside this slice. The application acceptance rule below now sets the investigation order.

Draft 0.58 removes seven prelude names without removing any behavior. takeWhileStream, takeUntilStream, dropWhileStream, filterStream, and mapStream are removed because takeWhileEffectStream, takeUntilEffectStream, dropWhileEffectStream, filterEffectStream, and mapEffect accept pure callbacks and compile to the pure operations. A removed name gives the ordinary unknown-name diagnostic. schemaDefaultNullKey and schemaDefaultEncodedKey are merged into schemaDefaultKey, whose options record gains the on option: schemaDefaultKey(schema, callback, { on: "null" }) replaces schemaDefaultNullKey, and { on: "encoded" } replaces schemaDefaultEncodedKey. The option on must be a string literal, and a call without on keeps its meaning. Read the old names in drafts 0.14 to 0.19, 0.48, 0.49, 0.52, 0.56, and 0.57 as these forms. No IR shape, type, or emission path changes, and the emitted TypeScript is identical.

Source Layers and host adapters

Layer<Services,E,R> is a deferred provider value. layer(Service, acquire) receives an inline acquisition callback that produces exactly the service methods. Its failures and requirements become acquisition channels.

The final record of the callback takes the method shapes of the service, so method parameters need no annotations. A missing, duplicate, or unknown method reports type-mismatch/005. A method of the wrong type reports type-mismatch/003.

mergeLayers(left, right) combines disjoint service sets and acquisition channels. An overlap is a type error. provide accepts both source Layer expressions and declared host providers.

Package implementations stay in TypeScript behind registered host modules. Adapters validate data boundaries, translate package failures into declared errors, and preserve requirement and lifetime contracts.

The first adapter targets a file operation from effect/FileSystem. Its host interface declares a specific expected error and any required service methods. The adapter translates package failures at the boundary.

Typed IR migration

Core 1 uses light-functional-ir version 2. The version 2 model adds Ref, Fiber, RetrySchedule, Codec, Schema, opaque types, borrow qualifiers, and the operations above. Each expression keeps active and deferred channels distinct.

The compiler rejects version 1 IR input with a version diagnostic. Existing fixtures and the linked model use version 2, including the core 1 nodes.

The independent validator must reject malformed type constructors, escaped borrowed identities, invalid concurrency scopes, unsound deferred channels, and Layer service overlaps before emission.

Raw Scope values, effectful pure-Codecs, broader Schema builders and issue construction, further Schedule composition and consumers, and direct package imports remain outside core 1.

Examples and deferred work

ExamplePurpose
effektive-examples/log.effA service declaration and one direct call.
effektive-examples/collections.effPure data-first collection pipelines.
effektive-examples/pay.effA complete payment workflow with matching, retry, and immutable updates.
effektive-examples/users.effLookup, update, recovery, and collection transformation in a reusable module.
effektive-examples/main.effA separate application module that provides the imported user service.
effektive-examples/core1.effScoped concurrency, scheduled stream batches, effectful optional Schema fields, guarded required-encoded fields, general union fallback, nullable field policies, default encoding strategies, missing-or-null defaults, encoded-value defaults, explicit construction factories, reusable constructor metadata, optional-adapter make inputs, guarded recursive constructor inputs, separate recursive wire types, constructor defaults on field descriptors, and explicit default propagation through adapter order.

The two figures below show users.eff and pay.eff in full. Each Effect-TS tab translates the complete module, including its exports.

The complete Users module View Users source
export type UserId = string
export type Name = string

export type User = {
  id: string,
  name: string,
  active: boolean
}

export error UserMissing = { id: string }
export error StoreUnavailable = { message: string }

export service Users {
  find(id: UserId): Option<User> fails StoreUnavailable
  save(user: User): void fails StoreUnavailable
  all(): User[] fails StoreUnavailable
}

export const getUser = (id: UserId): User
  fails UserMissing | StoreUnavailable
  uses Users.find =>
  match Users.find(id) {
    Some(user) => user,
    None => fail(UserMissing({ id }))
  }

export const rename = (id: UserId, name: Name): User
  fails UserMissing | StoreUnavailable
  uses Users.find | Users.save => {
  const user = getUser(id)
  const renamed = { ...user, name }
  Users.save(renamed)
  renamed
}

export const displayName = (id: UserId): Name
  fails StoreUnavailable
  uses Users.find =>
  try {
    getUser(id).name
  } catch {
    UserMissing(error) => "Guest"
  }

export const activeNames = (): Name[]
  fails StoreUnavailable
  uses Users.all =>
  Users.all()
    |> filter(user => user.active)
    |> map(user => user.name)
import { Context, Effect, Option, Schema } from "effect"

export type UserId = string
export type Name = string

export const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  active: Schema.Boolean
})
export type User = typeof User.Type

export class UserMissing extends Schema.TaggedError<UserMissing>()(
  "UserMissing",
  { id: Schema.String }
) {}

export class StoreUnavailable extends Schema.TaggedError<StoreUnavailable>()(
  "StoreUnavailable",
  { message: Schema.String }
) {}

export class Users extends Context.Service<Users, {
  readonly find: (id: UserId) => Effect.Effect<
    Option.Option<User>,
    StoreUnavailable
  >
  readonly save: (user: User) => Effect.Effect<
    void,
    StoreUnavailable
  >
  readonly all: () => Effect.Effect<
    ReadonlyArray<User>,
    StoreUnavailable
  >
}>()("Users") {}

export const getUser = (id: UserId): Effect.Effect<
  User,
  UserMissing | StoreUnavailable,
  Users
> =>
  Effect.flatMap(
    Users.use((users) => users.find(id)),
    (found) => Option.match(found, {
      onNone: () => Effect.fail(new UserMissing({ id })),
      onSome: (user) => Effect.succeed(user)
    })
  )

export const rename = (id: UserId, name: Name): Effect.Effect<
  User,
  UserMissing | StoreUnavailable,
  Users
> => Effect.gen(function* () {
  const user = yield* getUser(id)
  const renamed: User = { ...user, name }
  yield* Users.use((users) => users.save(renamed))
  return renamed
})

export const displayName = (id: UserId): Effect.Effect<
  Name,
  StoreUnavailable,
  Users
> => Effect.map(getUser(id), (user) => user.name).pipe(
  Effect.catchTag(
    "UserMissing",
    () => Effect.succeed("Guest")
  )
)

export const activeNames = (): Effect.Effect<
  ReadonlyArray<Name>,
  StoreUnavailable,
  Users
> => Effect.map(
  Users.use((users) => users.all()),
  (all) => all.filter((user) => user.active).map((user) => user.name)
)
The complete payment module View payment source
type Cents = int
type ChargeId = string

nominal OrderId = string
type Order = {
  id: OrderId,
  totalCents: int,
  status: "pending" | "paid"
}

error OrderNotFound = { id: OrderId }
error PaymentDeclined = { reason: string }

service Orders {
  find(id: OrderId): Option<Order>
  save(order: Order): void
}

service Payments {
  charge(amount: Cents): ChargeId fails PaymentDeclined
}

const findOrder = (id: OrderId) =>
  Orders.find(id) ?? fail(OrderNotFound({ id }))

const chargeOrder = (order: Order) => {
  retry(() => Payments.charge(order.totalCents), { times: 2 })
  order
}

const markPaid = (order: Order) =>
  ({ ...order, status: "paid" })

const saveOrder = (order: Order) => {
  Orders.save(order)
  order
}

export const pay = (id: OrderId): Order
  fails OrderNotFound | PaymentDeclined
  uses Orders | Payments.charge =>
  id
    |> findOrder
    |> chargeOrder
    |> markPaid
    |> saveOrder
import { Context, Effect, Option, Schema } from "effect"

type Cents = number
type ChargeId = string

const OrderId = Schema.String.pipe(Schema.brand("OrderId"))
type OrderId = typeof OrderId.Type

const Int = Schema.Finite.check(
  Schema.makeFilter(
    (value) => Number.isSafeInteger(value) || "expected a safe integer"
  )
)

const Order = Schema.Struct({
  id: OrderId,
  totalCents: Int,
  status: Schema.Literals(["pending", "paid"])
})
type Order = typeof Order.Type

class OrderNotFound extends Schema.TaggedError<OrderNotFound>()(
  "OrderNotFound",
  { id: OrderId }
) {}

class PaymentDeclined extends Schema.TaggedError<PaymentDeclined>()(
  "PaymentDeclined",
  { reason: Schema.String }
) {}

class Orders extends Context.Service<Orders, {
  readonly find: (id: OrderId) => Effect.Effect<Option.Option<Order>>
  readonly save: (order: Order) => Effect.Effect<void>
}>()("Orders") {}

class Payments extends Context.Service<Payments, {
  readonly charge: (amount: Cents) => Effect.Effect<
    ChargeId,
    PaymentDeclined
  >
}>()("Payments") {}

const findOrder = (id: OrderId): Effect.Effect<
  Order,
  OrderNotFound,
  Orders
> => Effect.flatMap(
  Orders.use((orders) => orders.find(id)),
  (found) => Option.match(found, {
    onNone: () => Effect.fail(new OrderNotFound({ id })),
    onSome: (order) => Effect.succeed(order)
  })
)

const chargeOrder = (order: Order): Effect.Effect<
  Order,
  PaymentDeclined,
  Payments
> => Effect.map(
  Effect.retry(
    Effect.suspend(() =>
      Payments.use((payments) => payments.charge(order.totalCents))
    ),
    { times: 2 }
  ),
  () => order
)

const markPaid = (order: Order): Order =>
  ({ ...order, status: "paid" })

const saveOrder = (order: Order): Effect.Effect<Order, never, Orders> =>
  Effect.map(
    Orders.use((orders) => orders.save(order)),
    () => order
  )

export const pay = (id: OrderId): Effect.Effect<
  Order,
  OrderNotFound | PaymentDeclined,
  Orders | Payments
> => Effect.gen(function* () {
  const order = yield* findOrder(id)
  const charged = yield* chargeOrder(order)
  const paid = markPaid(charged)
  return yield* saveOrder(paid)
})

The mini book explains these forms and pairs each one with an illustrative Effect-TS tab. The typed IR model and validation notes accompany both documents.

Core 1 specifies borrowed handles, scoped fibers, races, buffered streams, references, source Layers, pure codecs, and custom retry schedules. The validation report tracks implementation status.

Remaining and deferred gaps

All three accepted application gates pass. The following boundaries remain outside the accepted target. This is a gap record, with no implementation order or commitment. Earlier validation entries describe their original snapshots.

AreaRemaining boundary
General Effect and concurrency APIsOther combinators, coordination primitives and cause-level recovery lack general source forms. The selected collection, racing and fiber forms remain supported.
Full Scope paritySupplied close Exits, exit-aware callbacks, parallel finalization, mandatory closure and static dependency-cycle detection remain outside the selected CloseableScope contract.
General Stream and Sink APIsOther operators, arbitrary source Sink constructors, chunk-cost throttling and unrestricted nested group streams need additional source and lifetime rules.
General Schedule APIsOpaque retry and repetition consumers, incomparable input intersections, other combinators and cron policies remain uncovered. Comparable-input composition and aggregation consumers are supported.
Time controlA general source clock and time-control API remains a candidate without an application blocker. Primitive services and native virtual time cover the selected applications.
Schema and recursionRecursive Option data, mutual recursion, arbitrary self-bearing transformations and general Schema AST construction remain unsupported. Selected guarded recursion retains distinct decoded, encoded and make-input types.
General language extensionsUser-defined generics, effect-row polymorphism, source purity assertions, macros and early returns remain deferred.
Package interoperability: item 9Arbitrary package imports, external values and constructors, and direct package-specific builders remain explicitly deferred by user instruction. Registered typed host adapters remain supported.

Further API work requires a demonstrated application blocker or an explicit target change. Check equivalent existing source compositions before selecting a feature. Item 9 requires the user to revisit its explicit deferral; a new blocker does not revoke that instruction.

Runtime limits remain recorded alongside the witnesses. Native typed recovery can discard accompanying reasons from a mixed cause. Transaction interruption after COMMIT starts can preserve a committed row. Transduction can emit a partial batch before input failure. Queue capacity establishes no global memory bound, and the applications promise no exactly-once delivery.

Release acceptance is separate from the language stopping rule. Automated checks now cover editor references, rename, completion, semantic tokens, contract quick fixes, inlay hints for the inferred fails and uses of non-exported module-level functions, bundled local CLI/VSIX installation, installed editor save and restart, and playground keyboard/error recovery at 375, 720 and 1280 pixels. Seven loopback HTTP and file-backed SQLite checks extend the service workflow with local persistence, rollback, cancellation and connection cleanup. Full M1/M3/M4 release measurements meet the existing 100 ms p90 diagnostics budget on the large synthetic project, including paired 10,000-edit sessions with three open documents. Finite RSS, heap and type-table observations establish no memory bound. The user’s manual editor/playground acceptance remains pending. The original recording drivers establish no external integration coverage; the new local checks establish no distributed transaction, ambiguous-commit recovery or exactly-once guarantee.

The accepted stopping rule requires three complete applications: a service workflow, a scheduled worker, and a stream processor. Each must compile with precise result, failure, and service contracts and pass failure, cancellation, and cleanup checks. Application orchestration stays in Effektive. Typed host adapters may supply drivers and the runtime entry point. All three application gates now pass using existing forms, including the service workflow’s registered atomic SQL adapter. The accepted language stopping rule is satisfied; release acceptance remains separate work.

The scheduled-worker application keeps repetition, selective retry, progress, validation and resource release in Effektive. Typed drivers supply in-memory job operations and primitive waits. Its runtime entry point supplies whole-worker interruption for shutdown. Ten compiled runtime checks cover virtual-time comparison, Effect replay, failures, defects, and awaited cleanup during work and both kinds of wait. A source-authored concrete policy meets this application’s requirements. General opaque Schedule consumers remain open; this witness selects no new builtin.

The service workflow application keeps input validation, business rules, catalog access, total calculation and selective storage retry in Effektive. Its registered typed host adapter owns one atomic SQL save, receipt validation and connection release. Fifteen compiled checks cover commit, rejection, retry timing and replay, failures, defects, and awaited cancellation cleanup. Body interruption rolls back and releases. Interruption after COMMIT starts waits for commit and release; a committed row can remain. The recording driver performs no external I/O.

The stream processor application keeps live pulls, ordered concurrent mapping, selective fallback, buffering, batching and session brackets in Effektive. Thirteen compiled runtime checks cover output order, live arrivals, empty and partial batches, replay, backpressure, failures, defects and awaited cancellation cleanup. The pinned pipeline stops at seven pull calls while the first write is blocked. That trace establishes no global memory bound.

Native transduction can emit a partial batch before propagating an input failure. Item recovery follows native catchTags semantics: matching a typed reason can discard accompanying reasons from a mixed transformation cause. Session cleanup runs outside item recovery. The recording driver performs no external I/O.

The order lifecycle application is a witness for nominal types. It is not a gate application, and the stopping rule does not count it. Four nominal states across two modules and a recursive pricing rule tree use match by name with no wildcard arm. A function decodes JSON through a structural type and builds a nominal value. Nine compiled runtime checks cover each transition, the InvalidTransition failure, pricing, decoding and the encoded tag.

The portable publisher is an exploratory portability witness, not a gate application. Effektive source owns publication decisions, conflict handling, grants, and serial batching. Memory and file-backed SQLite pass one shared contract for conditional writes and snapshots. Compiled checks cover persistence, uncertainty without retry, revocation, interruption, and awaited cleanup. The lost-acknowledgement case simulates failure after an actual committed write.

These checks establish no crash recovery, exactly-once delivery, or HTTP admission protocol. The witness adds no language forms.

All three application gates pass without a new language form. Stop automatic API additions under the accepted rule. Further language work requires a demonstrated application blocker or an explicit target change. Editor, packaging, performance and external integration acceptance remain separate work.

Deferral does not imply that those features need new syntax. Each extension must preserve the current execution, failure, and capability distinctions.

Sources and lineage

Forma inspired the separation of success values, expected failures, and required capabilities. Effektive chooses a TypeScript-family functional surface for those ideas.

Source references: https://forma-lang.com/effect and https://forma-lang.com/effect/reference.

Direct-call design reference: https://forma-lang.com/rfcs/0003-direct-style-effects. That RFC described proposed behavior when the design research occurred.

Target references: https://effect.website/docs/v4/api/effect/Context and https://effect.website/docs/v4/error-management/expected-errors.

These references support the design lineage and target examples. They do not establish an Effektive implementation.