What Essence is
What the language refuses to do, what it does instead, and where it sits beside Node or Bun.
Essence is a small compiled language for the pure part of a program: the rules, the arithmetic and the data they work on. You write the rules in Essence and call them from TypeScript. Numbers are exact, a forgotten case is a compile error, and the tests sit beside the code. A program compiles to one self-contained ES module that runs on Node, on Bun and in the browser.
A pricing rule with its test
An order is priced, or refused with a reason. The test sits in the same file as the code. Save the program as Pricing.es:
implementation {
choice Zone {
Domestic,
Overseas,
}
type Order = {
item: String,
quantity: Integer,
zone: Zone,
discount: Optional<Integer>,
}
function shipping(to zone: Zone) -> Integer {
<- match zone -> Integer {
case #Domestic { <- 490 }
case #Overseas { <- 2_490 }
}
}
function total(of order: Order, price: Integer) -> Result<Integer, String> {
if order.quantity::isLessThan(1) {
<- #Failure("{order.item}: the quantity must be at least 1")
}
constant goods = price::multiply(with order.quantity)
constant discount = order.discount::value(defaultingTo 0)
<- #Value(goods::subtract(discount)::add(shipping(to order.zone)))
}
constant beans: Order = {
item = "Beans",
quantity = 2,
zone = #Overseas,
discount = #Empty,
}
Terminal.print(total(of beans, price 1_290)) § Value(5070)
Terminal.print(total(of { beans with quantity = 0 }, price 1_290)) § Failure("Beans: the quantity must be at least 1")
}
tests {
test "prices an order with its shipping" {
require #Value(cents) = total(of beans, price 1_290)
expect cents::is(5_070)
}
}
essence run Pricing.es compiles the file and runs it, and the program prints the two lines in the § comments. Installation puts the essence command on your machine. essence test Pricing.es runs the program’s statements, then the test:
Value(5070)
Failure("Beans: the quantity must be at least 1")
Pricing.es
✓ prices an order with its shipping
1 passed · 0.47 ms
Compared with JavaScript. The same rule as a TypeScript function:
type Zone = "Domestic" | "Overseas"
type Order = { item: string; quantity: number; zone: Zone; discount: number | null }
function shipping(zone: Zone): number {
return zone === "Domestic" ? 490 : 2490
}
function total(order: Order, price: number): number {
if (order.quantity < 1) {
throw new Error(`${order.item}: the quantity must be at least 1`)
}
const goods = price * order.quantity
const discount = order.discount ?? 0
return goods - discount + shipping(order.zone)
}
const beans: Order = { item: "Beans", quantity: 2, zone: "Overseas", discount: null }
console.log(total(beans, 1290)) // 5070
try {
console.log(total({ ...beans, quantity: 0 }, 1290))
} catch (error) {
console.log((error as Error).message) // Beans: the quantity must be at least 1
}
Three things in it do not exist in Essence. Pricing.es uses an exact Integer for the number, an Optional for the null, and a Result the caller takes apart for the throw. The ternary also prices any new zone as Overseas, where Essence refuses to compile shipping until it handles the new zone.
Your first program builds a smaller program one step at a time. Each construct in this one has its own page:
- Programs and values:
implementation { … }around the statements,constantwith its: Orderannotation,§comments, number literals such as1_290, and strings with a{order.item}hole.Terminal.printwrites a value and a newline. - Method calls:
::calls a method on a value, as inorder.quantity::isLessThan(1). The argument’s label is written at the call:price::multiply(with order.quantity). - Conditions: the
ifthat refuses a quantity below one. - Functions:
function total(of order: Order, price: Integer) -> …with its labelled parameters, the calltotal(of beans, price 1_290), and<-, which returns a value from the function. - Records: the record type
type Order = { … }and the literal{ item = "Beans", … }, whose members take=where JavaScript writes:..reads a member, as inorder.zone, and{ beans with quantity = 0 }is a copy with one member changed. - Choices and
match:choice Zoneis a type with a fixed set of named cases. If you know TypeScript’s discriminated unions, a choice is one with the tag built in.#Overseasbuilds a case (#marks a case, not a private field), andmatch zone -> Integertakes one apart. Inside acaseblock,<-gives thematchits value. - Optional:
discount: Optional<Integer>holds#Emptyor a value, andvalue(defaultingTo 0)reads it with0for an empty one. - Result:
#Value(…)and#Failure(…), the two cases of theResult<Integer, String>that a function which can fail returns. - Tests: the
testsblock and itstest.require #Value(cents) = …takes theResultapart and fails the test if it is a failure.expectfails the test when itsBooleanisfalse.
What Essence does not have
- No I/O beyond
Terminal,HttpandAsync.Terminalhas seven functions, each called likeTerminal.print(…):print,write,inspect,describe,readLine,readAllandask.Httpsends a request, andAsyncis the timer and the way to describe a computation for later; both answer work rather than blocking on it. There is no JSON, no file and no environment variable. Beside them,Randomness.entropy()reads random values from the host, the JavaScript runtime that runs the program.essence run Main.es -- alpha betahandsalpha betato the process, and nothing in the language reads them. - No
async,awaitorPromise. Work that has not run is a value instead: aFuture.completewaits for what one answers with,startputs one in flight, and a function is marked by writingcompletein its body rather than by a word in front of it. Asynchrony has it. - No exceptions. There is no
throw,tryorcatch. A function that can fail says so in its return type, astotaldoes with itsResult. - No
nullorundefined. A value that may be absent is anOptional. The value inside cannot be used until the program says what happens when it is empty. - No mutation of data. A record, a list or a string never changes.
{ order with quantity = 3 }is a new record, andorderkeeps its old value. There is no assignment to a member. Aconstantcannot be reassigned, and avariablecan only be pointed at another value. - No operators.
+,==,&&,!,<and?:are not syntax. Arithmetic, comparison and logic are method calls:price::add(tax),a::is(b),a::and(b),a::negate(),a::isLessThan(b).iscompares records and lists by value, where===compares objects by reference. A value picked by a condition is adefine { … }or amatch. - No classes. There is no
class, nothis, no prototype, and no type inherits from another. Data you declare is a record or a choice. A namespace attaches methods to a type from outside, and a protocol names the methods several types share. - No
fororwhile. A list is transformed withmap,reduceand the other list methods. Other repetition is recursion orloop, one function whose labels (while,through,upTo) pick the kind of loop. Iteration has all of them. - No recursive types. A
typeor achoicecannot name itself, so a tree or a linked list cannot be declared. The compiler reportsrecursive-type-declaration, orrecursive-generic-choicefor a generic choice. - No tuples.
(1, 2)does not parse. A group of values is a record with named members. - No packages. An import names a relative
.espath such as"./Money.es", and nothing is searched for. The standard library is in scope without an import. - No calling JavaScript from Essence. The boundary runs one way: JavaScript calls Essence, and an Essence file cannot import JavaScript. The only host-written JavaScript that runs during a call is a function passed in as an argument.
- No floating point. There is no floating-point
numberand noNaN. Essence’s ownNumberis the union of its exact number types. - No truthiness. An
ifcondition is aBoolean.if 1is refused ascondition-not-boolean. - No default export. An
export { … }block lists names, and a JavaScript host imports each one by its name. - No type arguments at a call site.
identity<Integer>(1)does not parse. A generic function’s type parameters are inferred from its arguments, as Generics shows.
What Essence does instead
- Exact numbers. An
Integerhas no upper bound and aRationalis an exact fraction:0.1::add(0.2)::is(0.3)istrue, and0.1::add(0.2)prints3/10. Numbers has every number type. OptionalandResult. Absence and failure are ordinary values with two cases each. Coming from JavaScript mapsnull,undefined,throwandtryonto them.- A closed
choicewith an exhaustivematch. A choice lists its cases once. Amatchthat leaves one out is refused asmissing-case, and its Note names the case it lacks:Unhandled: 'Zone#Overseas'. - Checked refinements.
NonEmptyStringandPositiveIntegerare types with a check attached. Insideif name::hasCharacters(), aStringis aNonEmptyString. Outside it, the compiler refuses a plainString,""included, where aNonEmptyStringis asked for. Checked refinements has the rest. - Tests in the same file. A failed
expectlabels the values that went into it. For adoublethat multiplies by three,expect double(2)::is(4)shows thatdouble(2)was6. Your first test reads a failure. - A checked JavaScript boundary. A value whose shape or base type does not fit is refused before the function runs. The error names the member that did not fit, as in
argument 1 → .lines[0].quantity: expected Integer, got the string "two". Calling Essence from JavaScript has every rule.
Essence inside a JavaScript program
Essence is the pure core of a program, and the JavaScript around it is the shell. The shell (Node, Bun or a browser page) reads the request, the file or the clock. It hands plain JavaScript values to an Essence function and gets plain values back. An Integer comes back as a bigint, and an Optional as its value or undefined. A case of a choice with no payloads anywhere comes back as its name, such as "Overseas". Any other case, a Result included, comes back as an object with a $case.
There are three ways to call a .es file from JavaScript, and the guide Calling Essence from JavaScript walks through each:
loadModulefrom@essence-lang/clientcompiles a file while the host runs. It returns a promise of a module whoseexportsare JavaScript functions and values.- The Vite, esbuild and Bun plugins turn an import of a
.esfile into an ordinary import, and can write a.d.es.tsfile for TypeScript. essence build --embedwrites a bundle and a descriptor of its boundary, andloadPrebuiltloads the pair without compiling anything.
examples/quote-server, listed below, uses the Bun plugin. Its server.ts parses the request and writes the response, and the pricing rules reach it through one import:
import type { Input } from "@essence-lang/client"
import { catalog, type Order, quote } from "./rules/Pricing.es"
// …
let answer = quote(body as Input<Order>)
if (answer.$case === "Quote#Rejected") {
return toJSON(answer, 422)
}
body is the parsed JSON of the request. Input<Order> is Order with every Integer widened to bigint | number, which is what a caller may pass. A body that does not fit Order makes quote throw an EssenceMarshalError before any rule runs, and server.ts responds with a 400. Nothing in server.ts checks a field by hand.
The example programs
Four programs in the repository show Essence at full size.
examples/leagueis a six-team season as a pure program. The matches are data, and the program works out the standings and prints them withessence run.examples/quote-serveris a Bun HTTP API that prices orders. Every rule is Essence, reached through the Bun plugin.examples/client-2048is the game 2048 in the browser. The board and the moves are Essence, Vite serves them through one plugin line, and undo reads a board that was never overwritten.examples/site-checkasks several addresses for their pages at once. The requests, the bound on how many run together and the report are Essence, and a Bun script awaits the one promise the module answers with.