Getting Started
The ORM library (lib/orm.lat) provides a simple object-relational mapping layer over the SQLite extension. Define models with schemas, then use a clean API for CRUD operations, querying, and schema management.
import "lib/orm" as orm
let db = orm.connect(":memory:")
flux schema = Map::new()
schema.set("id", "INTEGER PRIMARY KEY AUTOINCREMENT")
schema.set("name", "TEXT NOT NULL")
schema.set("email", "TEXT")
let User = orm.model(db, "users", schema)sqlite.dylib (or sqlite.so on Linux) is built and available in the extensions/sqlite/ directory.
connect / close
Opens a SQLite database connection. Pass ":memory:" for an in-memory database or a file path for persistent storage. Returns a database handle (Map).
let db = orm.connect(":memory:") // in-memory
let db = orm.connect("app.db") // file-basedCloses the database connection. Always call this when done to release the SQLite handle.
orm.close(db)model
Creates a model bound to a database table. The schema Map defines column names as keys and SQL type definitions as values. Returns a Map of CRUD closures.
flux schema = Map::new()
schema.set("id", "INTEGER PRIMARY KEY AUTOINCREMENT")
schema.set("name", "TEXT NOT NULL")
schema.set("age", "INTEGER")
let User = orm.model(db, "users", schema)CRUD Operations
Inserts a new row. The data Map should have column names as keys. Returns the last_insert_rowid.
flux data = Map::new()
data.set("name", "Alice")
data.set("age", 30)
let id = User.create(data) // 1Finds a row by its id column. Returns a Map of the row data, or nil if not found.
let user = User.find(1)
print(user.get("name")) // "Alice"Updates the row matching the given id. Only columns present in the data Map are modified.
flux changes = Map::new()
changes.set("age", 31)
User.update(1, changes)Deletes the row matching the given id.
User.delete(1)Querying
Returns all rows as an Array of Maps.
let users = User.all(0)
for u in users {
print(u.get("name"))
}Runs a custom WHERE clause with parameterized values. Returns matching rows as an Array of Maps.
let results = User.where("age > ?", [25])
let exact = User.where("name = ? AND age = ?", ["Alice", 30])Returns the total number of rows in the table.
let n = User.count(0)
print("${n} users")all, count, create_table, drop_table), pass any dummy value: model.all(0) or model.count(0). The argument is ignored.
Schema API
Creates the table if it does not exist, using the schema provided to orm.model(). Runs CREATE TABLE IF NOT EXISTS.
User.create_table(0)Drops the table if it exists. Runs DROP TABLE IF EXISTS.
User.drop_table(0)Full Example
import "lib/orm" as orm
let db = orm.connect(":memory:")
// Define schema
flux schema = Map::new()
schema.set("id", "INTEGER PRIMARY KEY AUTOINCREMENT")
schema.set("name", "TEXT NOT NULL")
schema.set("email", "TEXT")
schema.set("age", "INTEGER")
// Create model and table
let User = orm.model(db, "users", schema)
User.create_table(0)
// Insert records
flux d1 = Map::new()
d1.set("name", "Alice")
d1.set("email", "alice@example.com")
d1.set("age", 30)
User.create(d1)
flux d2 = Map::new()
d2.set("name", "Bob")
d2.set("email", "bob@example.com")
d2.set("age", 25)
User.create(d2)
// Query
let alice = User.find(1)
print(alice.get("name")) // "Alice"
let all = User.all(0)
print(len(all)) // 2
let young = User.where("age < ?", [28])
print(young.first().get("name")) // "Bob"
// Update
flux upd = Map::new()
upd.set("age", 31)
User.update(1, upd)
// Count & delete
print(User.count(0)) // 2
User.delete(2)
print(User.count(0)) // 1
orm.close(db)Getting Started
The test runner library (lib/test.lat) provides rich assertions and structured test organization. Define test suites with describe, individual tests with it, and run them with run.
import "lib/test" as t
t.run([
t.describe("Math", |_| {
return [
t.it("addition", |_| {
t.assert_eq(2 + 2, 4)
})
]
})
])Assertions
Fails if actual != expected, showing both values in the error message.
Fails if actual == expected.
Comparison assertions: greater than, less than, greater/equal, less/equal.
Fails if the absolute difference between actual and expected exceeds epsilon. Useful for floating-point comparisons.
Fails if haystack does not contain needle. Works with both Strings and Arrays.
Fails if the closure does not throw an error. The closure receives a single ignored argument.
t.assert_throws(|_| { 1 / 0 })Fails if typeof(value) does not match type_name.
Assert that a value is or is not nil.
Assert that a value is exactly true or false.
Fails if condition is falsy. A general-purpose assertion.
Organization
Creates a test case descriptor. The closure receives one ignored argument. Returns a Map with "name" and "fn" keys.
Creates a test suite. The builder function receives one ignored argument and should return an Array of test descriptors created with t.it().
Executes an array of test suites. Prints results with pass/fail indicators, counts, and elapsed time.
Full Example
import "lib/test" as t
t.run([
t.describe("Math operations", |_| {
return [
t.it("addition", |_| {
t.assert_eq(2 + 2, 4)
t.assert_eq(1.5 + 2.5, 4.0)
}),
t.it("division by zero", |_| {
t.assert_throws(|_| { 1 / 0 })
}),
t.it("comparisons", |_| {
t.assert_gt(5, 3)
t.assert_lt(1, 10)
t.assert_near(3.14159, 3.14, 0.01)
})
]
}),
t.describe("Types", |_| {
return [
t.it("type checks", |_| {
t.assert_type(42, "Int")
t.assert_type("hi", "String")
}),
t.it("nil checks", |_| {
t.assert_nil(nil)
t.assert_not_nil(42)
})
]
})
])Output:
Running tests...
Math operations
✓ addition
✓ division by zero
✓ comparisons
Types
✓ type checks
✓ nil checks
5 passed, 0 failed, 5 total
Completed in 1msGetting Started
The validation library (lib/validate.lat) provides declarative schema validation for Maps.
Build schemas with type constructors, add constraints, then validate data.
import "lib/validate" as v
let email_schema = v.pattern(v.string(), "^[^@]+@[^@]+\\.[^@]+$")
let result = v.check(email_schema, "user@example.com")
print(result.get("valid")) // trueSchema Builders
Schema that validates the value is a String.
Schema that validates the value is an Int or Float.
Schema that validates the value is a Bool.
Schema that validates the value is an Array. Each element is validated against the item schema.
Schema that validates the value is a Map with specific field schemas. The fields Map maps field names to their schemas.
Schema that accepts any non-nil value.
Constraints
Constraint helpers take a schema and return a new schema with the constraint added. They can be chained by nesting calls.
Set minimum/maximum length for strings and arrays.
Set minimum/maximum value for numbers.
Require the string to match a regex pattern.
Restrict the value to one of the given options (enumeration).
Require the number to be an Int (not Float).
Mark the field as optional (nil is accepted).
Set a default value for missing fields. Also marks the field as optional.
Checking
Validate data against a schema. Returns a Map with "valid" (Bool) and "errors" (Array of error strings).
Shorthand that returns true if data passes validation.
Returns a new Map with missing fields filled in from schema defaults.
Full Example
import "lib/validate" as v
// Build field schemas
flux fields = Map::new()
fields.set("name", v.max_len(v.min_len(v.string(), 1), 100))
fields.set("email", v.pattern(v.string(), "^[^@]+@[^@]+\\.[^@]+$"))
fields.set("age", v.opt(v.max(v.min(v.number(), 0), 150)))
fields.set("role", v.default_val(v.one_of(v.string(), ["admin", "user"]), "user"))
fields.set("tags", v.min_len(v.array(v.string()), 1))
let schema = v.object(fields)
// Validate
flux data = Map::new()
data.set("name", "Alice")
data.set("email", "alice@example.com")
data.set("tags", ["admin"])
let result = v.check(schema, data)
print(result.get("valid")) // true
// Apply defaults for missing fields
let filled = v.apply_defaults(schema, data)
print(filled.get("role")) // "user"Getting Started
The functional library (lib/fn.lat) provides lazy sequences, a Result type, currying/partial application, function composition, and collection utilities.
import "lib/fn" as F
// Lazy pipeline: range -> filter -> map -> collect
let squares = F.collect(
F.fmap(
F.select(F.range(1, 20), |x| { x % 2 == 0 }),
|x| { x * x }
)
)
print(squares) // [4, 16, 36, 64, 100, 144, 196, 256, 324]Lazy Sequence Constructors
Lazy integer range from start (inclusive) to end (exclusive).
F.collect(F.range(0, 5)) // [0, 1, 2, 3, 4]
F.collect(F.range(0, 10, 3)) // [0, 3, 6, 9]Create a lazy sequence from an existing array.
Infinite lazy sequence that always yields the same value. Use with take.
Infinite sequence: seed, f(seed), f(f(seed)), ...
F.collect(F.take(F.iterate(1, |x| { x * 2 }), 5)) // [1, 2, 4, 8, 16]Transformers
Lazily transform each element. Also accepts arrays directly.
Lazily filter elements where predicate returns true.
Take the first n elements, or skip the first n elements.
Yield elements while predicate holds, then stop.
Zip two sequences into pairs [a, b]. Stops when either is exhausted.
Consumers
Materialize a lazy sequence into an array.
Reduce a sequence with f(accumulator, element).
F.fold(F.range(1, 6), 0, |acc, x| { acc + x }) // 15Count elements in a lazy sequence.
Result Type
A Result is a Map with "tag" set to "ok" or "err" and a "value" field.
Construct Ok or Err results.
Check whether a Result is Ok or Err.
Extract the value from an Ok result. unwrap errors on Err; unwrap_or returns the default instead.
Transform the Ok value. flat_map_result expects f to return a Result.
Execute a closure, catching errors. Returns Ok(value) or Err(message).
let r = F.try_fn(|_| { 42 }) // ok(42)
let e = F.try_fn(|_| { 1 / 0 }) // err("division by zero")Composition & Currying
Curry a 2 or 3-argument function for one-at-a-time application.
Partially apply a function by binding the first 1–3 arguments.
let add5 = F.partial(|a, b| { a + b }, 5)
print(add5(3)) // 8Right-to-left composition: comp(f, g)(x) = f(g(x)).
Apply f to value n times: f(f(f(value))).
flip swaps argument order. constant returns a function that always returns the same value.
Collection Utilities
Group array elements by a key function. Returns a Map of key → Array.
F.group_by([1,2,3,4,5,6], |x| { if x % 2 == 0 { "even" } else { "odd" } })
// {"even": [2, 4, 6], "odd": [1, 3, 5]}Split into [matches, non_matches] based on a predicate.
Count occurrences of each element. Returns a Map of value → count.
Split array into sub-arrays of the given size.
Flatten a nested array by one level.
Deduplicate by key function. Keeps the first occurrence of each unique key.
Getting Started
The dotenv library (lib/dotenv.lat) loads environment variables from .env files.
Supports double-quoted and single-quoted values, variable expansion, multiline values, comments, and the export prefix.
import "lib/dotenv" as dotenv
dotenv.load() // load .env from current directory
let db = env("DATABASE_URL")API
Load .env from the current directory. Silently skips if the file doesn't exist. Does not override existing environment variables.
Load from a specific path. Errors if the file doesn't exist.
Load with options. Keys: "path" (file path), "override" (Bool, override existing vars), "required" (Array of required var names).
flux opts = Map::new()
opts.set("path", ".env.production")
opts.set("required", ["DATABASE_URL", "SECRET_KEY"])
dotenv.load_opts(opts)Parse a .env file and return a Map of key-value pairs without setting environment variables.
Parse .env format from a string. Useful for testing or processing env content from other sources.
File Syntax
Supported .env file features:
# Comments start with #
DB_HOST=localhost
DB_PORT=5432
# Double-quoted values with escape sequences
DB_PASS="p@ss\nword"
# Single-quoted values (literal, no escapes)
REGEX='^\d+$'
# Variable expansion in double-quoted values
DATABASE_URL="postgres://${DB_HOST}:${DB_PORT}/mydb"
# export prefix is stripped
export SECRET_KEY=abc123
# Multiline values (unclosed double quote continues)
RSA_KEY="-----BEGIN RSA-----
MIIBogIBAAJ...
-----END RSA-----"
# Inline comments (unquoted values only)
DEBUG=true # enable debug modeFull Example
import "lib/dotenv" as dotenv
// Load with required variables
flux opts = Map::new()
opts.set("path", ".env")
opts.set("required", ["DATABASE_URL", "SECRET_KEY"])
dotenv.load_opts(opts)
// Access variables
let db_url = env("DATABASE_URL")
let secret = env("SECRET_KEY")
print("Connected to: ${db_url}")
// Parse without loading (inspect only)
let vars = dotenv.parse(".env.example")
let keys = vars.keys()
for key in keys {
print("${key} = ${vars.get(key)}")
}Extension System
Lattice supports native extensions — shared libraries written in C that expose functions to Lattice code.
Use require_ext("name") to load an extension, which returns a Map of callable closures.
let sqlite = require_ext("sqlite")
// The returned Map contains closures for each registered function
let open = sqlite.get("open")
let close = sqlite.get("close")
let query = sqlite.get("query")
let run = sqlite.get("exec")
The runtime searches for the shared library at extensions/<name>/<name>.dylib (macOS) or .so (Linux). The library must export a lat_ext_init() function that registers its functions via the extension API.
SQLite Extension
The SQLite extension provides direct access to SQLite3 databases from Lattice. It supports parameterized queries with ? placeholders for safe value binding.
Opens a SQLite database. Returns an opaque handle (integer). Use ":memory:" for an in-memory database.
Closes a database handle previously returned by open.
Executes a SELECT query and returns an Array of Maps (one Map per row, with column names as keys). Optionally accepts an Array of parameters for ? placeholders.
let rows = query(db, "SELECT * FROM users WHERE age > ?", [25])
for row in rows {
print(row.get("name"))
}Executes a non-SELECT statement (INSERT, UPDATE, DELETE, CREATE TABLE, etc.). Optionally accepts parameterized values.
exec(db, "INSERT INTO users (name, age) VALUES (?, ?)", ["Alice", 30])
exec(db, "DELETE FROM users WHERE id = ?", [1])Returns the rowid of the most recently inserted row on this database handle.
Building Extensions
Extensions are compiled as shared libraries against include/lattice_ext.h. The extension must export a single entry point:
void lat_ext_init(LatExtContext *ctx) {
lat_ext_register(ctx, "my_function", my_function_impl);
}
Each registered function receives an array of LatExtValue pointers and returns one. Use the constructor functions (lat_ext_int, lat_ext_string, lat_ext_map_new, etc.) to build return values, and the accessor functions (lat_ext_as_int, lat_ext_as_string, etc.) to read arguments.
// Example: a function that adds two integers
LatExtValue *my_add(LatExtValue **args, size_t argc) {
if (argc != 2) return lat_ext_error("expected 2 args");
int64_t a = lat_ext_as_int(args[0]);
int64_t b = lat_ext_as_int(args[1]);
return lat_ext_int(a + b);
}
Compile the extension as a shared library and place it in extensions/<name>/:
# macOS
cc -shared -o extensions/myext/myext.dylib myext.c -I include
# Linux
cc -shared -fPIC -o extensions/myext/myext.so myext.c -I include
Then load it from Lattice with require_ext("myext").
Lattice