/ Libraries
Playground Docs Home GitHub

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)
Requirement: The ORM library depends on the SQLite native extension. Make sure sqlite.dylib (or sqlite.so on Linux) is built and available in the extensions/sqlite/ directory.

connect / close

orm.connect(path: String) -> Map

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-based
orm.close(db: Map)

Closes the database connection. Always call this when done to release the SQLite handle.

orm.close(db)

model

orm.model(db: Map, table_name: String, schema: Map) -> Map

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

model.create(data: Map) -> Int

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) // 1
model.find(id: Int) -> Map | Nil

Finds 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"
model.update(id: Int, data: Map)

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)
model.delete(id: Int)

Deletes the row matching the given id.

User.delete(1)

Querying

model.all(_) -> Array

Returns all rows as an Array of Maps.

let users = User.all(0) for u in users { print(u.get("name")) }
model.where(condition: String, params: Array) -> Array

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])
model.count(_) -> Int

Returns the total number of rows in the table.

let n = User.count(0) print("${n} users")
Zero-arity closures: Lattice closures require at least one parameter. For methods that take no arguments (all, count, create_table, drop_table), pass any dummy value: model.all(0) or model.count(0). The argument is ignored.

Schema API

model.create_table(_)

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)
model.drop_table(_)

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

t.assert_eq(actual: Any, expected: Any)

Fails if actual != expected, showing both values in the error message.

t.assert_neq(actual: Any, expected: Any)

Fails if actual == expected.

t.assert_gt(a: Any, b: Any)t.assert_ltt.assert_gtet.assert_lte

Comparison assertions: greater than, less than, greater/equal, less/equal.

t.assert_near(actual: Any, expected: Any, epsilon: Any)

Fails if the absolute difference between actual and expected exceeds epsilon. Useful for floating-point comparisons.

t.assert_contains(haystack: Any, needle: Any)

Fails if haystack does not contain needle. Works with both Strings and Arrays.

t.assert_throws(closure: Fn)

Fails if the closure does not throw an error. The closure receives a single ignored argument.

t.assert_throws(|_| { 1 / 0 })
t.assert_type(value: Any, type_name: String)

Fails if typeof(value) does not match type_name.

t.assert_nil(value)t.assert_not_nil(value)

Assert that a value is or is not nil.

t.assert_true(value)t.assert_false(value)

Assert that a value is exactly true or false.

t.check(condition: Any)

Fails if condition is falsy. A general-purpose assertion.

Organization

t.it(name: String, closure: Fn) -> Map

Creates a test case descriptor. The closure receives one ignored argument. Returns a Map with "name" and "fn" keys.

t.describe(name: String, builder_fn: Fn) -> Map

Creates a test suite. The builder function receives one ignored argument and should return an Array of test descriptors created with t.it().

t.run(suites: Array)

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 1ms

Getting 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")) // true

Schema Builders

v.string() -> Map

Schema that validates the value is a String.

v.number() -> Map

Schema that validates the value is an Int or Float.

v.boolean() -> Map

Schema that validates the value is a Bool.

v.array(item_schema: Map) -> Map

Schema that validates the value is an Array. Each element is validated against the item schema.

v.object(fields: Map) -> Map

Schema that validates the value is a Map with specific field schemas. The fields Map maps field names to their schemas.

v.any() -> Map

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.

v.min_len(schema, n)v.max_len(schema, n)

Set minimum/maximum length for strings and arrays.

v.min(schema, n)v.max(schema, n)

Set minimum/maximum value for numbers.

v.pattern(schema, regex: String)

Require the string to match a regex pattern.

v.one_of(schema, options: Array)

Restrict the value to one of the given options (enumeration).

v.integer(schema)

Require the number to be an Int (not Float).

v.opt(schema)

Mark the field as optional (nil is accepted).

v.default_val(schema, value)

Set a default value for missing fields. Also marks the field as optional.

Checking

v.check(schema: Map, data: Any) -> Map

Validate data against a schema. Returns a Map with "valid" (Bool) and "errors" (Array of error strings).

v.is_valid(schema: Map, data: Any) -> Bool

Shorthand that returns true if data passes validation.

v.apply_defaults(schema: Map, data: Any) -> Map

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

F.range(start: Int, end: Int, step: Int = 1) -> Seq

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]
F.from_array(array: Array) -> Seq

Create a lazy sequence from an existing array.

F.repeat(value: Any) -> Seq

Infinite lazy sequence that always yields the same value. Use with take.

F.iterate(seed: Any, f: Fn) -> Seq

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

F.fmap(seq, transform: Fn) -> Seq

Lazily transform each element. Also accepts arrays directly.

F.select(seq, predicate: Fn) -> Seq

Lazily filter elements where predicate returns true.

F.take(seq, n: Int) -> SeqF.drop(seq, n) -> Seq

Take the first n elements, or skip the first n elements.

F.take_while(seq, predicate: Fn) -> Seq

Yield elements while predicate holds, then stop.

F.zip(seq1, seq2) -> Seq

Zip two sequences into pairs [a, b]. Stops when either is exhausted.

Consumers

F.collect(seq) -> Array

Materialize a lazy sequence into an array.

F.fold(seq, initial: Any, f: Fn) -> Any

Reduce a sequence with f(accumulator, element).

F.fold(F.range(1, 6), 0, |acc, x| { acc + x }) // 15
F.count(seq) -> Int

Count elements in a lazy sequence.

Result Type

A Result is a Map with "tag" set to "ok" or "err" and a "value" field.

F.ok(value) -> ResultF.err(message) -> Result

Construct Ok or Err results.

F.is_ok(r) -> BoolF.is_err(r) -> Bool

Check whether a Result is Ok or Err.

F.unwrap(r) -> AnyF.unwrap_or(r, default) -> Any

Extract the value from an Ok result. unwrap errors on Err; unwrap_or returns the default instead.

F.map_result(r, f: Fn) -> ResultF.flat_map_result(r, f) -> Result

Transform the Ok value. flat_map_result expects f to return a Result.

F.try_fn(closure: Fn) -> 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

F.curry(f: Fn) -> Fn

Curry a 2 or 3-argument function for one-at-a-time application.

F.partial(f: Fn, ...bound) -> Fn

Partially apply a function by binding the first 1–3 arguments.

let add5 = F.partial(|a, b| { a + b }, 5) print(add5(3)) // 8
F.comp(f, g) -> Fn

Right-to-left composition: comp(f, g)(x) = f(g(x)).

F.apply_n(f, n: Int, value) -> Any

Apply f to value n times: f(f(f(value))).

F.flip(f) -> FnF.constant(value) -> Fn

flip swaps argument order. constant returns a function that always returns the same value.

Collection Utilities

F.group_by(array, key_fn: Fn) -> Map

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]}
F.partition(array, predicate: Fn) -> Array

Split into [matches, non_matches] based on a predicate.

F.frequencies(array) -> Map

Count occurrences of each element. Returns a Map of value → count.

F.chunk(array, size: Int) -> Array

Split array into sub-arrays of the given size.

F.flatten(array) -> Array

Flatten a nested array by one level.

F.uniq_by(array, key_fn: Fn) -> Array

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

dotenv.load()

Load .env from the current directory. Silently skips if the file doesn't exist. Does not override existing environment variables.

dotenv.load_file(path: String)

Load from a specific path. Errors if the file doesn't exist.

dotenv.load_opts(options: Map)

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)
dotenv.parse(path: String) -> Map

Parse a .env file and return a Map of key-value pairs without setting environment variables.

dotenv.parse_string(content: String) -> Map

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 mode

Full 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.

open(path: String) -> Int

Opens a SQLite database. Returns an opaque handle (integer). Use ":memory:" for an in-memory database.

close(handle: Int)

Closes a database handle previously returned by open.

query(handle: Int, sql: String, params?: Array) -> Array

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")) }
exec(handle: Int, sql: String, params?: Array)

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])
last_insert_rowid(handle: Int) -> Int

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").