Language Manual
ƿit is a scripting language for actor-based programming. It combines a familiar syntax with a prototype-based object system and strict immutability semantics.
Basics
Variables and Constants
var x = 10 // mutable variable (block-scoped like let)
def PI = 3.14159 // constant (cannot be reassigned)
Data Types
ƿit has six fundamental types:
- number — DEC64 decimal floating point (no rounding errors)
- text — Unicode strings
- logical —
trueorfalse - null — the absence of a value (no
undefined) - array — ordered, numerically-indexed sequences
- object — key-value records with prototype inheritance
- blob — binary data (bits, not bytes)
- function — first-class callable values
Literals
// Numbers
42
3.14
1_000_000 // underscores for readability
// Text
"hello"
'world'
`template ${x}` // string interpolation
// Logical
true
false
// Null
null
// Arrays
[1, 2, 3]
["a", "b", "c"]
// Objects
{name: "pit", version: 1}
{x: 10, y: 20}
Operators
// Arithmetic
+ - * / %
** // exponentiation
// Comparison (always strict)
== // equals (like === in JS)
!= // not equals (like !== in JS)
< > <= >=
// Logical
&& || !
// Assignment
= += -= *= /=
Control Flow
// Conditionals
if (x > 0) {
log.console("positive")
} else if (x < 0) {
log.console("negative")
} else {
log.console("zero")
}
// Ternary
var sign = x > 0 ? 1 : -1
// Loops
for (var i = 0; i < 10; i++) {
log.console(i)
}
for (var item of items) {
log.console(item)
}
for (var key in obj) {
log.console(key, obj[key])
}
while (condition) {
// body
}
// Control
break
continue
return value
disrupt
Functions
// Named function
function add(a, b) {
return a + b
}
// Anonymous function
var multiply = function(a, b) {
return a * b
}
// Arrow function
var square = x => x * x
var sum = (a, b) => a + b
// Rest parameters
function log_all(...args) {
for (var arg of args) log.console(arg)
}
// Default parameters
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}!`
}
All closures capture this (like arrow functions in JavaScript).
Arrays
Arrays are distinct from objects. They are ordered, numerically-indexed sequences. You cannot add arbitrary string keys to an array.
var arr = [1, 2, 3]
arr[0] // 1
arr[2] = 10 // [1, 2, 10]
length(arr) // 3
// Array spread
var more = [...arr, 4, 5] // [1, 2, 10, 4, 5]
Objects
Objects are key-value records with prototype-based inheritance.
var point = {x: 10, y: 20}
point.x // 10
point["y"] // 20
// Object spread
var point3d = {...point, z: 30}
// Prototype inheritance
var colored_point = {__proto__: point, color: "red"}
colored_point.x // 10 (inherited)
Prototypes
// Create object with prototype
var child = meme(parent)
// Get prototype
var p = proto(child)
// Check prototype chain
isa(child, parent) // true
Immutability with Stone
The stone() function makes values permanently immutable.
var config = stone({
debug: true,
maxRetries: 3
})
config.debug = false // Error! Stone objects cannot be modified
Stone is deep — all nested objects and arrays are also frozen. This cannot be reversed.
stone.p(value) // returns true if value is stone
Built-in Functions
length(value)
Returns the length of arrays (elements), text (codepoints), blobs (bits), or functions (arity).
length([1, 2, 3]) // 3
length("hello") // 5
length(function(a,b){}) // 2
use(path)
Import a module. Returns the cached, stone value.
var math = use('math/radians')
var json = use('json')
isa(value, type)
Check type or prototype chain.
is_number(42) // true
is_text("hi") // true
is_array([1,2]) // true
is_object({}) // true
isa(child, parent) // true if parent is in prototype chain
reverse(array)
Returns a new array with elements in reverse order.
reverse([1, 2, 3]) // [3, 2, 1]
logical(value)
Convert to boolean.
logical(0) // false
logical(1) // true
logical("true") // true
logical("false") // false
logical(null) // false
Logging
log.console("message") // standard output
log.error("problem") // error output
Pattern Matching
ƿit supports regex patterns in string functions, but not standalone regex objects.
text.search("hello world", /world/)
replace("hello", /l/g, "L")
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.
To test whether an operation disrupts:
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.
ƿit organizes code into two types of scripts: modules (.cm) and actors (.ce).
The Actor Model
ƿit is built on the actor model of computation. Each actor:
- Has its own isolated memory — actors never share state
- Runs to completion each turn — no preemption
- Performs its own garbage collection
- Communicates only through message passing
This isolation makes concurrent programming safer and more predictable.
Modules (.cm)
A module is a script that returns a value. The returned value is cached and frozen (made stone).
// math_utils.cm
var math = use('math/radians')
function distance(x1, y1, x2, y2) {
var dx = x2 - x1
var dy = y2 - y1
return math.sqrt(dx * dx + dy * dy)
}
function midpoint(x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
}
}
return {
distance: distance,
midpoint: midpoint
}
Key properties:
- Must return a value — it’s an error not to
- Executed once per actor — subsequent
use()calls return the cached value - Return value is stone — immutable, safe to share
- Modules can import other modules with
use()
Using Modules
var utils = use('math_utils')
var d = utils.distance(0, 0, 3, 4) // 5
Actors (.ce)
An actor is a script that does not return a value. It runs as an independent unit of execution.
// worker.ce
log.console("Worker started")
$receiver(function(msg, reply) {
log.console("Received:", msg)
// Process message...
})
Key properties:
- Must not return a value — it’s an error to return
- Has access to actor intrinsics (functions starting with
$) - Runs until explicitly stopped or crashes
Actor Intrinsics
Actors have access to special functions prefixed with $:
$me
Reference to the current actor.
log.console($me) // actor reference
$stop()
Stop the current actor.
$stop()
$send(actor, message, callback)
Send a message to another actor.
$send(other_actor, {type: "ping", data: 42}, function(reply) {
log.console("Got reply:", reply)
})
Messages are automatically splatted — flattened to plain data without prototypes.
$start(callback, program)
Start a new actor from a script.
$start(function(new_actor) {
log.console("Started:", new_actor)
}, "worker")
$delay(callback, seconds)
Schedule a callback after a delay.
$delay(function() {
log.console("5 seconds later")
}, 5)
$clock(callback)
Get called every frame/tick.
$clock(function(dt) {
// Called each tick with delta time
})
$receiver(callback)
Set up a message receiver.
$receiver(function(message, reply) {
// Handle incoming message
reply({status: "ok"})
})
$portal(callback, port)
Open a network port.
$portal(function(connection) {
// Handle new connection
}, 8080)
$contact(callback, record)
Connect to a remote address.
$contact(function(connection) {
// Connected
}, {host: "example.com", port: 80})
$time_limit(requestor, seconds)
Wrap a requestor with a timeout.
$time_limit(my_requestor, 10) // 10 second timeout
Module Resolution
When you call use('name'), ƿit searches:
- Current package — files relative to package root
- Dependencies — packages declared in
pit.toml - Core — built-in ƿit modules
// From within package 'myapp':
use('utils') // myapp/utils.cm
use('helper/math') // myapp/helper/math.cm
use('json') // core json module
use('otherlib/foo') // dependency 'otherlib', file foo.cm
Files starting with underscore (_helper.cm) are private to the package.
Example: Simple Actor System
// main.ce - Entry point
var config = use('config')
log.console("Starting application...")
$start(function(worker) {
$send(worker, {task: "process", data: [1, 2, 3]})
}, "worker")
$delay(function() {
log.console("Shutting down")
$stop()
}, 10)
// worker.ce - Worker actor
$receiver(function(msg, reply) {
if (msg.task == "process") {
var result = array(msg.data, x => x * 2)
reply({result: result})
}
})
// config.cm - Shared configuration
return {
debug: true,
timeout: 30
}
Requestors are functions that encapsulate asynchronous work. They provide a structured way to compose callbacks, manage cancellation, and coordinate concurrent operations between actors.
What is a Requestor
A requestor is a function with this signature:
function my_requestor(callback, value) {
// Do async work, then call callback with result
// Return a cancel function
}
- callback — called when the work completes:
callback(value, reason)- On success:
callback(result)orcallback(result, null) - On failure:
callback(null, reason)where reason explains the failure
- On success:
- value — input passed from the previous step (or the initial caller)
- return — a cancel function, or null if cancellation is not supported
The cancel function, when called, should abort the in-progress work.
Writing a Requestor
function fetch_data(callback, url) {
$contact(function(connection) {
$send(connection, {get: url}, function(response) {
callback(response)
})
}, {host: url, port: 80})
return function cancel() {
// clean up if needed
}
}
A requestor that always succeeds immediately:
function constant(callback, value) {
callback(42)
}
A requestor that always fails:
function broken(callback, value) {
callback(null, "something went wrong")
}
Composing Requestors
ƿit provides four built-in functions for composing requestors into pipelines.
sequence(requestor_array)
Run requestors one after another. Each result becomes the input to the next. The final result is passed to the callback.
var pipeline = sequence([
fetch_user,
validate_permissions,
load_profile
])
pipeline(function(profile, reason) {
if (reason) {
log.error(reason)
} else {
log.console(profile.name)
}
}, user_id)
If any step fails, the remaining steps are skipped and the failure propagates.
parallel(requestor_array, throttle, need)
Start all requestors concurrently. Results are collected into an array matching the input order.
var both = parallel([
fetch_profile,
fetch_settings
])
both(function(results, reason) {
var profile = results[0]
var settings = results[1]
}, user_id)
- throttle — limit how many requestors run at once (null for no limit)
- need — minimum number of successes required (default: all)
race(requestor_array, throttle, need)
Like parallel, but returns as soon as the needed number of results arrive. Unfinished requestors are cancelled.
var fastest = race([
fetch_from_cache,
fetch_from_network,
fetch_from_backup
])
fastest(function(results) {
// results[0] is whichever responded first
}, request)
Default need is 1. Useful for redundant operations where only one result matters.
fallback(requestor_array)
Try each requestor in order. If one fails, try the next. Return the first success.
var resilient = fallback([
fetch_from_primary,
fetch_from_secondary,
use_cached_value
])
resilient(function(data, reason) {
if (reason) {
log.error("all sources failed")
}
}, key)
Timeouts
Wrap any requestor with $time_limit to add a timeout:
var timed = $time_limit(fetch_data, 5) // 5 second timeout
timed(function(result, reason) {
// reason will explain timeout if it fires
}, url)
If the requestor does not complete within the time limit, it is cancelled and the callback receives a failure.
Requestors and Actors
Requestors are particularly useful with actor messaging. Since $send is callback-based, it fits naturally:
function ask_worker(callback, task) {
$send(worker, task, function(reply) {
callback(reply)
})
}
var pipeline = sequence([
ask_worker,
process_result,
store_result
])
pipeline(function(stored) {
log.console("done")
$stop()
}, {type: "compute", data: [1, 2, 3]})
Packages are the fundamental unit of code organization and sharing in ƿit.
Package Structure
A package is a directory containing a pit.toml manifest:
mypackage/
├── pit.toml # package manifest
├── main.ce # entry point (optional)
├── utils.cm # module
├── helper/
│ └── math.cm # nested module
├── render.c # C extension
└── _internal.cm # private module (underscore prefix)
pit.toml
The package manifest declares metadata and dependencies:
package = "mypackage"
version = "1.0.0"
[dependencies]
prosperon = "gitea.pockle.world/john/prosperon"
mylib = "/Users/john/work/mylib"
Fields
- package — canonical package name
- version — semantic version
- dependencies — map of alias to package locator
Module Resolution
When importing with use(), ƿit searches in order:
- Local package — relative to package root
- Dependencies — via aliases in
pit.toml - Core — built-in ƿit modules
// In package 'myapp' with dependency: renderer = "gitea.pockle.world/john/renderer"
use('utils') // myapp/utils.cm
use('helper/math') // myapp/helper/math.cm
use('renderer/sprite') // renderer package, sprite.cm
use('json') // core module
Private Modules
Files starting with underscore are private:
// _internal.cm is only accessible within the same package
use('internal') // OK from same package
use('myapp/internal') // Error from other packages
Package Locators
Remote Packages
Hosted on Gitea servers:
gitea.pockle.world/user/repo
Local Packages
Absolute filesystem paths:
/Users/john/work/mylib
Local packages are symlinked into the shop, making development seamless.
The Shop
ƿit stores all packages in the shop at ~/.pit/:
~/.pit/
├── packages/
│ ├── core -> gitea.pockle.world/john/cell
│ ├── gitea.pockle.world/
│ │ └── john/
│ │ ├── cell/
│ │ └── prosperon/
│ └── Users/
│ └── john/
│ └── work/
│ └── mylib -> /Users/john/work/mylib
├── lib/
│ ├── local.dylib
│ └── gitea_pockle_world_john_prosperon.dylib
├── build/
│ └── <content-addressed cache>
├── cache/
│ └── <downloaded zips>
├── lock.toml
└── link.toml
lock.toml
Tracks installed package versions:
[gitea.pockle.world/john/prosperon]
type = "gitea"
commit = "abc123..."
updated = 1702656000
link.toml
Development links override package resolution:
[gitea.pockle.world/john/prosperon]
target = "/Users/john/work/prosperon"
Installing Packages
# Install from remote
pit install gitea.pockle.world/john/prosperon
# Install from local path
pit install /Users/john/work/mylib
Updating Packages
# Update all
pit update
# Update specific package
pit update gitea.pockle.world/john/prosperon
Development Workflow
For active development, link packages locally:
# Link a package for development
pit link add gitea.pockle.world/john/prosperon /Users/john/work/prosperon
# Changes to /Users/john/work/prosperon are immediately visible
# Remove link when done
pit link delete gitea.pockle.world/john/prosperon
C Extensions
C files in a package are compiled into a dynamic library:
mypackage/
├── pit.toml
├── render.c # compiled to mypackage.dylib
└── render.cm # optional ƿit wrapper
The library is named after the package and placed in ~/.pit/lib/.
See Writing C Modules for details.
Platform-Specific Files
Use suffixes for platform-specific implementations:
mypackage/
├── audio.c # default implementation
├── audio_playdate.c # Playdate-specific
└── audio_emscripten.c # Web-specific
ƿit selects the appropriate file based on the build target.
The intrinsics are constants and functions that are built into the language. The use statement is not needed to access them.
A programmer is not obliged to consult the list of intrinsics before naming a new variable or input. New intrinsics may be added to ƿit without breaking existing programs.
Constants
false
The value of 1 == 0. One of the two logical values.
true
The value of 1 == 1. One of the two logical values.
null
The value of 1 / 0. The null value is an empty immutable object. All attempts to obtain a value from null by refinement will produce null.
Any attempt to modify null will disrupt. Any attempt to call null as a function will disrupt.
null is the value of missing input values, missing fields in records, and invalid numbers.
pi
An approximation of circumference / diameter: 3.1415926535897932.
Creator Functions
The creator functions make new objects. Some can take various types. All return null if their inputs are not suitable.
array
array(number) — Make an array. All elements are initialized to null.
array(number, initial_value) — Make an array. All elements are initialized to initial_value. If initial_value is a function, it is called for each element. If the function has arity >= 1, it is passed the element number.
array(3) // [null, null, null]
array(3, 0) // [0, 0, 0]
array(5, i => i * 2) // [0, 2, 4, 6, 8]
array(array) — Copy. Make a mutable copy of the array.
array(array, function, reverse, exit) — Map. Call the function with each element, collecting return values in a new array. The function is passed each element and its element number.
array([1, 2, 3], x => x * 10) // [10, 20, 30]
If reverse is true, starts with the last element and works backwards.
If exit is not null, when the function returns the exit value, array returns early. The exit value is not stored into the new array.
array(array, another_array) — Concat. Produce a new array concatenating both.
array([1, 2], [3, 4]) // [1, 2, 3, 4]
array(array, from, to) — Slice. Make a mutable copy of part of an array. If from is negative, add length(array). If to is negative, add length(array).
array([1, 2, 3, 4, 5], 1, 4) // [2, 3, 4]
array([1, 2, 3], -2) // [2, 3]
array(record) — Keys. Make an array containing all text keys in the record.
array(text) — Split text into grapheme clusters.
array("hello") // ["h", "e", "l", "l", "o"]
array(text, separator) — Split text into an array of subtexts.
array(text, length) — Dice text into an array of subtexts of a given length.
logical
logical(0) // false
logical(false) // false
logical("false") // false
logical(null) // false
logical(1) // true
logical(true) // true
logical("true") // true
All other values return null.
number
number(logical) — Returns 1 or 0.
number(number) — Returns the number.
number(text, radix) — Convert text to number. Radix is 2 thru 37 (default: 10).
number(text, format) — Parse formatted numbers:
| Format | Radix | Separator | Decimal point |
|---|---|---|---|
"" | 10 | .period | |
"n" | 10 | .period | |
"u" | 10 | _underbar | .period |
"d" | 10 | ,comma | .period |
"s" | 10 | space | .period |
"v" | 10 | .period | ,comma |
"l" | 10 | locale | locale |
"b" | 2 | ||
"o" | 8 | ||
"h" | 16 | ||
"j" | auto |
number("123,456,789.10", "d") // 123456789.1
number("123.456.789,10", "v") // 123456789.1
number("666", "o") // 438
number("666", "h") // 1638
text
text(array, separator) — Convert array to text. Elements are concatenated with the separator (default: empty text).
text(number, radix) — Convert number to text. Radix is 2 thru 37 (default: 10).
text(number, format) — Format a number as text:
Real styles:
| Style | Description | Separator | Decimal |
|---|---|---|---|
"e" | Scientific | .period | |
"n" | Number | .period | |
"s" | Space | space | .period |
"u" | Underbar | _underbar | .period |
"d" | Decimal | ,comma | .period |
"c" | Comma | .period | ,comma |
"l" | Locale | locale | locale |
Integer styles:
| Style | Base | Separator |
|---|---|---|
"i" | 10 | _underbar |
"b" | 2 | _underbar |
"o" | 8 | _underbar |
"h" | 16 | _underbar |
"t" | 32 | _underbar |
The format text is: separation_digit + style_letter + places_digits
var data = 123456789.1
text(data) // "123456789.1"
text(data, "3s4") // "123 456 789.1000"
text(data, "d2") // "123,456,789.10"
text(data, "e") // "1.234567891e8"
text(data, "i") // "123456789"
text(data, "8b") // "111_01011011_11001101_00010101"
text(data, "h") // "75BCD15"
text(12, "4b8") // "0000_1100"
text(text, from, to) — Extract a substring. If from/to are negative, add length(text).
var my_text = "miskatonic"
text(my_text, 0, 3) // "mis"
text(my_text, 5) // "tonic"
text(my_text, -3) // "nic"
record
record(record) — Copy. Make a mutable copy.
record(record, another_record) — Combine. Copy a record, then put all fields of another into the copy.
record(record, array_of_keys) — Select. New record with only the named fields.
record(array_of_keys) — Set. New record using array as keys, each value is true.
record(array_of_keys, value) — Value Set. Each field value is value.
record(array_of_keys, function) — Functional Value Set. The function is called for each key to produce field values.
Sensory Functions
The sensory functions always return a logical value. In ƿit, they use the is_ prefix:
is_array([]) // true
is_blob(blob.make()) // true
is_function(x => x) // true
is_integer(42) // true
is_logical(true) // true
is_null(null) // true
is_number(3.14) // true
is_object({}) // true
is_text("hello") // true
Additional type checks: is_character, is_data, is_digit, is_false, is_fit, is_letter, is_lower, is_pattern, is_stone, is_true, is_upper, is_whitespace.
Standard Functions
abs(number)
Absolute value. Returns null for non-numbers.
apply(function, array)
Execute the function, passing the array elements as input values. If the array is longer than the function’s arity, it disrupts.
ceiling(number, place)
Round up. If place is 0 or null, round to the smallest integer >= number.
ceiling(12.3775) // 13
ceiling(12.3775, -2) // 12.38
ceiling(-12.3775) // -12
character(value)
If text, returns the first character. If a non-negative 32-bit integer, returns the character from that codepoint.
codepoint(text)
Returns the codepoint number of the first character.
extract(text, pattern, from, to)
Match text to pattern. Returns a record of saved fields, or null if no match.
fallback(requestor_array)
Returns a requestor that tries each requestor in order until one succeeds. Returns a cancel function.
filter(array, function)
Call function for each element. When it returns true, the element is included in the new array.
filter([0, 1.25, 2, 3.5, 4, 5.75], is_integer) // [0, 2, 4]
find(array, function, reverse, from)
Call function for each element. If it returns true, return the element number. If the second argument is not a function, it is compared directly to elements. Returns null if not found.
find([1, 2, 3], x => x > 1) // 1
find([1, 2, 3], 2) // 1
floor(number, place)
Round down. If place is 0 or null, round to the greatest integer <= number.
floor(12.3775) // 12
floor(12.3775, -2) // 12.37
floor(-12.3775) // -13
for(array, function, reverse, exit)
Call function with each element and its element number. If exit is not null and the function returns the exit value, return early.
format(text, collection, transformer)
Substitute {key} placeholders in text with values from a collection (array or record).
format("{0} in {1}!", ["Malmborg", "Plano"])
// "Malmborg in Plano!"
fraction(number)
Returns the fractional part of a number. See also whole.
length(value)
| Value | Result |
|---|---|
| array | number of elements |
| blob | number of bits |
| text | number of codepoints |
| function | number of named inputs (arity) |
| record | record.length() |
| other | null |
lower(text)
Returns text with all uppercase characters converted to lowercase.
max(number, number)
Returns the larger of two numbers. Returns null if either is not a number.
min(number, number)
Returns the smaller of two numbers.
modulo(dividend, divisor)
Result is dividend - (divisor * floor(dividend / divisor)). Result has the sign of the divisor.
neg(number)
Negate. Reverse the sign of a number.
normalize(text)
Unicode normalize.
not(logical)
Returns the opposite logical. Returns null for non-logicals.
parallel(requestor_array, throttle, need)
Returns a requestor that starts all requestors in the array. Results are collected into an array matching the input order. Optional throttle limits concurrent requestors. Optional need specifies the minimum number of successes required.
race(requestor_array, throttle, need)
Like parallel, but returns as soon as the needed number of results are obtained. Default need is 1. Unfinished requestors are cancelled.
reduce(array, function, initial, reverse)
Reduce an array to a single value by applying a function to pairs of elements.
reduce([1, 2, 3, 4, 5, 6, 7, 8, 9], (a, b) => a + b) // 45
remainder(dividend, divisor)
For fit integers: dividend - ((dividend // divisor) * divisor).
replace(text, target, replacement, limit)
Return text with target replaced by replacement. Target can be text or pattern. Replacement can be text or a function.
reverse(array)
Returns a new array with elements in the opposite order.
round(number, place)
Round to nearest.
round(12.3775) // 12
round(12.3775, -2) // 12.38
search(text, target, from)
Search text for target. Returns character position or null.
sequence(requestor_array)
Returns a requestor that processes each requestor in order. Each result becomes the input to the next. The last result is the final result.
sign(number)
Returns -1, 0, or 1.
sort(array, select)
Returns a new sorted array. Sort keys must be all numbers or all texts. Sort is ascending and stable.
| select type | Sort key | Description |
|---|---|---|
| null | element itself | Simple sort |
| text | element[select] | Sort by record field |
| number | element[select] | Sort by array index |
| array | select[index] | External sort keys |
sort(["oats", "peas", "beans", "barley"])
// ["barley", "beans", "oats", "peas"]
sort([{n: 3}, {n: 1}], "n")
// [{n: 1}, {n: 3}]
stone(value)
Petrify the value, making it permanently immutable. The operation is deep — all nested objects and arrays are also frozen. Returns the value.
trim(text, reject)
Remove characters from both ends. Default removes whitespace.
trunc(number, place)
Truncate toward zero.
trunc(12.3775) // 12
trunc(-12.3775) // -12
upper(text)
Returns text with all lowercase characters converted to uppercase.
whole(number)
Returns the whole part of a number. See also fraction.
ƿit includes a standard library of modules loaded with use().
| Module | Description |
|---|---|
| text | String conversion and manipulation |
| number | Numeric conversion and operations |
| array | Array creation and manipulation |
| object | Object creation and manipulation |
| blob | Binary data (bits, not bytes) |
| time | Time constants and conversions |
| math | Trigonometry, logarithms, roots |
| json | JSON encoding and decoding |
| random | Random number generation |
Most numeric functions (floor, max, abs, etc.) are global intrinsics and do not require use. See Built-in Functions for the full list.
The text function and its methods handle string conversion and manipulation.
Conversion
text(array, separator)
Convert an array to text, joining elements with a separator (default: space).
text([1, 2, 3]) // "1 2 3"
text([1, 2, 3], ", ") // "1, 2, 3"
text(["a", "b"], "-") // "a-b"
text(number, radix)
Convert a number to text. Radix is 2-36 (default: 10).
text(255) // "255"
text(255, 16) // "ff"
text(255, 2) // "11111111"
text(text, from, to)
Extract a substring from index from to to.
text("hello world", 0, 5) // "hello"
text("hello world", 6) // "world"
Methods
text.lower(text)
Convert to lowercase.
text.lower("HELLO") // "hello"
text.upper(text)
Convert to uppercase.
text.upper("hello") // "HELLO"
text.trim(text, reject)
Remove characters from both ends. Default removes whitespace.
text.trim(" hello ") // "hello"
text.trim("xxhelloxx", "x") // "hello"
text.search(text, target, from)
Find the position of target in text. Returns null if not found.
text.search("hello world", "world") // 6
text.search("hello world", "xyz") // null
text.search("hello hello", "hello", 1) // 6
text.replace(text, target, replacement, cap)
Replace occurrences of target with replacement. If cap is not specified, replaces all occurrences.
text.replace("hello", "l", "L") // "heLLo" (replaces all)
text.replace("hello", "l", "L", 1) // "heLlo" (replaces first only)
// With function
text.replace("hello", "l", function(match, pos) {
return pos == 2 ? "L" : match
}) // "heLLo" (replaces all by default)
text.format(text, collection, transformer)
Substitute {key} placeholders with values from a collection.
text.format("Hello, {name}!", {name: "World"})
// "Hello, World!"
text.format("{0} + {1} = {2}", [1, 2, 3])
// "1 + 2 = 3"
text.normalize(text)
Unicode normalize the text (NFC form).
text.normalize("cafe\u0301") // normalized form
text.codepoint(text)
Get the Unicode codepoint of the first character.
text.codepoint("A") // 65
text.extract(text, pattern, from, to)
Match a pattern and extract named groups.
text.extract("2024-01-15", /(\d+)-(\d+)-(\d+)/)
// Returns match info
The number function and its methods handle numeric conversion and operations.
Conversion
number(logical)
Convert boolean to number.
number(true) // 1
number(false) // 0
number(text, radix)
Parse text to number. Radix is 2-36 (default: 10).
number("42") // 42
number("ff", 16) // 255
number("1010", 2) // 10
number(text, format)
Parse formatted numbers.
| Format | Description |
|---|---|
"" | Standard decimal |
"u" | Underbar separator (1_000) |
"d" | Comma separator (1,000) |
"s" | Space separator (1 000) |
"v" | European (1.000,50) |
"b" | Binary |
"o" | Octal |
"h" | Hexadecimal |
"j" | JavaScript style (0x, 0o, 0b prefixes) |
number("1,000", "d") // 1000
number("0xff", "j") // 255
Methods
abs(n)
Absolute value.
abs(-5) // 5
abs(5) // 5
sign(n)
Returns -1, 0, or 1.
sign(-5) // -1
sign(0) // 0
sign(5) // 1
floor(n, place)
Round down.
floor(4.9) // 4
floor(4.567, 2) // 4.56
ceiling(n, place)
Round up.
ceiling(4.1) // 5
ceiling(4.123, 2) // 4.13
round(n, place)
Round to nearest.
round(4.5) // 5
round(4.567, 2) // 4.57
trunc(n, place)
Truncate toward zero.
trunc(4.9) // 4
trunc(-4.9) // -4
whole(n)
Get the integer part.
whole(4.9) // 4
whole(-4.9) // -4
fraction(n)
Get the fractional part.
fraction(4.75) // 0.75
min(…values)
Return the smallest value.
min(3, 1, 4, 1, 5) // 1
max(…values)
Return the largest value.
max(3, 1, 4, 1, 5) // 5
remainder(dividend, divisor)
Compute remainder.
remainder(17, 5) // 2
The array function and its methods handle array creation and manipulation.
Creation
array(number)
Create an array of specified size, filled with null.
array(3) // [null, null, null]
array(number, initial)
Create an array with initial values.
array(3, 0) // [0, 0, 0]
array(3, i => i * 2) // [0, 2, 4]
array(array)
Copy an array.
var copy = array(original)
array(array, from, to)
Slice an array.
array([1, 2, 3, 4, 5], 1, 4) // [2, 3, 4]
array([1, 2, 3], -2) // [2, 3]
array(array, another)
Concatenate arrays.
array([1, 2], [3, 4]) // [1, 2, 3, 4]
array(object)
Get keys of an object.
array({a: 1, b: 2}) // ["a", "b"]
array(text)
Split text into grapheme clusters.
array("hello") // ["h", "e", "l", "l", "o"]
array(text, separator)
Split text by separator.
array("a,b,c", ",") // ["a", "b", "c"]
array(text, length)
Split text into chunks.
array("abcdef", 2) // ["ab", "cd", "ef"]
Methods
array.for(arr, fn, reverse, exit)
Iterate over elements.
array.for([1, 2, 3], function(el, i) {
log.console(i, el)
})
// With early exit
array.for([1, 2, 3, 4], function(el) {
if (el > 2) return true
log.console(el)
}, false, true) // prints 1, 2
array.find(arr, fn, reverse, from)
Find element index.
array.find([1, 2, 3], 2) // 1
array.find([1, 2, 3], x => x > 1) // 1
array.find([1, 2, 3], x => x > 1, true) // 2 (from end)
array.filter(arr, fn)
Filter elements.
array.filter([1, 2, 3, 4], x => x % 2 == 0) // [2, 4]
array.reduce(arr, fn, initial, reverse)
Reduce to single value.
array.reduce([1, 2, 3, 4], (a, b) => a + b) // 10
array.reduce([1, 2, 3, 4], (a, b) => a + b, 10) // 20
array.sort(arr, select)
Sort array (returns new array).
array.sort([3, 1, 4, 1, 5]) // [1, 1, 3, 4, 5]
// Sort by field
array.sort([{n: 3}, {n: 1}], "n") // [{n: 1}, {n: 3}]
// Sort by index
array.sort([[3, "c"], [1, "a"]], 0) // [[1, "a"], [3, "c"]]
Map with array()
The array(arr, fn) form maps over elements:
array([1, 2, 3], x => x * 2) // [2, 4, 6]
array([1, 2, 3], function(el, i) {
return el + i
}) // [1, 3, 5]
The object function and related utilities handle object creation and manipulation.
Creation
object(obj)
Shallow copy an object.
var copy = object(original)
object(obj, another)
Combine two objects.
object({a: 1}, {b: 2}) // {a: 1, b: 2}
object({a: 1}, {a: 2}) // {a: 2}
object(obj, keys)
Select specific keys.
object({a: 1, b: 2, c: 3}, ["a", "c"]) // {a: 1, c: 3}
object(keys)
Create object from keys (values are true).
object(["a", "b", "c"]) // {a: true, b: true, c: true}
object(keys, value)
Create object from keys with specified value.
object(["a", "b"], 0) // {a: 0, b: 0}
object(keys, fn)
Create object from keys with computed values.
object(["a", "b", "c"], (k, i) => i) // {a: 0, b: 1, c: 2}
Prototypes
meme(prototype)
Create a new object with the given prototype.
var animal = {speak: function() { log.console("...") }}
var dog = meme(animal)
dog.speak = function() { log.console("woof") }
proto(obj)
Get an object’s prototype.
var p = proto(dog) // animal
isa(obj, prototype)
Check if prototype is in the chain.
isa(dog, animal) // true
Serialization
splat(obj)
Flatten an object’s prototype chain into a plain object. Only includes primitive types (numbers, text, booleans, arrays, objects).
var base = {x: 1}
var derived = meme(base)
derived.y = 2
splat(derived) // {x: 1, y: 2}
When sending objects between actors with $send, they are automatically splatted.
Key Iteration
var obj = {a: 1, b: 2, c: 3}
// Get all keys
var keys = array(obj) // ["a", "b", "c"]
// Iterate
for (var key in obj) {
log.console(key, obj[key])
}
Blobs are binary large objects — containers of bits (not bytes). They’re used for encoding data, messages, images, network payloads, and more.
States
A blob exists in one of two states:
- antestone (mutable) — write operations allowed
- stone (immutable) — read operations allowed
var blob = use('blob')
var b = blob.make() // antestone
blob.write_bit(b, 1)
blob.write_fit(b, 42, 8)
stone(b) // now stone, readable
Creation
blob.make()
Create an empty blob.
var b = blob.make()
blob.make(capacity)
Create with initial capacity (bits).
var b = blob.make(1024) // 1024 bits capacity
blob.make(length, logical)
Create filled with zeros or ones.
blob.make(64, false) // 64 zero bits
blob.make(64, true) // 64 one bits
blob.make(blob, from, to)
Copy a range from another blob.
var slice = blob.make(original, 0, 32) // first 32 bits
Writing (antestone only)
blob.write_bit(b, logical)
Append a single bit.
blob.write_bit(b, true) // append 1
blob.write_bit(b, false) // append 0
blob.write_fit(b, value, length)
Append a fixed-width integer.
blob.write_fit(b, 255, 8) // 8-bit value
blob.write_fit(b, 1000, 16) // 16-bit value
blob.write_blob(b, other)
Append another blob’s contents.
blob.write_blob(b, other_blob)
blob.write_dec64(b, number)
Append a 64-bit DEC64 number.
blob.write_dec64(b, 3.14159)
blob.write_text(b, text)
Append text (kim-encoded length + characters).
blob.write_text(b, "hello")
blob.write_pad(b, block_size)
Pad to block boundary (1 bit + zeros).
blob.write_pad(b, 8) // pad to byte boundary
Reading (stone only)
blob.read_logical(b, from)
Read a single bit.
var bit = blob.read_logical(b, 0) // first bit
blob.read_fit(b, from, length)
Read a fixed-width integer.
var value = blob.read_fit(b, 0, 8) // read 8 bits from position 0
blob.read_blob(b, from, to)
Extract a range as new blob.
var slice = blob.read_blob(b, 8, 24) // bits 8-23
blob.read_dec64(b, from)
Read a 64-bit DEC64 number.
var num = blob.read_dec64(b, 0)
blob.read_text(b, from)
Read kim-encoded text.
var str = blob.read_text(b, 0)
blob.pad?(b, from, block_size)
Check if padding is valid.
if (blob["pad?"](b, pos, 8)) {
// valid byte-aligned padding
}
Length
length(b) // returns bit count
Example
var blob = use('blob')
// Encode a simple message
var msg = blob.make()
blob.write_fit(msg, 1, 8) // message type
blob.write_fit(msg, 42, 32) // payload
blob.write_text(msg, "hello") // text data
stone(msg)
// Decode
var type = blob.read_fit(msg, 0, 8)
var payload = blob.read_fit(msg, 8, 32)
var txt = blob.read_text(msg, 40)
The time module provides time constants and conversion functions.
var time = use('time')
Constants
| Constant | Value | Description |
|---|---|---|
time.second | 1 | Seconds in a second |
time.minute | 60 | Seconds in a minute |
time.hour | 3600 | Seconds in an hour |
time.day | 86400 | Seconds in a day |
time.week | 604800 | Seconds in a week |
time.month | 2629746 | Seconds in a month (30.44 days) |
time.year | 31556952 | Seconds in a year (365.24 days) |
Getting Current Time
time.number()
Get current time as seconds since epoch.
var now = time.number() // e.g., 1702656000
time.record()
Get current time as a record.
var now = time.record()
// {year: 2024, month: 1, day: 15, hour: 10, minute: 30, second: 45, nanosecond: 123456789}
time.text(format)
Get current time as formatted text.
time.text() // default format
time.text("yyyy-MM-dd HH:mm:ss.SSS") // custom format
Converting Time
time.number(text, format, zone)
Parse text to timestamp.
time.number("2024-01-15", "yyyy-MM-dd")
time.number(record)
Convert record to timestamp.
time.number({year: 2024, month: 1, day: 15})
time.text(number, format, zone)
Format timestamp as text.
time.text(1702656000, "yyyy-MM-dd") // "2024-01-15"
time.record(number)
Convert timestamp to record.
time.record(1702656000)
// {year: 2024, month: 1, day: 15, ...}
Time Arithmetic
var now = time.number()
// Tomorrow at this time
var tomorrow = now + time.day
// One week ago
var last_week = now - time.week
// In 2 hours
var later = now + (2 * time.hour)
// Format future time
log.console(time.text(tomorrow))
Example
var time = use('time')
// Measure execution time
var start = time.number()
// ... do work ...
var elapsed = time.number() - start
log.console(`Took ${elapsed} seconds`)
// Schedule for tomorrow
var tomorrow = time.number() + time.day
log.console(`Tomorrow: ${time.text(tomorrow, "yyyy-MM-dd")}`)
ƿit provides three math modules with identical functions but different angle representations:
var math = use('math/radians') // angles in radians
var math = use('math/degrees') // angles in degrees
var math = use('math/cycles') // angles in cycles (0-1)
Trigonometry
sine(angle)
math.sine(math.pi / 2) // 1 (radians)
math.sine(90) // 1 (degrees)
math.sine(0.25) // 1 (cycles)
cosine(angle)
math.cosine(0) // 1
tangent(angle)
math.tangent(math.pi / 4) // 1 (radians)
arc_sine(n)
Inverse sine.
math.arc_sine(1) // pi/2 (radians)
arc_cosine(n)
Inverse cosine.
math.arc_cosine(0) // pi/2 (radians)
arc_tangent(n, denominator)
Inverse tangent. With two arguments, computes atan2.
math.arc_tangent(1) // pi/4 (radians)
math.arc_tangent(1, 1) // pi/4 (radians)
math.arc_tangent(-1, -1) // -3pi/4 (radians)
Exponentials and Logarithms
e(power)
Euler’s number raised to a power. Default power is 1.
math.e() // 2.718281828...
math.e(2) // e^2
ln(n)
Natural logarithm (base e).
math.ln(math.e()) // 1
log(n)
Base 10 logarithm.
math.log(100) // 2
log2(n)
Base 2 logarithm.
math.log2(8) // 3
Powers and Roots
power(base, exponent)
math.power(2, 10) // 1024
sqrt(n)
Square root.
math.sqrt(16) // 4
root(radicand, n)
Nth root.
math.root(27, 3) // 3 (cube root)
Constants
Available in the radians module:
math.pi // 3.14159...
math.e() // 2.71828...
Example
var math = use('math/radians')
// Distance between two points
function distance(x1, y1, x2, y2) {
var dx = x2 - x1
var dy = y2 - y1
return math.sqrt(dx * dx + dy * dy)
}
// Angle between two points
function angle(x1, y1, x2, y2) {
return math.arc_tangent(y2 - y1, x2 - x1)
}
// Rotate a point
function rotate(x, y, angle) {
var c = math.cosine(angle)
var s = math.sine(angle)
return {
x: x * c - y * s,
y: x * s + y * c
}
}
JSON encoding and decoding.
var json = use('json')
Encoding
json.encode(value, space, replacer, whitelist)
Convert a value to JSON text.
json.encode({a: 1, b: 2})
// '{"a":1,"b":2}'
// Pretty print with 2-space indent
json.encode({a: 1, b: 2}, 2)
// '{
// "a": 1,
// "b": 2
// }'
Parameters:
- value — the value to encode
- space — indentation (number of spaces or string)
- replacer — function to transform values
- whitelist — array of keys to include
// With replacer
json.encode({a: 1, b: 2}, null, function(key, value) {
if (key == "b") return value * 10
return value
})
// '{"a":1,"b":20}'
// With whitelist
json.encode({a: 1, b: 2, c: 3}, null, null, ["a", "c"])
// '{"a":1,"c":3}'
Decoding
json.decode(text, reviver)
Parse JSON text to a value.
json.decode('{"a":1,"b":2}')
// {a: 1, b: 2}
json.decode('[1, 2, 3]')
// [1, 2, 3]
Parameters:
- text — JSON string to parse
- reviver — function to transform parsed values
// With reviver
json.decode('{"date":"2024-01-15"}', function(key, value) {
if (key == "date") return parse_date(value)
return value
})
Example
var json = use('json')
// Save configuration
var config = {
debug: true,
maxRetries: 3,
endpoints: ["api.example.com"]
}
var config_text = json.encode(config, 2)
// Load configuration
var loaded = json.decode(config_text)
log.console(loaded.debug) // true
Random number generation.
var random = use('random')
Functions
random.random()
Returns a number between 0 (inclusive) and 1 (exclusive).
random.random() // e.g., 0.7234...
random.random_fit()
Returns a random 56-bit integer in the range -36028797018963968 to 36028797018963967.
random.random_fit() // e.g., 12345678901234
random.random_whole(max)
Returns a whole number from 0 (inclusive) to max (exclusive).
random.random_whole(10) // 0-9
random.random_whole(100) // 0-99
random.random_whole(6) + 1 // dice roll: 1-6
Examples
var random = use('random')
// Random boolean
var coin_flip = random.random() < 0.5
// Random element from array
function pick(arr) {
return arr[random.random_whole(length(arr))]
}
var colors = ["red", "green", "blue"]
var color = pick(colors)
// Shuffle array
function shuffle(arr) {
var result = array(arr) // copy
for (var i = length(result) - 1; i > 0; i--) {
var j = random.random_whole(i + 1)
var temp = result[i]
result[i] = result[j]
result[j] = temp
}
return result
}
// Random in range
function random_range(min, max) {
return min + random.random() * (max - min)
}
var x = random_range(-10, 10) // -10 to 10
ƿit makes it easy to extend functionality with C code. C files in a package are compiled into a dynamic library and can be imported like any other module.
Basic Structure
A C module exports a single function that returns a JavaScript value:
// mymodule.c
#include "cell.h"
#define CELL_USE_NAME js_mypackage_mymodule_use
static JSValue js_add(JSContext *js, JSValue self, int argc, JSValue *argv) {
double a = js2number(js, argv[0]);
double b = js2number(js, argv[1]);
return number2js(js, a + b);
}
static JSValue js_multiply(JSContext *js, JSValue self, int argc, JSValue *argv) {
double a = js2number(js, argv[0]);
double b = js2number(js, argv[1]);
return number2js(js, a * b);
}
static const JSCFunctionListEntry js_funcs[] = {
MIST_FUNC_DEF(mymodule, add, 2),
MIST_FUNC_DEF(mymodule, multiply, 2),
};
CELL_USE_FUNCS(js_funcs)
Symbol Naming
The exported function must follow this naming convention:
js_<package>_<filename>_use
Where:
<package>is the package name with/and.replaced by_<filename>is the C file name without extension
Examples:
mypackage/math.c->js_mypackage_math_usegitea.pockle.world/john/lib/render.c->js_gitea_pockle_world_john_lib_render_use
Required Headers
Include cell.h for all ƿit integration:
#include "cell.h"
This provides:
- QuickJS types and functions
- Conversion helpers
- Module definition macros
Conversion Functions
JavaScript <-> C
// Numbers
double js2number(JSContext *js, JSValue v);
JSValue number2js(JSContext *js, double g);
// Booleans
int js2bool(JSContext *js, JSValue v);
JSValue bool2js(JSContext *js, int b);
// Strings (must free with JS_FreeCString)
const char *JS_ToCString(JSContext *js, JSValue v);
void JS_FreeCString(JSContext *js, const char *str);
JSValue JS_NewString(JSContext *js, const char *str);
Blobs
// Get blob data (returns pointer, sets size in bytes)
void *js_get_blob_data(JSContext *js, size_t *size, JSValue v);
// Get blob data in bits
void *js_get_blob_data_bits(JSContext *js, size_t *bits, JSValue v);
// Create new stone blob from data
JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes);
// Check if value is a blob
int js_is_blob(JSContext *js, JSValue v);
Function Definition Macros
JSC_CCALL
Define a function with automatic return value:
JSC_CCALL(mymodule_greet,
const char *name = JS_ToCString(js, argv[0]);
char buf[256];
snprintf(buf, sizeof(buf), "Hello, %s!", name);
ret = JS_NewString(js, buf);
JS_FreeCString(js, name);
)
JSC_SCALL
Shorthand for functions taking a string first argument:
JSC_SCALL(mymodule_strlen,
ret = number2js(js, strlen(str));
)
MIST_FUNC_DEF
Register a function in the function list:
MIST_FUNC_DEF(prefix, function_name, arg_count)
Module Export Macros
CELL_USE_FUNCS
Export an object with functions:
static const JSCFunctionListEntry js_funcs[] = {
MIST_FUNC_DEF(mymod, func1, 1),
MIST_FUNC_DEF(mymod, func2, 2),
};
CELL_USE_FUNCS(js_funcs)
CELL_USE_INIT
For custom initialization:
CELL_USE_INIT(
JSValue obj = JS_NewObject(js);
// Custom setup...
return obj;
)
Complete Example
// vector.c - Simple 2D vector operations
#include "cell.h"
#include <math.h>
#define CELL_USE_NAME js_mypackage_vector_use
JSC_CCALL(vector_length,
double x = js2number(js, argv[0]);
double y = js2number(js, argv[1]);
ret = number2js(js, sqrt(x*x + y*y));
)
JSC_CCALL(vector_normalize,
double x = js2number(js, argv[0]);
double y = js2number(js, argv[1]);
double len = sqrt(x*x + y*y);
if (len > 0) {
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "x", number2js(js, x/len));
JS_SetPropertyStr(js, result, "y", number2js(js, y/len));
ret = result;
}
)
JSC_CCALL(vector_dot,
double x1 = js2number(js, argv[0]);
double y1 = js2number(js, argv[1]);
double x2 = js2number(js, argv[2]);
double y2 = js2number(js, argv[3]);
ret = number2js(js, x1*x2 + y1*y2);
)
static const JSCFunctionListEntry js_funcs[] = {
MIST_FUNC_DEF(vector, length, 2),
MIST_FUNC_DEF(vector, normalize, 2),
MIST_FUNC_DEF(vector, dot, 4),
};
CELL_USE_FUNCS(js_funcs)
Usage in ƿit:
var vector = use('vector')
var len = vector.length(3, 4) // 5
var n = vector.normalize(3, 4) // {x: 0.6, y: 0.8}
var d = vector.dot(1, 0, 0, 1) // 0
Combining C and ƿit
A common pattern is to have a C file provide low-level functions and a .cm file provide a higher-level API:
// _vector_native.c
// ... raw C functions ...
// vector.cm
var native = this // C module passed as 'this'
function Vector(x, y) {
return {x: x, y: y}
}
Vector.length = function(v) {
return native.length(v.x, v.y)
}
Vector.normalize = function(v) {
return native.normalize(v.x, v.y)
}
return Vector
Build Process
C files are automatically compiled when you run:
pit build
pit update
The resulting dynamic library is placed in ~/.pit/lib/.
Platform-Specific Code
Use filename suffixes for platform variants:
audio.c # default
audio_playdate.c # Playdate
audio_emscripten.c # Web/Emscripten
ƿit selects the appropriate file based on the target platform.
Static Declarations
Keep internal functions and variables static:
static int helper_function(int x) {
return x * 2;
}
static int module_state = 0;
This prevents symbol conflicts between packages.
Kim is a character and count encoding designed by Douglas Crockford. It encodes Unicode characters and variable-length integers using continuation bytes. Kim is simpler and more compact than UTF-8 for most text.
Continuation Bytes
The fundamental idea in Kim is the continuation byte:
C D D D D D D D
- C — continue bit. If 1, read another byte. If 0, this is the last byte.
- D (7 bits) — data bits.
To decode: shift the accumulator left by 7 bits, add the 7 data bits. If the continue bit is 1, repeat with the next byte. If 0, the value is complete.
To encode: take the value, emit 7 bits at a time from most significant to least significant, setting the continue bit on all bytes except the last.
Character Encoding
Kim encodes Unicode codepoints directly as continuation byte sequences:
| Range | Bytes | Characters |
|---|---|---|
| U+0000 to U+007F | 1 | ASCII |
| U+0080 to U+3FFF | 2 | First quarter of BMP |
| U+4000 to U+10FFFF | 3 | All other Unicode |
Unlike UTF-8, there is no need for surrogate pairs or escapement. Every Unicode character, including emoji and characters from extended planes, is encoded in at most 3 bytes.
Examples
'A' (U+0041) → 41
'é' (U+00E9) → 81 69
'💩' (U+1F4A9) → 87 E9 29
Count Encoding
Kim is also used for encoding counts (lengths, sizes). The same continuation byte format represents non-negative integers of arbitrary size:
| Range | Bytes |
|---|---|
| 0 to 127 | 1 |
| 128 to 16383 | 2 |
| 16384 to 2097151 | 3 |
Comparison with UTF-8
| Property | Kim | UTF-8 |
|---|---|---|
| ASCII | 1 byte | 1 byte |
| BMP (first quarter) | 2 bytes | 2-3 bytes |
| Full Unicode | 3 bytes | 3-4 bytes |
| Self-synchronizing | No | Yes |
| Sortable | No | Yes |
| Simpler to implement | Yes | No |
| Byte count for counts | Variable (7 bits/byte) | Not applicable |
Kim trades self-synchronization (the ability to find character boundaries from any position) for simplicity and compactness. In practice, Kim text is accessed sequentially, so self-synchronization is not needed.
Usage in ƿit
Kim is used internally by blobs and by the Nota message format.
In Blobs
The blob.write_text and blob.read_text functions use Kim to encode text into binary data:
var blob = use('blob')
var b = blob.make()
blob.write_text(b, "hello") // Kim-encoded length + characters
stone(b)
var text = blob.read_text(b, 0) // "hello"
In Nota
Nota uses Kim for two purposes:
- Counts — array lengths, text lengths, blob sizes, record pair counts
- Characters — text content within Nota messages
The preamble byte of each Nota value incorporates the first few bits of a Kim-encoded count, with the continue bit indicating whether more bytes follow.
See Nota Format for the full specification.
Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols.
Nota stands for Network Object Transfer Arrangement.
Design Philosophy
JSON had three design rules: minimal, textual, and subset of JavaScript. The textual and JavaScript rules are no longer necessary. Nota maintains JSON’s philosophy of being at the intersection of most programming languages and most data types, but departs by using counts instead of brackets and binary encoding instead of text.
Nota uses Kim continuation bytes for counts and character encoding. See Kim Encoding for details.
Type Summary
| Bits | Type |
|---|---|
000 | Blob |
001 | Text |
010 | Array |
011 | Record |
100 | Floating Point (positive exponent) |
101 | Floating Point (negative exponent) |
110 | Integer (zero exponent) |
111 | Symbol |
Preambles
Every Nota value starts with a preamble byte that is a Kim value with the three most significant bits used for type information.
Most types provide 3 or 4 data bits in the preamble. If the Kim encoding of the data fits in those bits, it is incorporated directly and the continue bit is off. Otherwise the continue bit is on and the continuation follows.
Blob
C 0 0 0 D D D D
- C — continue the number of bits
- DDDD — the number of bits
A blob is a string of bits. The data produces the number of bits. The number of bytes that follow: floor((number_of_bits + 7) / 8). The final byte is padded with 0 if necessary.
Example: A blob containing 25 bits 1111000011100011001000001:
80 19 F0 E3 20 80
Text
C 0 0 1 D D D D
- C — continue the number of characters
- DDDD — the number of characters
The data produces the number of characters. Kim-encoded characters follow. ASCII characters are 1 byte, first quarter BMP characters are 2 bytes, all other Unicode characters are 3 bytes. Unlike JSON, there is never a need for escapement.
Examples:
"" → 10
"cat" → 13 63 61 74
Array
C 0 1 0 D D D D
- C — continue the number of elements
- DDDD — the number of elements
An array is an ordered sequence of values. Following the preamble are the elements, each beginning with its own preamble. Nesting is encouraged.
Record
C 0 1 1 D D D D
- C — continue the number of pairs
- DDDD — the number of pairs
A record is an unordered collection of key/value pairs. Keys must be text and must be unique within the record. Values can be any Nota type.
Floating Point
C 1 0 E S D D D
- C — continue the exponent
- E — sign of the exponent
- S — sign of the coefficient
- DDD — three bits of the exponent
Nota floating point represents numbers as coefficient * 10^exponent. The coefficient must be an integer. The preamble may contain the first three bits of the exponent, followed by the continuation of the exponent (if any), followed by the coefficient.
Use the integer type when the exponent is zero.
Examples:
-1.01 → 5A 65
98.6 → 51 87 5A
-0.5772156649 → D8 0A 95 C0 B0 BD 69
-10000000000000 → C8 0D 01
Integer
C 1 1 0 S D D D
- C — continue the integer
- S — sign
- DDD — three bits of the integer
Integers in the range -7 to 7 fit in a single byte. Integers in the range -1023 to 1023 fit in two bytes. Integers in the range -131071 to 131071 fit in three bytes.
Examples:
0 → 60
2023 → E0 8F 67
-1 → 69
Symbol
0 1 1 1 D D D D
- DDDD — the symbol
There are currently five symbols:
null → 70
false → 72
true → 73
private → 78
system → 79
The private prefix must be followed by a record containing a private process address. The system prefix must be followed by a record containing a system message. All other symbols are reserved.
Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume.
Wota stands for Word Object Transfer Arrangement.
Type Summary
| Byte | Type |
|---|---|
00 | Integer |
01 | Floating Point |
02 | Array |
03 | Record |
04 | Blob |
05 | Text |
07 | Symbol |
Preambles
Every Wota value starts with a preamble word. The least significant byte contains the type. The remaining 56 bits contain type-specific data.
Blob
A blob is a string of bits. The remaining field contains the number of bits. The number of words that follow: floor((number_of_bits + 63) / 64). The first bit of the blob goes into the most significant bit of the first word. The final word is padded with 0.
Example: A blob containing 25 bits 111100001110001100100001:
0000000000001904 # preamble: 25 bits, type blob
F0E3208000000000 # data (padded to 64 bits)
Text
The text is a string of UTF-32 characters packed 2 per word. The remaining field contains the number of characters. The number of words that follow: floor((number_of_characters + 1) / 2). The final word is padded with 0.
Example: "cat":
0000000000000305 # preamble: 3 characters, type text
0000006300000061 # 'c' and 'a'
0000007400000000 # 't' and padding
Array
An array is an ordered sequence of values. The remaining field contains the number of elements. Following the preamble are the elements, each beginning with its own preamble. Nesting is encouraged. Cyclic structures are not allowed.
Example: ["duck", "dragon"]:
0000000000000202 # preamble: 2 elements, type array
0000000000000405 # text "duck": 4 chars
0000006400000074 # 'd' 't' (reversed pair order)
000000630000006B # 'c' 'k'
0000000000000605 # text "dragon": 6 chars
0000006400000072 # 'd' 'r'
0000006100000067 # 'a' 'g'
0000006F0000006E # 'o' 'n'
Record
A record is a set of key/value pairs. Keys must be text. The remaining field contains the number of pairs.
Example: {"ox": ["O", "X"]}:
0000000000000103 # preamble: 1 pair, type record
0000000000000205 # key "ox": 2 chars
0000006F00000078 # 'o' 'x'
0000000000000202 # value: array of 2
0000000000000105 # "O": 1 char
0000004F00000000 # 'O'
0000000000000105 # "X": 1 char
0000005800000000 # 'X'
Number
Numbers are represented as DEC64. To arrange an integer, shift the integer up 8 bits. The number is incorporated directly into the preamble.
Example: 7:
0000000000000700 # integer 7 as DEC64
To arrange a floating point number, place the number in the word following the floating point preamble.
Example: 4.25:
0000000000000001 # preamble: type floating point
000000000001A9FE # DEC64 encoding of 4.25
Care must be taken when decoding that the least significant byte of the number is not 80 (the null exponent).
Symbol
The remaining field contains the symbol.
Example: [null, false, true, private, system]:
0000000000000502 # array of 5
0000000000000007 # null
0000000000000207 # false
0000000000000307 # true
0000000000000807 # private
0000000000000907 # system