Skip to content

Mental Model ​

Bonsai reads like JavaScript, so most expressions do what you expect. This page covers the ideas behind the language and the few rules that differ from JavaScript.

An expression is text plus an environment ​

You store expression text (in a database, a config file, a form field) and evaluate it against a context object. The environment decides what the text may refer to: the declared variables, the built-in functions, and your host functions. There is nothing else in scope: no globals, no Math, no Date, no window.

bonsai
order.total >= threshold && order.country in ["GB", "IE"] // => true

One kind of absent value: null ​

A missing property, a missing variable, and a host undefined all read as null. There is no undefined in the language, so x == null means "missing or null". A host key holding undefined is absent everywhere, so a record means the same whether it came from JSON (key left out) or from an object that set the key to undefined: it is not listed by keys, has and in report it missing, and == ignores it.

bonsai
user.middleName == null // => true
user.nickname == null // => true
user.address.city // => null
user.nickname ?? user.name // => "Ada"

Reading a property of null gives null rather than an error, so ?. is only needed for calls: user.nickname?.toUpperCase().

Supply defaults where a value may be absent: order.discount ?? 0 before arithmetic, and ?? 0 after an aggregate of a list that may be empty, since avg, min, and max of an empty list return null (sum returns 0).

bonsai
avg(scores) // => null
avg(scores) ?? 0 // => 0
sum(scores) // => 0

Values compare by value, with no coercion ​

== compares lists and maps deeply and never converts between types. Operators that would silently coerce in JavaScript are errors instead.

bonsai
[1, [2, 3]] == [1, [2, 3]] // => true
{ a: 1, b: 2 } == { b: 2, a: 1 } // => true
1 == "1" // => false
"total: " + 5 // error: TYPE_ERROR
`total: ${5}` // => "total: 5"

Booleans are booleans ​

&&, ||, !, and the test of ?: take booleans. null counts as false; anything else (a number, a string, a list) is an error rather than being "truthy". Use ?? for defaults and explicit comparisons for tests.

bonsai
count > 0 || name != "" // => false
!flag // => true
count && true // error: TYPE_ERROR

Comparisons with null are false ​

<, <=, >, >= return false when either side is null, so a filter skips items that lack the field instead of failing:

bonsai
users.filter(.age >= 18).map(.name) // => ["Ada"]

Arithmetic never produces NaN or Infinity ​

Division by zero and any other non-finite result are errors. try(expr, fallback) recovers from evaluation errors when you want a default.

bonsai
visits / days // error: DIVISION_BY_ZERO
try(visits / days, 0) // => 0

Every function is also a method ​

There is one function namespace. f(x, a) and x.f(a) are the same call, and the name always resolves to a built-in or a host function, never to something stored in the data. That is why methods chain on any value the function accepts:

bonsai
sum(map(items, .price * .qty)) // => 18
items.map(.price * .qty).sum() // => 18

. is the current item ​

Inside an argument that the function treats as a lambda (filter, map, sortBy, ...), a leading . refers to the current item. When items nest, name the outer item with an arrow lambda. See Lambdas.

Expressions always terminate ​

There are no loops, recursion, or assignment. Work proportional to data is counted against a step budget, and produced strings and lists are size-limited, so a hostile expression cannot hang your process. See Safety and Limits.

Check first, then run ​

Declare your data with t and the checker reports typos, type errors, and possible nulls before an expression runs. Without declarations everything is typed any and checked at run time instead, so you can adopt types gradually. See Static Checking.

Released under the MIT License.