ƿit Language

ƿit is an actor based language. It combines a familiar syntax with strict immutability semantics.

Variables and Constants

Variables are declared with var, constants with def. All declarations must be initialized and must appear at the function body level — not inside if, while, for, or do blocks.

var x = 10
var name = "pit"
var empty = null

def pi = 3.14159   // constant, cannot be reassigned

var a = 1, b = 2, c = 3  // multiple declarations

Data Types

ƿit has eight fundamental types:

  • number — exact within a range the build selects. Arithmetic with no representable answer produces null. A whole number the build represents exactly is a fit number; is_fit tests it, and the range is target-dependent (see Semantics).
  • text — Unicode strings
  • logicaltrue or false
  • null — the absence of a value
  • array — ordered, numerically-indexed sequences
  • record — key-value records. Keys are normally text, but records can also be used as keys — record keys are effectively private, since only code holding a reference to the exact same record can access the field.
  • blob — binary data (bits, not bytes)
  • function — first-class callable values

These types are always immutable: number, text, logical, null, function. These types are initially mutable but can be made permanently immutable with stone(): array, record, blob.

Reading a missing or invalid key from a record produces null. Writing to an invalid key disrupts.

Literals

// Numbers
42
3.14
-5
0
1e3           // scientific notation (1000)

// Text
"hello"
`template ${x}`   // string interpolation
`${1 + 2}`        // expression interpolation

// Logical
true
false

// Null
null

// Arrays
[1, 2, 3]
[]

// Records
{a: 1, b: "two"}
{}

// Regex
/\d+/
/hello/i          // with flags

Operators

Arithmetic

2 + 3      // 5
5 - 3      // 2
3 * 4      // 12
12 / 4     // 3
10 % 3     // 1

Arithmetic operators work on numbers only. Give one a non-number and the result is null — see Semantics.

& joins text, converting number operands to text first:

"item " & "count"  // "item count"
"item " & 3        // "item 3"

Choosing between + and & says what you meant: + is a claim that both operands are numbers, & that the result is text.

Null selection

| returns the first non-null value:

var a = null | 3      // 3
var b = 7 | 9        // 7

Comparison

All comparisons are strict: operands are compared as they are. Operators: ==, !=, <, >, <=, >=.

Logical

/\, \/, and !. Logical operators short-circuit:

var called = false
var fn = function() { called = true; return true }
var r = false /\ fn()   // fn() not called
r = true \/ fn()        // fn() not called

&& and || are accepted as compatibility spellings for the same operations, but new code should prefer /\ and \/.

Increment and Decrement

var x = 5
x++         // returns 5, x becomes 6 (postfix)
++x         // returns 7, x becomes 7 (prefix)
x--         // returns 7, x becomes 6 (postfix)
--x         // returns 5, x becomes 5 (prefix)

Compound Assignment

+=, -=, *=, /=, %=.

Ternary

condition ? a : b — standard ternary operator.

Removing Record Fields

Assign null to a record field to remove that key from the record.

var o = {a: 1, b: 2}
o.a = null
o.a         // null
o.b         // 2

Accessing a key on a record which does not exist always returns null. Assigning to an invalid key disrupts.

Property Access

Dot and Bracket

var o = {x: 10}
o.x           // 10 (dot read)
o.x = 20      // dot write
o["x"]        // 20 (bracket read)
var key = "x"
o[key]        // 20 (computed bracket)
o["y"] = 30   // bracket write

Record as Key

Records can be used as keys in other Records.

var k = {}
var o = {}
o[k] = 42
o[k]          // 42
o[{}]         // null (different Record)
o[k] = null
o[k]          // null

Chained Access

var d = {a: {b: [1, {c: 99}]}}
d.a.b[1].c    // 99

Arrays

Arrays are distinct from Records. They are ordered, numerically-indexed sequences. Like records, accessing an illegal or non existent array index returns null; assigning to an invalid array index disrupts. Valid array indices are integers, 0 and greater.

var arr = [1, 2, 3]
arr[0]              // 1
arr[2] = 10         // [1, 2, 10]
length(arr)         // 3

Push and Pop

var a = [1, 2]
a[] = 3             // push: [1, 2, 3]
length(a)           // 3
var v = a[]         // pop: v is 3, a is [1, 2]
length(a)           // 2

Control Flow

If / Else

var x = 0
if (true) x = 1
if (false) x = 2 else x = 3
if (false) x = 4
else if (true) x = 5
else x = 6

While

var i = 0
while (i < 5) i++

// break
i = 0
while (true) {
  if (i >= 3) break
  i++
}

// continue
var sum = 0
i = 0
while (i < 5) {
  i++
  if (i % 2 == 0) continue
  sum += i
}

For

Variables cannot be declared in the for initializer. Declare them at the function body level.

var sum = 0
var i = 0
for (i = 0; i < 5; i++) sum += i

// break
sum = 0
i = 0
for (i = 0; i < 10; i++) {
  if (i == 5) break
  sum += i
}

// continue
sum = 0
i = 0
for (i = 0; i < 5; i++) {
  if (i % 2 == 0) continue
  sum += i
}

// nested
sum = 0
var j = 0
for (i = 0; i < 3; i++) {
  for (j = 0; j < 3; j++) {
    sum++
  }
}

Functions

Function Expressions

var add = function(a, b) { return a + b }
add(2, 3)           // 5

Return

A function with no return returns null. An early return exits immediately.

var fn = function() { var x = 1 }
fn()                // null

var fn2 = function() { return 1; return 2 }
fn2()               // 1

Go

go is an explicit tail call. It must be followed by a function call. The current function is replaced by the called function, so no code after the go statement runs in the current function.

var loop = function(n, acc) {
  if (n == 0) return acc
  go loop(n - 1, acc + n)
}

loop(100, 0)        // 5050

Use go when the next step is simply to continue as another function call. Use return when returning a value that has already been computed.

Arguments

Functions can have at most 4 parameters. Use a record to pass more values.

Missing arguments are null. Passing more arguments than a function accepts disrupts; when the compiler can prove the call over-applies, it refuses the compile instead of leaving the disruption for run time.

var fn = function(a, b) { return a + b }
fn(1, 2)            // 3
fn(1, 2, 3)         // refused: fn expects 2 args, called with 3

var fn2 = function(a, b) { return a }
fn2(1)              // 1 (b is null)

// More than 4 parameters — use a record
var draw = function(shape, opts) {
  // opts.x, opts.y, opts.color, ...
}

Closures

Functions capture variables from their enclosing scope.

var make = function(x) {
  return function(y) { return x + y }
}
var add5 = make(5)
add5(3)             // 8

Captured variables can be mutated:

var counter = function() {
  var n = 0
  return function() { n = n + 1; return n }
}
var c = counter()
c()                 // 1
c()                 // 2

Identifiers

Identifiers can contain ? and ! characters, both as suffixes and mid-name.

var nil? = function(x) { return x == null }
nil?(null)          // true
nil?(42)            // false

var set! = function(x) { return x + 1 }
set!(5)             // 6

var is?valid = function(x) { return x > 0 }
is?valid(3)         // true

var do!stuff = function() { return 42 }
do!stuff()          // 42

The ? in an identifier is not confused with the ternary operator:

var nil? = function(x) { return x == null }
var a = nil?(null) ? "yes" : "no"   // "yes"

Type Checking

Falsy values: false, 0, "", null. Everything else is truthy.

Regex

Regex literals are written with forward slashes, with optional flags.

var r = /\d+/
var result = extract("abc123", r)
result[0]               // "123"

var ri = /hello/i
var result2 = extract("Hello", ri)
result2[0]              // "Hello"

Error Handling

ƿit uses disrupt and disruption for error handling. A disrupt signals that something went wrong. The disruption block attached to a function catches it.

var safe_divide = function(a, b) {
  if (b == 0) disrupt
  return a / b
} disruption {
  log.error("something went wrong")
}

disrupt is a bare keyword — it does not carry a value. The disruption block knows that something went wrong, but not what.

Re-raising

A disruption block can re-raise by calling disrupt again:

var outer = function() {
  var inner = function() { disrupt } disruption { disrupt }
  inner()
} disruption {
  // caught here after re-raise
}
outer()

Testing for Disruption

var should_disrupt = function(fn) {
  var caught = false
  var wrapper = function() {
    fn()
  } disruption {
    caught = true
  }
  wrapper()
  return caught
}

If an actor has an unhandled disruption, it crashes.