Interop & Stdlib
A new language is only as useful as what it can reach. Lumina reaches C directly, ships a practical standard library, and treats calls to an LLM as a typed, compiler-checked effect.
Calling C: @ExternC
@Trusted
@ExternC function strlen(s: String): Integer;
@Trusted
@ExternC function atoi(s: String): Integer;
function main(console: Console): Integer {
console.println(`strlen = {strlen("lumina")}, atoi = {atoi("42")}`)
return 0
}
strlen = 6, atoi = 42
- An
@ExternCdeclaration has no body and ends with;. Lumina strings are passed as C strings. @Trustedis the audit marker. A C binding outside trusted code gets warning W408 in dev builds, and W408 becomes an error in release builds.grep @Trustedlists every place where memory safety rests on hand-checked code.unsafeblocks narrow such an escape to a few lines instead of a whole function designed.- For application code, the recommended pattern is to wrap the C function in a
@Capabilityinterface (see Capabilities), so the effect shows up in signatures and tests can fake it. Calling FFI directly from non-trusted code is an error (W407) in progress. Pointer<T>and@CStruct(C-compatible layout) cover the lower-level cases.Language_Primitives.mdanddocs/c_interop.mdhave the details.
A short stdlib tour
import { sha256Hex } from "library/std/crypto"
import { regex_matches } from "library/std/regex"
function main(console: Console): Integer {
body = "{\"user\": {\"name\": \"Ada\", \"langs\": [\"ts\", \"rust\"]}}"
name = Json.getPath(body, "user.name") ?? "unknown"
second = Json.getPath(body, "user.langs.1") ?? "none"
console.println(`{name} likes {second}`)
console.println(sha256Hex("abc"))
isDate = regex_matches("^[0-9]+-[0-9]+-[0-9]+$", "2026-10-25")
console.println(`date? {isDate == 1}`)
return 0
}
Ada likes rust
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
date? true
Json.getPath returns String?, so a missing path is handled with ?? instead of an exception. regex_matches returns 1 or 0. Capture groups, counted quantifiers and replace round out the regex module in progress.
What else is there:
| Area | What you get |
|---|---|
| Strings | views (StringView), splitting, search, case, templates, a string builder |
| Bytes and I/O | Bytes / MutableBytes, BytesView, buffer pools, Decoder / Encoder, line streams over files |
| Data formats | JSON, CSV, INI, TOML, XML |
| Crypto | SHA-256, MD5, HMAC, PBKDF2, Base64, ed25519, random tokens |
| Randomness | a Random capability with SystemRandom, SeededRandom and FixedRandom (for tests) |
| Collections | Array, Map, Set, plus List, Deque and PriorityQueue in library/collections |
| Networking | TCP sockets with peek / spliceTo (see Zero-Copy & Servers), WebSocket and JSON-RPC modules in stdlib/net |
| Text algorithms | Levenshtein, Jaro-Winkler, Soundex, Metaphone, Porter stemmer, slugify |
| Misc | UUIDs, URL encoding, time, ANSI colors, property tests |
Language_Stdlib.md is the reference and FEATURE_STATUS.md tracks per-module status. The standard library lives in two places: library/std/ (core modules) and stdlib/ (larger subsystems such as ai, net and mcp). This split is visible in the import paths.
LLMs as a typed effect
Most languages treat an LLM call as just another HTTP request, so nothing stops a helper three layers down from quietly calling a model. In Lumina, calling a model is an algebraic effect, Llm, declared in stdlib/ai/effects.lumina. There is no std.ai module: the code lives in stdlib/ai/, stdlib/agent/ and stdlib/mcp/ and is imported by path, for example import { askLlm, Llm } from "stdlib/ai/effects".
public effect Llm {
complete(req: LlmRequest): Response!LlmError
}
Any function that transitively performs Llm.complete carries / {Llm} in its effect row. If that effect reaches main without a handle ... with Llm { ... }, the program does not compile (E9003). Code that goes through Llm shows it in its types, and the compiler enforces that. Direct functions such as callLlm, the agent loop and the builtin http_* calls do not carry the effect in progress.
A test handler resumes with a canned answer, so no network and no API key are needed:
result: Response!LlmError = handle { askLlm(provider, "Tell me a short story about a compiler.") } with Llm {
complete(req) => resume(Ok(Response {
content: "Once upon a time, a compiler caught a missing effect handler and saved the kingdom.",
tokensUsed: 18, inputTokens: 9, outputTokens: 18, stopReason: "stop",
toolCalls: [], latencyMs: 0, model: "stub-claude-sonnet", provider: "stub"
}))
}
$ lumina run examples/ai_effects/main.lumina
Story: Once upon a time, a compiler caught a missing effect handler and saved the kingdom.
(model: stub-claude-sonnet, tokens: 18)
Err: RateLimit — back off and retry.
Failures are a closed enum (RateLimit, Timeout, AuthFailed, BudgetExceeded, BadResponse, NetworkError, MissingApiKey), so match forces you to decide what each one means. The test handler and the E9003/E9004 checks are implemented. No production Llm handler ships in stdlib designed: you write the bridge from Llm.complete to callWithRetry yourself. Around the effect, stdlib/ai/ has provider configs, retry policies, rate limiting, budgets, provider routing and conversations.
Tool calling is in progress: wire formats for OpenAI and Anthropic and an agent loop with injected closures exist. Approval is not wired into the loop designed, and streaming is designed. The local llama backend needs a native shim library and has no capability gating in progress.
The full status table is in Language_AI.md.
Reflection
Types can be inspected at compile time and at run time through .reflect:
struct Point {
public x: Integer
public y: Integer
}
function main(console: Console): Integer {
p = Point { x: 1, y: 2 }
console.println(`{Point.reflect.name} is a {p.reflect.kind} with {p.reflect.fieldCount} fields`)
return 0
}
Point is a struct with 2 fields
Reflection drives derivation: listing an interface in implements triggers a @CompileTime static derive() that generates the implementation from the type's shape in progress. Language_Advanced_Reflection.md lists what is in place.
Next: Tooling