blots 0.13.1

A small, simple, expression-oriented programming language.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# Blots

Blots is a small, dynamic, functional, expression-oriented programming language designed to be quick to learn, easy to use, and to produce code that's readable yet reasonably compact. Blots is intended for quick calculations and data transformation.

## Installation

### Installing a Prebuilt Binary

#### Homebrew

`brew install paul-russo/tap/blots`

### Building from Source, Using `cargo`

> If you don't have Rust installed, you can use [rustup](https://rustup.rs/) to install the latest stable version of Rust, including the `cargo` tool.

```
cargo install blots
```

## The Blots Language

### Core Types

- **Number**: 64-bit float with decimal/sci-notation support, `_` separators, and `0b`/`0x` prefixes for binary/hex literals (e.g., `1_000_000`, `3.14e-2`, `0b1010`, `0xFF`)
- **String**: Single or double quotes (`'hello'`, `"world"`); concatenate with `+`
- **Boolean**: `true`, `false`; operators: `and`/`&&`, `or`/`||`, `not`/`!`
- **List**: Ordered collection `[1, 2, 3]`; access with `list[index]` (0-based, or negative to count from end: `list[-1]` is the last element); spread with `[...list1, ...list2]`
- **Record**: JSON compatible. Key-value pairs `{a: 1, "hello there": "hi"}`; key shorthand `{foo}`; access with `record.a` or `record["key"]`
- **Function**: `x => x+1`, `(x,y?) => x + (y ?? 0)`, `(f, ...rest) => map(rest,f)`
- **Null**: `null`

### Operators & Control Flow

- **Arithmetic**: `+ - * / % ^ !`
- **Comparison**: `== != < <= > >=` (with broadcasting)
- **Non-broadcasting comparison**: `.== .!= .< .<= .> .>=` (use these to compare entire lists as whole values)
- **Logic**: `&& || !` or `and or not`
- **Special**: `??` (null-coalesce), `...` (spread)
- **Conditional**: `if cond then expr else expr`

### Bindings

There are no mutable variables in Blots. Instead, values are _bound_ to a _name_. Once created, a _binding_ cannot be mutated; it's the same for the life of the program. This property makes Blots code more "pure": it is difficult to construct an expression in Blots that can return a different result for the same inputs. This also means that functions can be losslessly serialized and `output` from programs.

### Broadcasting

Arithmetic and comparison operations automatically "broadcast" over lists, meaning they apply to each element:

```blots
[1, 2, 3] * 10     // [10, 20, 30]
[10, 20, 30] + 2   // [12, 22, 32]
[4, 5, 6] > 3      // [true, true, true]
[1, 2] == [2, 2]   // [false, true]
```

#### Dot-Prefixed Comparison Operators

Sometimes you want to compare whole values without broadcasting. The dot-prefixed comparison operators (`.==`, `.!=`, `.<`, `.<=`, `.>`, `.>=`) disable broadcasting and perform direct value comparisons:

```blots
// Regular == with broadcasting
[10, 5, 10] == 10 // [true, false, true] (equality is evaluated for each element)

// Dot operator without broadcasting
[10, 5, 10] .== 10           // false (list isn't the same type as `10`)
[10, 5, 10] .== [10, 5, 10]  // true (lists are identical)
```

##### List Comparison Algorithm

For ordering operators (`.<`, `.<=`, `.>`, `.>=`), lists are compared **lexicographically** (like dictionary ordering):

1. **Element-by-element comparison**: Lists are compared element by element from left to right
2. **First difference decides**: The first non-equal pair of elements determines the result
3. **Length as tiebreaker**: If all compared elements are equal, the shorter list is considered less than the longer list

```blots
// Element-by-element comparison
[1, 2, 3] .< [1, 2, 4]   // true  (first two elements equal, 3 < 4)
[1, 2, 3] .< [1, 3, 0]   // true  (first element equal, 2 < 3)
[2, 0, 0] .> [1, 9, 9]   // true  (first element decides: 2 > 1)

// Length comparison when elements are equal
[1, 2] .< [1, 2, 3]      // true  (all common elements equal, shorter is less)
[] .< [1]                // true  (empty list is less than any non-empty list)
[1, 2, 3] .== [1, 2]     // false (different lengths)

// Nested lists work recursively
[[1, 2], [3]] .< [[1, 2], [3, 4]]  // true  ([3] < [3, 4])
[[2]] .> [[1, 9]]        // true  ([2] > [1, 9] because 2 > 1)
```

##### Equality Comparisons

For `.==` and `.!=`, lists must be exactly equal in both structure and values:

```blots
// Deep equality check
[1, 2, 3] .== [1, 2, 3]              // true  (same values, same order)
[[1, 2], [3, 4]] .== [[1, 2], [3, 4]]  // true  (nested equality)

// Any difference makes them unequal
[1, 2, 3] .!= [1, 2, 4]              // true  (different values)
[1, 2, 3] .!= [1, 2]                 // true  (different lengths)
[1, 2, 3] .!= 123                    // true  (different types)
```

##### Mixed Type Comparisons

When comparing different types:
- Equality operators (`==`, `!=`, `.==`, `.!=`) work across all types, returning `false` or `true` respectively when types differ
- Ordering operators (`<`, `<=`, `>`, `>=` and their dot variants) **error** when types cannot be compared

```blots
"hello" .== [1, 2, 3]    // false (string != list)
"abc" .< "def"           // true  (strings compare lexicographically)
5 .< [1, 2, 3]           // ERROR: cannot compare number with list
null > 0                 // ERROR: cannot compare null with number
```

### `via` and `into`

The `via` operator takes a value and sends it through a function, applying the function to each element if the value is a list. For example:

```blots
'hello' via uppercase // 'HELLO' (because uppercase('hello') = 'HELLO')
['hello', 'world'] via uppercase // ['HELLO', 'WORLD'] (because [uppercase('hello') = 'HELLO', uppercase('world') = 'WORLD'])
```

`into` works exactly the same as `via`, except there is no broadcasting. This means that you can "reduce" a list into a single value (though you could also produce another list). Example:

```blots
'hello' into head // 'h' (because head('hello') = 'h')
['hello', 'world'] via head // ['h', 'w'] (because [head('hello') = 'h', head('world') = 'w'])
['hello', 'world'] into head // 'hello' (because head(['hello', 'world']) = 'hello')
```

### `where`

The `where` operator filters a list based on a predicate function. It's analogous to `filter` in the same way that `via` is analogous to `map`. The predicate function must return a boolean value, and only elements for which the predicate returns `true` are included in the result. For example:

```blots
[1, 2, 3, 4, 5] where x => x > 3           // [4, 5]
[1, 2, 3, 4, 5] where x => x % 2 == 0      // [2, 4]
["apple", "banana", "cherry"] where s => s == "banana"  // ["banana"]
```

The predicate function can accept either one argument (the element) or two arguments (the element and its index):

```blots
[10, 20, 30, 40] where (val, idx) => idx > 0       // [20, 30, 40] (filters out the first element)
[10, 20, 30, 40] where (val, idx) => idx % 2 == 0  // [10, 30] (keeps elements at even indices)
```

Important notes about `where`:
- The left side must be a list (using `where` with a scalar will result in an error)
- The right side must be a function that returns a boolean value
- If the predicate returns a non-boolean value, the program will error
- The `where` operator only works with lists, unlike `via` which also works with scalars

### Chaining `via`, `into`, and `where`

These operators can be naturally chained together:

```blots
// Chaining works intuitively at the top level
[1,2,3] via x => x * 2 where y => y > 2  // [4, 6]

// Chain as many operations as you want
[1,2,3,4,5,6] via x => x * 2 where y => y > 5 via z => z + 1  // [7, 9, 11, 13]

// Or assign intermediate results for clarity
doubled = [1,2,3] via x => x * 2
doubled where x => x > 2  // [4, 6]
```

**Note:** If you need to use `via`, `into`, or `where` *inside* a lambda body (not at the top level), you must wrap the expression in parentheses:

```blots
// ❌ This won't parse (via/into/where not allowed in lambda bodies without parens)
[[1,2,3], [4,5,6]] via list => list via x => x * 2

// ✅ Use parentheses to enable via/into/where inside lambda bodies
[[1,2,3], [4,5,6]] via list => (list via x => x * 2)  // [[2,4,6], [8,10,12]]
```

### `do` Blocks

Blots is an expression-oriented language, in the sense that every statement in a Blots program should evaluate to a useful value. This works well with a functional approach, where you compose functions to compute values. However, sometimes it's more intuitive to represent a computation as a series of discrete steps that happen one after another, instead of composing functions. For these cases, you can use `do` blocks to create an expression whose final value is the result of imperative code with intermediate variables:

```blots
result = do {
  y = x * 2
  z = -y
  return z          
}
```

Some things to note about `do` blocks:
- Since each `do` block is an expression and needs to evaluate to a single value, it must end with a `return` statement.
- Statements in `do` blocks are separated by newlines, just like other statements in Blots. Alternatively, if you want to keep things more compact, you can use semicolons (`;`) to separate statements on the same line.

### Inputs and Outputs

#### Inputs

The Blots CLI accepts JSON values as inputs, either as piped input or via the `--input` (`-i`) flag:
```bash
blots -i '{ "name": "Paul" }'
```

All input values are merged together and made available via the `inputs` record:
```blots
output greeting = "Hey " + inputs.name // "Hey Paul"
```

##### Input Shorthand Syntax

For convenience, you can use the `#` character as shorthand for `inputs.`:
```blots
// These are equivalent:
output greeting = "Hey " + #name
output greeting = "Hey " + inputs.name

// Useful with the coalesce operator for default values:
principal = #principal ?? 1000
years = #years ?? 10
```

The `#field` syntax works everywhere `inputs.field` works and returns `null` for missing fields (making it compatible with the `??` operator).

JSON arrays and primitive values (numbers, strings, booleans, and `null`) can be passed directly as inputs as well:
```bash
blots -i '[1,2,3]'
```

These unnamed inputs are named like `value_{1-based index}`:
```blots
output total = sum(...inputs.value_1) // 6
```

##### More Input Examples

**Multiple inputs:**
```bash
# Combine multiple JSON inputs
blots -i '{"x": 10}' -i '{"y": 20}' "output total = inputs.x + inputs.y"
# Output: {"total": 30}
```

**Piped input:**
```bash
# Pipe JSON data into Blots
echo '{"items": [1,2,3,4,5]}' | blots -e "output average = avg(...inputs.items)"
# Output: {"average": 3}

# Process command output
curl -s "https://api.example.com/data.json" | blots -e "output count = len(inputs.results)"
# Output: { "count": 20 }
```


#### Outputs

Use the `output` keyword to include bound values in the `outputs` record. This record will be sent to stdout as a JSON object when your Blots program successfully executes (or when you close an interactive Blots session). The `output` keyword can be used in two ways:

```blots
// For new bindings
output one = 1

// For existing bindings
answer = 42
output answer
```

The above example would yield this output:

```json
{ "one": 1, "answer": 42 }
```

##### More Output Examples

**Multiple outputs:**
```blots
// Calculate statistics from input data
data = inputs.values
output mean = avg(...data)
output min_val = min(...data)
output max_val = max(...data)
output std_dev = sqrt(avg(...map(data, x => (x - mean)^2)))
```

**Structured outputs:**
```blots
// Return nested data structures
output result = {
  summary: {
    total: sum(...inputs.items),
    count: len(inputs.items)
  },
  processed: map(inputs.items, x => x * 2)
}
```

**Function output:**

Functions are serialized as JSON objects with a special `__blots_function` key containing the function source code. Simple functions are output as-is:

```blots
output double = x => x * 2
```

```json
{ "double": { "__blots_function": "(x) => x * 2" } }
```

Closure values are inlined into the function body, making functions self-contained and losslessly serializable:

```blots
multiplier = 10
output scale = x => x * multiplier
```

```json
{ "scale": { "__blots_function": "(x) => x * 10" } }
```

Functions can be passed as inputs to other Blots programs, enabling function composition across programs.

**Using outputs with other tools:**
```bash
# Format output with jq
blots -i '[1,2,3,4,5]' "output stats = {minimum: min(...inputs.value_1), maximum: max(...inputs.value_1)}" | jq

# Save output to file
blots "output data = range(1, 11) via (x => x^2)" -o squares.json
# Or:
blots "output data = range(1, 11) via (x => x^2)" > squares.json

# Chain Blots programs
blots "output nums = range(1, 6)" | blots "output squares = inputs.nums via x => x^2"
```

### Comments

Comments start with `//`, and run until the end of the line:

```blots
// This is a comment
x = 42 // This is also a comment
```

### Built-in Functions

#### Math Functions
- `sqrt(x)` - returns the square root of x
- `sin(x)` - returns the sine of x (in radians)
- `cos(x)` - returns the cosine of x (in radians)
- `tan(x)` - returns the tangent of x (in radians)
- `asin(x)` - returns the arcsine of x (in radians)
- `acos(x)` - returns the arccosine of x (in radians)
- `atan(x)` - returns the arctangent of x (in radians)
- `log(x)` - returns the natural logarithm of x
- `log10(x)` - returns the base-10 logarithm of x
- `exp(x)` - returns _e_ raised to the power of x
- `abs(x)` - returns the absolute value of x
- `floor(x)` - returns the largest integer less than or equal to x (e.g. `2.7` becomes `2` and `-2.7` becomes `-3`)
- `ceil(x)` - returns the smallest integer greater than or equal to x (e.g. `2.1` becomes `3` and `-4.5` becomes `-4`)
- `round(x)` - returns x rounded to the nearest integer (e.g. `2.7` becomes `3`)
- `trunc(x)` - returns the integer part of x (removes fractional part) (e.g. both `2.7` and `2.1` become `2`, and both `-2.7` and `-2.1` become `-2`)
- `random(seed)` - returns a pseudo-random number in the range [0, 1) based on the given seed. The same seed always produces the same result, making it deterministic and reproducible. Use different seeds to generate different random numbers (e.g., `random(42)` always returns the same value, while `[1, 2, 3, 4, 5] via random` generates five different random numbers)

#### Aggregate Functions
- `min(list)` or `min(...args)` - returns the minimum given value from a list
- `max(list)` or `max(...args)` - returns the maximum value from a list
- `avg(list)` or `avg(...args)` - returns the average (mean) of values in a list
- `sum(list)` or `sum(...args)` - returns the sum of all values in a list
- `prod(list)` or `prod(...args)` - returns the product of all values in a list
- `median(list)` or `median(...args)` - returns the median value from a list
- `percentile(list, p)` - returns the percentile value at position p (0-100) from a list

#### List Functions
- `range(n)` - returns `[0, 1, ..., n-1]`
- `range(start, end)` - returns `[start, start+1, ..., end-1]`
- `len(list)` - returns the length of a list
- `head(list)` - returns the first element of a list
- `tail(list)` - returns all but the first element of a list
- `slice(list, start, end)` - returns a sublist from start (inclusive) to end (exclusive) indices
- `concat(list1, list2, ...)` - concatenates multiple lists
- `dot(list1, list2)` - returns the dot product of two lists
- `unique(list)` - returns unique elements from a list
- `sort(list)` - returns a sorted copy of the list (ascending)
- `sort_by(list, fn)` - sorts a list using a comparison function
- `reverse(list)` - returns a reversed copy of the list
- `any(list)` - returns true if *any* element in the list is `true`
- `all(list)` - returns true if *all* elements in the list are `true`
- `flatten(list)` - flattens nested lists by one level (e.g., `[[1, 2], [3, 4]]` becomes `[1, 2, 3, 4]`)
- `zip(list1, list2, ...)` - combines multiple lists into a list of tuples; pads shorter lists with `null` (e.g., `zip([1, 2], ["a", "b"])` returns `[[1, "a"], [2, "b"]]`)
- `chunk(list, n)` - splits a list into sublists of size n; the last chunk may be smaller (e.g., `chunk([1, 2, 3, 4, 5], 2)` returns `[[1, 2], [3, 4], [5]]`)
- `group_by(list, fn)` - groups elements by the result of fn (must return a string); returns a record of lists (e.g., `group_by(["apple", "banana"], x => slice(x, 0, 1))` returns `{"a": ["apple"], "b": ["banana"]}`)
- `count_by(list, fn)` - counts elements by the result of fn (must return a string); returns a record of counts (e.g., `count_by(["a", "b", "a"], x => x)` returns `{"a": 2, "b": 1}`)

#### Higher-Order Functions
- `map(list, fn)` - applies a function to each element of a list
- `reduce(list, fn, initial)` - reduces a list to a single value using a function
- `filter(list, fn)` - returns elements where the function returns true
- `every(list, fn)` - returns true if all elements satisfy the predicate
- `some(list, fn)` - returns true if any element satisfies the predicate

#### String Functions
- `split(string, delimiter)` - splits a string into a list
- `join(list, delimiter)` - joins a list into a string
- `replace(string, search, replacement)` - replaces occurrences in a string
- `trim(string)` - removes leading and trailing whitespace
- `uppercase(string)` - converts string to uppercase
- `lowercase(string)` - converts string to lowercase
- `includes(string, substring)` - checks if string contains substring
- `format(string, ...values)` - formats a string with placeholder values (e.g. `format("answer: {}", 42))

#### Type Functions
- `typeof(value)` - returns the type of a value ("number", "string", "boolean", "null", "list", "record", "built-in function", or "function")
- `arity(fn)` - returns the minimum number of parameters a function expects
- `to_string(value)` - converts a value to its string representation
- `to_number(value)` - converts a string or boolean to a number. If value is a string, parses it as a floating-point number. If value is a boolean, returns 1 for `true` and 0 for `false`.
- `to_bool(number)` - converts a number to a boolean. If the number is 0, then returns `false`. Otherwise, returns `true`.

#### Record Functions
- `keys(record)` - returns a list of all keys in a record
- `values(record)` - returns a list of all values in a record
- `entries(record)` - returns a list of [key, value] pairs from a record

#### Unchecked Comparison Functions
- `ugt(a, b)` - unchecked greater than: returns `true` if `a > b`, `false` otherwise (including when types cannot be compared)
- `ult(a, b)` - unchecked less than: returns `true` if `a < b`, `false` otherwise
- `ugte(a, b)` - unchecked greater than or equal: returns `true` if `a >= b`, `false` otherwise
- `ulte(a, b)` - unchecked less than or equal: returns `true` if `a <= b`, `false` otherwise

#### Unit Conversion
- `convert(value, from_unit, to_unit)` - converts a numeric value from one unit to another

The `convert` function supports 200+ units across 19 categories:
- **Angle**: degrees, radians, gradians, revolutions, arc minutes, arc seconds
- **Area**: square meters/kilometers/miles/feet/etc., acres, hectares
- **Concentration of Mass**: grams per liter, milligrams per deciliter
- **Duration**: seconds, minutes, hours, days, weeks, milliseconds, etc.
- **Electric Charge**: coulombs, ampere hours (with metric prefixes)
- **Electric Current**: amperes (with metric prefixes)
- **Electric Potential Difference**: volts (with metric prefixes)
- **Electric Resistance**: ohms (with metric prefixes)
- **Energy**: joules, calories, kilocalories, kilowatt hours
- **Frequency**: hertz (with metric prefixes)
- **Fuel Efficiency**: liters per 100km, miles per gallon
- **Information Storage**: bits, bytes, kilobytes, kibibytes, megabytes, mebibytes, etc.
- **Length**: meters, kilometers, miles, feet, inches, nautical miles, light years, etc.
- **Mass**: kilograms, grams, pounds, ounces, tons, stones, etc.
- **Power**: watts (with metric prefixes), horsepower
- **Pressure**: pascals, bars, atmospheres, psi, mmHg, inHg
- **Speed**: meters per second, kilometers per hour, miles per hour, knots
- **Temperature**: Celsius, Fahrenheit, Kelvin
- **Volume**: liters, gallons, cubic meters, cups, pints, quarts, etc.

Units can be specified by full name or abbreviation and comparisons default to case-insensitive matching. Metric units support both American ("meter") and British ("metre") spellings. When multiple units share the same lowercase alias (for example `mA` vs `MA`), `convert` returns an ambiguous-unit error; provide the canonical casing shown below to disambiguate.

Examples:
```blots
convert(100, "celsius", "fahrenheit")  // 212
convert(5, "km", "miles")              // 3.1068559611866697
convert(1, "kg", "lbs")                // 2.2046226218487757
convert(1024, "bytes", "kibibytes")    // 1
convert(180, "degrees", "radians")     // 3.141592653589793 (π)
convert(1, "kilowatt", "watts")        // 1000
```

### Constants

Access mathematical constants via `constants.*`:
- `constants.pi`: The mathematical constant _π_.
- `constants.e`: The mathematical constant _e_.
- `constants.max_value`: The maximum value that can be represented as a 64-bit floating point number.
- `constants.min_value`: The minimum non-zero value that can be represented as a 64-bit floating point number.

## Tools

There's a language support [extension](https://github.com/paul-russo/blots-syntax) for Blots, available on both the [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=Blots.blots-syntax) and the [Open VSX Registry](https://open-vsx.org/extension/blots/blots-syntax). You should be able to install it from within your editor like other extensions, but you can also download the VSIX file directly from either directory.

For Vim users, there's also a [Vim plugin](https://github.com/paul-russo/blots-vim) providing syntax highlighting for `.blots` files.