Types
I'll be honest — the type system grew larger than I originally planned. I started with the basics (int, string, bool) and then kept adding things as I needed them. Dates, times, complex numbers, vectors, matrices... every time I built an application, I'd hit a wall and think "well, I need this type natively." So here we are.
The good news is that everything fits together in a coherent way. The bad news is that this chapter is long.
Primitive Types
int (32-bit Integer)
A signed 32-bit integer, ranging from -2,147,483,648 to 2,147,483,647.
int x = 42;
int negative = -100;
int hex = 0xFF; // 255 in hexadecimal
| Property | Value |
|---|---|
| Default value | 0 |
| Storage size | 4 bytes |
| Can be map key | Yes |
long (64-bit Integer)
A signed 64-bit integer for larger values. Use the L suffix for literals.
long bigNumber = 9223372036854775807L;
long timestamp = 1234567890123L;
| Property | Value |
|---|---|
| Default value | 0L |
| Storage size | 8 bytes |
| Can be map key | Yes |
double (64-bit Floating Point)
A double-precision floating point number for decimal values.
double pi = 3.14159265359;
double scientific = 1.5e10; // 1.5 * 10^10
double negative = -273.15;
| Property | Value |
|---|---|
| Default value | 0.0 |
| Storage size | 8 bytes |
| Can be map key | No |
bool (Boolean)
A true/false value for logical operations.
bool isActive = true;
bool hasPermission = false;
| Property | Value |
|---|---|
| Default value | false |
| Storage size | 4 bytes |
| Can be map key | No |
string
A UTF-8 encoded text string.
string name = "Alice";
string empty = "";
string multiline = "line1\nline2";
| Property | Value |
|---|---|
| Default value | "" (empty string) |
| Can be map key | Yes |
Methods:
| Method | Returns | Description |
|---|---|---|
.length() |
int |
Number of characters in the string |
String Operations:
string greeting = "Hello, " + "World!"; // Concatenation
string repeated = "ha" * 3; // "hahaha"
bool matches = "hello world" =? "world"; // Fuzzy/contains search
complex (Complex Numbers)
A complex number with real and imaginary components, both stored as doubles. I added this because I needed it for signal processing math in one of my projects, and once it was in the language I couldn't bring myself to rip it out. Complex numbers are constructed using the @i constant (the imaginary unit) combined with arithmetic operations.
complex z = 3.0 + 4.0 * @i; // 3 + 4i
complex pure_imaginary = @i; // i
complex real_only = 5.0 + 0.0 * @i; // 5 + 0i
| Property | Value |
|---|---|
| Default value | 0 + 0i |
| Storage size | 80 bytes |
Methods:
| Method | Returns | Description |
|---|---|---|
.real() or .re() |
double |
The real component |
.imaginary() or .im() |
double |
The imaginary component |
complex z = 3.0 + 4.0 * @i;
double r = z.real(); // 3.0
double i = z.im(); // 4.0
Temporal Types
I built these in because dealing with dates and times as raw integers is a recipe for bugs. Every application I've ever built needed temporal types eventually, so I put them in the language from the start.
date
A calendar date in the Gregorian calendar.
date today = @date 2025/1/15; // January 15, 2025
date epoch = @date 1970/1/1;
| Property | Value |
|---|---|
| Default value | @date 1/1/1 |
Methods:
| Method | Returns | Description |
|---|---|---|
.year() |
int |
The year component |
.month() |
int |
The month (1-12) |
.day() |
int |
The day of month |
date d = @date 2025/6/15;
int y = d.year(); // 2025
int m = d.month(); // 6
int day = d.day(); // 15
time
A time of day at minute precision (no seconds).
time morning = @time 9:30; // 9:30 AM
time midnight = @time 0:00; // Midnight
time evening = @time 18:45; // 6:45 PM
| Property | Value |
|---|---|
| Default value | @time 0:00 |
Methods:
| Method | Returns | Description |
|---|---|---|
.hour() |
int |
The hour (0-23) |
.minute() |
int |
The minute (0-59) |
time t = @time 14:30;
int h = t.hour(); // 14
int m = t.minute(); // 30
datetime
A full timestamp with date, time, and timezone.
datetime now = @datetime "2025-01-15T14:30:00-05:00";
| Property | Value |
|---|---|
| Default value | 1900-01-01T00:00:00-00:00 |
Datetime values support comparison operators (<, <=, ==, !=, >=, >).
timespan
A duration of time. Timespans are created using the @timespan constant followed by a number and a unit.
Supported units: sec, min, hr, day, week
timespan hour = @timespan 1 hr; // 1 hour
timespan minute = @timespan 1 min; // 1 minute
timespan seconds = @timespan 30 sec; // 30 seconds
| Property | Value |
|---|---|
| Default value | @timespan 0 sec |
Methods:
| Method | Returns | Description |
|---|---|---|
.seconds() |
double |
The duration in seconds |
Timespan Arithmetic:
timespan a = @timespan 60 sec;
timespan b = @timespan 30 sec;
timespan sum = a + b; // 90 seconds
timespan scaled = a * 2; // 120 seconds
timespan scaled2 = 0.5 * a; // 30 seconds
The Maybe Type
The maybe<T> type is Adama's answer to null. Instead of letting any variable be null and praying you check before you dereference, maybe<T> makes optionality explicit. You either have a value or you don't, and the compiler forces you to deal with both cases.
I stole this idea from ML-family languages, and it's one of the best decisions I made.
Declaration and Construction
maybe<int> optionalNumber; // Empty by default
maybe<int> withValue = @maybe(42); // Contains 42
maybe<string> empty; // No value
| Property | Value |
|---|---|
| Default value | Empty (no value) |
Pattern Matching with if ... as
The idiomatic way to work with maybe values is pattern matching:
maybe<int> score = @maybe(95);
@construct {
if (score as value) {
// 'value' is available here as an int
int doubled = value * 2; // 190
}
}
The as pattern extracts the value and makes it available in the block only if the maybe contains a value.
Methods
| Method | Returns | Description |
|---|---|---|
.has() |
bool |
Returns true if a value is present |
.getOrDefaultTo(default) |
T |
Returns the value or the provided default |
.delete() |
void | Removes the value |
maybe<int> x = @maybe(10);
bool exists = x.has(); // true
int value = x.getOrDefaultTo(0); // 10
maybe<int> y;
bool empty = y.has(); // false
int fallback = y.getOrDefaultTo(-1); // -1
Why Division Returns maybe
Division always returns maybe<T> because division by zero is undefined. I know this is annoying. I know you want 10 / 2 to just give you 5. But silent NaN propagation has caused so many bugs in so many systems that I decided to make the tradeoff: a little more verbosity for a lot more safety.
Instead of crashing or returning infinity/NaN, division operations return a maybe type:
int a = 10;
int b = 0;
@construct {
maybe<double> result = a / b; // Empty (division by zero)
int c = 2;
maybe<double> valid = a / c; // Contains 5.0
// Handle the result safely
if (valid as v) {
// Use v here
}
}
The result type depends on the operands:
| Division | Result Type |
|---|---|
int / int |
maybe<double> |
int / long |
maybe<double> |
double / double |
maybe<double> |
complex / complex |
maybe<complex> |
| Any numeric / numeric | maybe<double> or maybe<complex> |
Similarly, the modulo operator % returns maybe<int> or maybe<long>:
maybe<int> remainder = 10 % 3; // Contains 1
maybe<int> invalid = 10 % 0; // Empty
Collections
list
A dynamic, ordered collection of elements.
list<int> numbers;
list<string> names;
| Property | Value |
|---|---|
| Default value | Empty list |
Methods:
| Method | Returns | Description |
|---|---|---|
.size() |
int |
Number of elements |
.toArray() |
T[] |
Convert to array |
.reverse() |
list<T> |
Reversed copy |
.skip(n) |
list<T> |
Skip first n elements |
.drop(n) |
list<T> |
Remove last n elements |
.flatten() |
list<U> |
Flatten nested lists |
.manifest() |
list<U> |
Extract values from list<maybe<U>> |
@construct {
list<int> nums;
int count = nums.size(); // 0
int[] arr = nums.toArray(); // Convert to array
list<int> reversed = nums.reverse();
list<int> skipTwo = nums.skip(2); // Skip first 2
}
Index Access:
@construct {
list<int> nums;
maybe<int> first = nums[0]; // Returns maybe because index might be invalid
}
map<K, V>
A key-value dictionary. Keys must be types that can serve as map domains (int, long, string, principal).
map<string, int> scores;
map<int, string> lookup;
map<principal, int> userScores;
| Property | Value |
|---|---|
| Default value | Empty map |
Methods:
| Method | Returns | Description |
|---|---|---|
.size() |
int |
Number of entries |
.has(key) |
bool |
Check if key exists |
.insert(map) |
map<K,V> |
Merge another map |
.remove(key) |
void | Remove entry by key |
.clear() |
void | Remove all entries |
.min() |
maybe<pair<K,V>> |
Entry with minimum key |
.max() |
maybe<pair<K,V>> |
Entry with maximum key |
map<string, int> scores;
@construct {
// Access by key returns maybe<V>
maybe<int> aliceScore = scores["Alice"];
// Check existence
bool hasAlice = scores.has("Alice");
// Remove a key
scores.remove("Bob");
// Get size
int count = scores.size();
}
Arrays (T[])
Fixed-size collections, typically created from list conversions or literals. Array literals use square brackets.
@construct {
int[] numbers = [1, 2, 3, 4, 5];
string[] names = ["Alice", "Bob"];
}
Methods:
| Method | Returns | Description |
|---|---|---|
.size() |
int |
Number of elements |
@construct {
int[] arr = [1, 2, 3];
int length = arr.size(); // 3
maybe<int> first = arr[0]; // Returns maybe because index might be out of bounds
}
Note: Array indexing returns
maybe<T>because the index might be out of bounds. Use pattern matching or.getOrDefaultTo()to safely access the value. I know, I know — it's more typing. But out-of-bounds access is a real bug in real code and I'd rather catch it.
Special Types
principal
Represents a user identity in the system. Every connected user has a principal. At its core, a principal is just an agent string paired with an authority string — but it's the foundation of Adama's privacy and access control model.
principal owner;
| Property | Value |
|---|---|
| Default value | @no_one |
| Can be map key | Yes |
Special principals:
@no_one- Represents no user@who- The current user (in channels/policies)
principal owner;
message Empty {}
channel doSomething(Empty msg) {
principal currentUser = @who;
if (owner == @who) {
// Current user is the owner
}
}
Methods:
| Method | Returns | Description |
|---|---|---|
.agent() |
string |
Extract the agent string from a principal |
.authority() |
string |
Extract the authority string from a principal |
.fromAuthority(key) |
bool |
Check if principal is from a specific authority |
.principalOf() |
principal |
Construct a principal from a string |
message Empty {}
channel inspect(Empty msg) {
// Decompose a principal for inspection
string agent = @who.agent();
string auth = @who.authority();
// Construct a principal from a string
principal p = "user123".principalOf();
}
asset
Represents an uploaded file attached to the document.
asset profilePicture;
| Property | Value |
|---|---|
| Default value | @nothing |
Methods:
| Method | Returns | Description |
|---|---|---|
.name() |
string |
Original filename |
.type() |
string |
MIME type |
.size() |
long |
File size in bytes |
.valid() |
bool |
Whether asset exists |
.id() |
string |
Unique identifier |
.md5() |
string |
MD5 hash |
.sha384() |
string |
SHA-384 hash |
asset file;
@construct {
if (file.valid()) {
string filename = file.name();
long bytes = file.size();
string contentType = file.type();
}
}
label
A reference to a state machine state. Used for state machine transitions. This type only makes sense once you've read the state machines chapter, but I'm listing it here for completeness.
#waiting {}
#finished {}
label next = #waiting;
label done = #finished;
| Property | Value |
|---|---|
| Default value | # (empty/terminal) |
#start {
transition #processing;
}
#processing {
// Do work
transition #done;
}
#done {
// Terminal state
}
dynamic
A flexible type that can hold arbitrary JSON-like data. I try not to use this much — it undermines the whole type safety thing — but sometimes you need to pass around data whose shape you don't know at compile time. Interop with external systems, mostly.
public dynamic food;
@construct {
dynamic ninja;
// Convert a value to dynamic
dynamic d = (123).dyn();
// Convert an anonymous object to dynamic
dynamic obj = {name: "test", value: 42}.to_dynamic();
}
| Property | Value |
|---|---|
| Default value | null |
Methods:
| Method | Returns | Description |
|---|---|---|
.dyn() |
dynamic |
Convert a value to dynamic (available on most types) |
.to_dynamic() |
dynamic |
Convert an anonymous object to dynamic |
future
Represents an asynchronous computation that will eventually produce a value. Returned by channel operations. Futures are how Adama handles the "I need to wait for a human to respond" problem.
message MyMessage {}
channel<MyMessage> somechannel;
@connected {
future<MyMessage> response = somechannel.fetch(@who);
return true;
}
Methods:
| Method | Returns | Description |
|---|---|---|
.await() |
T |
Block until value is available |
message MyMessage {}
channel<MyMessage> input;
principal theUser;
#awaiting {
future<MyMessage> f = input.fetch(theUser);
MyMessage msg = f.await(); // Blocks until message arrives
}
result
Represents the outcome of an operation that may succeed or fail. You'll see this with service calls mostly.
result<string> outcome;
Methods:
| Method | Returns | Description |
|---|---|---|
.has() |
bool |
Whether a value is present |
.failed() |
bool |
Whether the operation failed |
.finished() |
bool |
Whether the operation completed |
.code() |
int |
Error code if failed |
.message() |
string |
Error message if failed |
.await() |
maybe<T> |
Block and get result |
.as_maybe() |
maybe<T> |
Convert to maybe |
result<string> r = someServiceCall();
if (r.failed()) {
int errorCode = r.code();
string errorMsg = r.message();
} else if (r.has()) {
maybe<string> value = r.as_maybe();
}
Enums
Enums define a fixed set of named values with explicit integer assignments.
enum Status { Active:1, Inactive:2, Archived:3 }
enum Priority { Low:0, Medium:1, High:2, Critical:3 }
Enum values require explicit integer assignments (e.g., Active:1). I require this because implicit numbering leads to subtle bugs when you reorder values or add new ones in the middle. Explicit is better here. Always use PascalCase for both enum names and values.
| Property | Value |
|---|---|
| Default value | First declared value |
| Can be map key | No |
Methods:
| Method | Returns | Description |
|---|---|---|
.to_int() |
int |
Convert enum value to its integer representation |
.to_string() |
string |
Convert enum value to its string name |
enum Status { Active:1, Inactive:2, Archived:3 }
Status s = Status::Active;
int val = s.to_int(); // 1
string name = s.to_string(); // "Active"
Access syntax: Use EnumType::Value to reference specific values:
enum Status { Active:1, Inactive:2, Archived:3 }
Status status;
@construct {
if (status == Status::Active) {
// handle active status
}
}
Switch with wildcard patterns: Enums support prefix matching in switch statements — this is one of my favorite little features:
enum EventType { StartClockIn:1, StartResume:2, EndClockOut:3, EndPause:4 }
EventType event;
@construct {
switch (event) {
case EventType::Start*:
// matches StartClockIn AND StartResume
break;
case EventType::End*:
// matches EndClockOut AND EndPause
break;
}
}
Enums can be used with maybe<T> and as view types:
enum Status { Active:1, Inactive:2, Archived:3 }
maybe<Status> optionalStatus;
view Status currentFilter;
Vector and Matrix Types
I added these for 2D/3D graphics and game development. If you're building a board game or anything with spatial coordinates, these save you from writing record Vec2 { double x; double y; } for the hundredth time.
Vectors
| Type | Components | Default Value |
|---|---|---|
vec2 |
x, y | @vec [0.0, 0.0] |
vec3 |
x, y, z | @vec [0.0, 0.0, 0.0] |
vec4 |
x, y, z, w | @vec [0.0, 0.0, 0.0, 0.0] |
vec2 position = @vec [10.0, 20.0];
vec3 point3d = @vec [1.0, 2.0, 3.0];
vec4 color = @vec [1.0, 0.5, 0.0, 1.0]; // RGBA
Component Access:
vec3 v = @vec [1.0, 2.0, 3.0];
double x = v.x(); // 1.0
double y = v.y(); // 2.0
double z = v.z(); // 3.0
vec4 v4 = @vec [1.0, 2.0, 3.0, 4.0];
double w = v4.w(); // 4.0
Vector Operations:
vec3 a = @vec [1.0, 0.0, 0.0];
vec3 b = @vec [0.0, 1.0, 0.0];
vec3 sum = a + b; // Addition
vec3 diff = a - b; // Subtraction
double dot = a * b; // Dot product
vec3 cross = a % b; // Cross product (vec3 only)
vec3 scaled = a * 2.0; // Scalar multiplication
vec3 scaled2 = 2.0 * a; // Scalar multiplication (commutative)
// For vec2, cross product returns a scalar
vec2 v1 = @vec [1.0, 0.0];
vec2 v2 = @vec [0.0, 1.0];
double cross2d = v1 % v2; // Returns double
Matrices
Matrices are created using the @matrix literal syntax. The tilde (~) separates rows.
Matrix Sizes:
- 2x2 matrix:
@matrix [a, b ~ c, d] - 3x3 matrix:
@matrix [a, b, c ~ d, e, f ~ g, h, i] - 4x4 matrix:
@matrix [a, b, c, d ~ e, f, g, h ~ i, j, k, l ~ m, n, o, p] - 3x4 matrix (homogeneous):
@matrix [a, b, c, d ~ e, f, g, h ~ i, j, k, l]
// 2x2 matrix
public formula m2 = @matrix [1, 2 ~ 3, 4];
// 3x3 matrix
public formula m3 = @matrix [1, 2, 3 ~ 4, 5, 6 ~ 7, 8, 9];
// 4x4 matrix
public formula m4 = @matrix [1, 2, 3, 4 ~ 5, 6, 7, 8 ~ 9, 10, 11, 12 ~ 13, 14, 15, 16];
Matrix Operations:
// Matrix multiplication
public formula product = @matrix [1, 2 ~ 3, 4] * @matrix [5, 6 ~ 7, 8];
// Matrix inverse
public formula inv = (@matrix [1, 2 ~ 3, 4]).inverse();
Type Conversion
Structural Conversion with @convert
@convert<T> performs structural conversion between compatible types by matching fields by name. I use this constantly for projecting records into view-friendly message types:
record Player {
public int id;
public string name;
public int score;
private string internal_notes;
}
message PlayerView {
int id;
string name;
int score;
}
table<Player> _players;
// Project records into a view type (fields matched by name)
bubble leaderboard = @convert<PlayerView>(iterate _players order by score desc);
@convert<T> maps fields by name between structurally compatible types. Fields in the source that don't exist in the target are dropped; fields in the target that don't exist in the source get default values. Simple, predictable.
Numeric Promotion
When mixing numeric types in expressions, Adama automatically promotes to the wider type:
int -> long -> double -> complex
int a = 5;
long b = 10L;
long result = a + b; // int promoted to long
double c = 3.14;
double result2 = a + c; // int promoted to double
complex z = 1.0 + 1.0 * @i;
complex result3 = a + z; // int promoted to complex
String Conversion
Any type can be concatenated with a string, which implicitly converts it:
int num = 42;
string msg = "The answer is " + num; // "The answer is 42"
bool flag = true;
string text = "Status: " + flag; // "Status: true"
double pi = 3.14;
string s = "Pi is " + pi; // "Pi is 3.14"
Type Summary Table
| Type | Default | Can be Map Key | Orderable | Description |
|---|---|---|---|---|
int |
0 |
Yes | Yes | 32-bit integer |
long |
0L |
Yes | Yes | 64-bit integer |
double |
0.0 |
No | Yes | 64-bit float |
bool |
false |
No | Yes | Boolean |
string |
"" |
Yes | Yes | UTF-8 text |
complex |
0 + 0i |
No | No | Complex number |
date |
@date 1/1/1 |
No | Yes | Calendar date |
time |
@time 0:00 |
No | Yes | Time of day |
datetime |
(1900) | No | Yes | Full timestamp |
timespan |
@timespan 0 sec |
No | Yes | Duration |
principal |
@no_one |
Yes | No | User identity |
asset |
@nothing |
No | No | File reference |
label |
# |
No | No | State reference |
dynamic |
null |
No | No | Arbitrary JSON |
maybe<T> |
Empty | No | No | Optional value |
list<T> |
Empty | No | No | Dynamic list |
map<K,V> |
Empty | No | No | Key-value store |
T[] |
Empty | No | No | Fixed array |
vec2/3/4 |
Zero vector | No | Yes | Vector |
@matrix |
- | No | No | Matrix |
future<T> |
- | No | No | Async result |
result<T> |
Empty | No | No | Operation outcome |