Mora — an AI-native statically-typed scripting language for LLM agent orchestration and cloud-native observability. One binary ships both an HTTP server and an MCP server.
Architecture · CHANGELOG · Influences · Spec
- AI-native primitives — first-class prompt literals
p"...",ai.chat(prompt)/ai.critic(answer, ctx?)/ai.tokens,agent.create(name, config).run(task),with model = "...", temperature = 0.7config blocks, SHA-256 audit-log hash chain. - HTTP + MCP in one binary —
Router::new()+router.route(method, path, handler)+router.listen(addr)for REST, andMcpServer::new()server.tool(name, schema, handler)+server.serve()for MCP over stdio; CLI subcommandmora mcp tool-listenumerates tools.
- Pregel BSP for multi-agent orchestration —
state/node/channel/checkpoint/command/send/interrupt/rewindprimitives (v0.50);orchestrate moa(parallel LLM aggregation, v0.75.84) andorchestrate moe(sparse top-k experts, v0.75.85) built on top. - Record / replay / diff —
mora record/replay/difffor deterministic AI-call regression testing,.mora/recordings/*.jsonl. - Single-pass compile + MIR SSA + JIT — Lexer → ParserV3 →
MirFunction<MirInst>+MirWitness→ witness typecheck → MIR optimize → DAG →vm::run_mir; copy-and-patch JIT always compiled (v0.75.43, no feature gate). - HM type inference — Hindley-Milner inference is the default
type checker (
src/typeck/hm/); v0.75.29 removed the deadMORA_HM=1opt-in switch. Type checking always runs. - LSP server (
mora-lsp) — hover / completion / definition / references / rename / semanticTokens / foldingRange / publishDiagnostics over stdio JSON-RPC 2.0. - 9-language heritage — Clojure, Common Lisp, Prolog, Lisp, Ballerina, StreamIt, APL, Logo, Smalltalk (see docs/influences.md).
Pre-built binaries at
Releases
include both mora and mora-lsp.
| Target | Asset |
|---|---|
| Windows x86_64 | mora-x86_64-pc-windows-msvc.zip |
| Linux x86_64 (glibc) | mora-x86_64-unknown-linux-gnu.tar.gz |
| Linux x86_64 (musl) | mora-x86_64-unknown-linux-musl.tar.gz |
| macOS Intel | mora-x86_64-apple-darwin.tar.gz |
| macOS Apple Silicon | mora-aarch64-apple-darwin.tar.gz |
Add the extracted binary to PATH; you get both mora and mora-lsp.
From source: cargo build --release produces the same binaries.
mora script.mora # run a script
mora --repl # interactive REPL
mora --check script.mora # type check only
mora --opt=1 script.mora # SSA optimization: 0=off / 1=basic / >=2=aggressive
mora-lsp # language server (stdio JSON-RPC)-- v0.104.5: explicit constructors + method chaining (no `serve as` blocks)
let router = Router::new()
let router = router.route("GET", "/health", fn(req) => {status: "ok"})
let server = McpServer::new()
let server = server.tool("search", {query: {type: "string"}}, fn(args)
return "found docs for: " + args["query"]
end)
-- observability: block-scoped, kind is trace / metric / log
observe trace do
span "user_request" tags {path: "/chat"} do
let answer = ai.chat(p"answer this")
end
end
-- config block: comma-separated bindings, applies to enclosed calls
with model = "gpt-4o", temperature = 0.7
let reply = ai.chat(p"summarize")
end
-- both entry points block; run one per process
router.listen("127.0.0.1:3000")
-- server.serve()
Run with mora script.mora — pick one blocking entry point (listen for
HTTP, serve for MCP over stdio) per process. mora mcp tool-list
enumerates the registered MCP tools.
| Concept | Syntax |
|---|---|
| Variables | let x = 1, let s: string = "" |
| Tasks (named fns) | task foo(x) ... end (bare params, no type annotations) |
| Anonymous fns | fn(x) return x + 1 end |
| Lists | [1, 2, 3], list.map(fn), list.filter(fn), list.reduce(fn, init) |
| Dicts | {key: val}, dict.get("key") |
| String methods | .len(), .upper(), .lower(), .trim(), .split(), .contains(), .replace() |
| Control flow | if/then/end, for x in list/end, match expr with/end |
| Pipe | data |> func() |
| Parallel | parallel ... end |
| Modules | import "path", export let/task |
| Dyn dispatch | expr as dyn Name coercion, dyn Name<T> annotations |
| Generics | annotations only: list<T>, dict<K, V> |
| Types | type Name = TargetType (definition only, not an annotation) |
| Enums | enum Name / V1 / end (multi-line, no payloads) |
| Structs | struct Name / field: Type / end (multi-line) |
| Macros | macro name(params) ... end |
| Version | Feature | From | Example |
|---|---|---|---|
| v0.16 | Pattern match guards | Prolog | match n with x when x > 0 -> ... end |
| v0.17 | Pipe |> |
StreamIt | 5 |> fn(x) return x * 2 end |
| v0.17 | Sliding window | StreamIt | [1,2,3,4,5].window(3) |
| v0.17 | Array shape/reshape | APL | .shape(), .flatten(), .transpose(), .reshape() |
| v0.18 | Function compose | Clojure | compose(f, g, h) |
| v0.18 | Partial application | Lisp | partial(add, 10) |
| v0.19 | Atoms + swap/deref | Clojure | atom(0), swap(), deref() |
| v0.19 | Worker pool | Ballerina | parallel worker w1 ... end end |
| v0.19 | Transaction with compensation | Ballerina | transaction ... compensation ... end |
| v0.20 | Reflection | Smalltalk | type_of(), is_instance(), methods_of() |
| v0.20 | Macros | Common Lisp | macro name(params) ... end |
| v0.22 | First-class AI primitives | — | prompt literal p"..." + ai.chat() |
| v0.24 | Type alias / enum / struct | — | type Name = T (definition only) / multi-line enum / struct |
| Namespace | Functions |
|---|---|
json.* |
json.parse(text), json.stringify(value) |
web.* |
web.fetch(url) (HTTP via ureq) |
file.* |
read_text / write_text / append_text / read_bytes / write_bytes, exists / is_file / is_dir / size / list / mkdir / remove / rename / copy / touch, cwd / chdir / home_dir / join / abs / basename / dirname / extname |
| Persistence | save "file.json", value / load "file.json", var |
| Stream I/O | read "a.txt" into x / write "a.txt", content / append "a.txt", content |
| Memory | memory.store / recall / search / forget / clear / size / keys |
| Event | bus.emit(name, payload), bus.count() |
| Sandbox | sandbox.check_builtin, sandbox.check_path |
| Schedule | schedule.add(name, schedule, payload, [interval_seconds]), schedule.count() |
| CCR | ccr.put(content) / ccr.get(hash) |
| Mock | mock backend for testing |
| Skill | dual registry (CLI-Anything SKILL.md pattern, v0.46) |
| ToolPlane | Core/Extension adapter (v0.45) |
| Group | Construct | Notes |
|---|---|---|
| Prompt | p"hello {name}" |
first-class prompt literal with interpolation |
| Chat | ai.chat(prompt) / ai.chat(prompt, {model: "..."}) |
mock mode without OPENAI_API_KEY; optional dict picks the model |
| Critic | ai.critic(answer) / ai.critic(answer, ctx) |
structured {verdict, critique, score} |
| Context | with model = "...", temperature = 0.7 |
comma-separated bindings; applies to enclosed calls |
| Token accounting | ai.tokens → .input / .output / .total |
per-run counters |
| Tool calling | McpServer::new() + server.tool(name, schema, handler) |
MCP over stdio; mora mcp tool-list enumerates |
| Observability | observe trace do ... end, span "name" tags {k: "v"} do ... end |
kind is trace / metric / log; tag values are strings or bare identifiers |
| Memory | memory.store / recall / search / forget / clear / size / keys |
|
| Agent | agent.create(name, config).run(task); .name / .max_steps; agent.critic(text) / agent.critic(text, ctx) |
|
| Multi-agent | orchestrate moe (v0.75.85 sparse top-k), orchestrate moa (v0.75.84 parallel aggregation) |
declared form, sequential or BSP depending on kind |
| Var | Meaning | Default |
|---|---|---|
OPENAI_API_KEY |
AI API key | unset → mock mode |
MORA_AI_MODEL |
Default AI model | gpt-4o-mini |
MORA_AI_BASE_URL |
AI base URL | https://api.openai.com/v1 |
MORA_OPT |
SSA optimization level (0/1/2) |
unset → no opt |
Two explicit entry points, both blocking — run one per process:
| Mode | API | Purpose |
|---|---|---|
| HTTP | Router::new() → router.route(method, path, handler) → router.listen("127.0.0.1:3000") |
REST API |
| MCP | McpServer::new() → server.tool(name, schema, handler) → server.serve() |
Claude Desktop / MCP clients over stdio |
v0.11 four-port fallback: the HTTP listener requests port N; on
EADDRINUSE it falls back to N+1, N+2, N+3 in sequence. Banner line:
[serve] requested port 3000 unavailable, using 3001 instead.
-- config block: pick the model per call site
with model = "gpt-4o-mini", temperature = 0.7
let summary = ai.chat(p"summarize: {text}")
end
-- observe + span (kinds: trace / metric / log)
observe trace do
span "user_request" tags {path: "/chat"} do
let answer = ai.chat(p"analyze: {question}")
end
end
-- structured self-critique + token counters
let verdict = ai.critic("some answer text", "context")
let usage = ai.tokens.total
mora record script.mora my-recording # → .mora/recordings/my-recording.jsonl
mora replay script.mora my-recording # deterministic replay
mora diff a-recording b-recording # compare two recordings
mora record list # list recordings
mora record stats my-recording # stats
mora record timeline my-recording # call timeline
mora record export my-recording --format md # export JSONL / Markdown
mora record audit my-recording # secret scan (default `.moraignore`)
mora record report my-recording --verify "…" # evidence report
mora snapshot script.mora my-snapshot # snapshot test
mora mcp tool-list # list MCP tools
mora mcp tool-search <query> # search MCP tools
mora mcp toolsets # list toolsetslet name: string = "mora" -- OK
let age: number = "thirty" -- typeck: string → number
task add(a: number, b: number): number
return a + b
end
add(1, 2) -- OK
add(1) -- error: 2 args expected
add("x", 2) -- error: arg 1 must be number
Errors emit Type error at line N: …. Library / builtin tasks fall back
to Any only when no signature is declared. Type checking always runs —
there is no opt-out switch.
mora-lsp (binary in every release) speaks JSON-RPC over stdio. Editors
should configure it as the LSP launch command. Capabilities advertised
in initialize:
textDocumentSync(full sync)hover— types / signatures for variables and taskscompletion— local symbols + keywords + tasks + builtinsdefinition— go-to-definitionreferences— symbol referencesdocumentSymbol— outlinedocumentFormatting+documentRangeFormattingrenamesemanticTokensfoldingRange—if/for/taskblockspublishDiagnostics— typeck results
Smoke test: cargo run --example lsp_smoke spawns mora-lsp, opens a
buffer, and asserts typeck diagnostics / hover / completion.
See editors/ for the six supported editors:
| Editor | Folder | Format |
|---|---|---|
| VS Code | vscode/ | VSIX, TextMate grammar in package.json |
| Neovim | neovim/ | lua/mora-lsp.lua |
| Helix | helix/ | languages.toml |
| Sublime Text | sublime/ | mora.sublime-settings |
| Vim | vim/ | ftplugin/mora.vim |
| Emacs | emacs/ | mora-mode.el |
CI builds mora + mora-lsp for the five targets above via
.github/workflows/release.yml and uploads to GitHub Releases.
.mora ──► lexer ──► tokens ──► parser_v3 ──► MirFunction<MirInst> + MirWitness
──► witness typecheck ──► MIR SSA optimize (--opt=1/2)
──► DAG (pregel worker pool) ──► value runtime
├──► ai.chat / ai.critic / ai.tokens / with / observe
├──► memory.store / recall
├──► agent.create().run()
├──► web.fetch / json.* / file.*
└──► HTTP / MCP / record / replay
▼
LSP (mora-lsp) ──► hover / completion / diagnostics
Key modules under src/:
lexer.rs— tokensparser_v3/— Arena-based MIR expression builder (v0.55)mir/— SSA IR + DAG + copy-and-patch JIT (v0.75.43) +witnesstypeckinterpreter/— value runtime, AI builtins (ai_chat.rs+builtins/)typeck/— HM inference (default type checker,src/typeck/hm/) +witnesscheckerlsp/— JSON-RPC server + 9 providerspregel/— v0.50 BSP worker poolcheckpoint/— v0.50 Memory + SQLite persistence (optionalcheckpoint-sqlitefeature)audit/— v0.42.1 SHA-256 hash-chained JSONL audit logtoolplane/+ccr/— v0.45 Core/Extension adaptercompress/— v0.29 context compression + JSON crushskill/— v0.46 dual registrymcp_server.rs/http_server.rs— server entry points
Run any file with mora path/to/file.mora:
mcp_server_demo.mora— MCP tool registration viaMcpServer::new()+.tool()compress_demo.mora/compress_smart_demo.mora/compact_demo.mora— v0.29/v0.30 compress + SmartCrusherhm_basic_demo.mora— HM type inference walk-through (HM is the default type checker)integration_v0_34.mora— bus / sandbox / schedule / ccr / mock builtin tourjit_bench.rs—cargo run --release --example jit_bench(JIT vs interpreter)lsp_smoke.rs/lsp_v04_smoke.py— LSP end-to-endhm_inference_examples.md— HM inference playbook
- docs/mora-spec.md — language spec, 20 chapters
- docs/influences.md — 9-language heritage
- docs/learning-plan.md — 6-stage learning path
- docs/ARCHITECTURE.md — runtime architecture (v0.50 snapshot)
- CHANGELOG.md — full version history
1265 tests across 30 suites (cargo test --all-targets, 2026-09-21):
959 unit tests in the mora crate plus 306 integration tests under
tests/, with 0 failures.
Recent CHANGELOG highlights:
- v0.104.4 (2026-09-17) — CI first green: reflexive unification, clippy 1.98 lints, repo-wide rustfmt
- v0.104 (2026-09-16) — DAG executor / optimizer fixes (loop bodies, CSE rename loops, sequence stitching) + numeric tower unification
- v0.103 (2026-09-14) — explicit API constructors (
Router::new()/McpServer::new()),observe/spanblocks wired,ai.critic(answer, ctx?) - v0.99 (2026-09-13) —
randommodule as ambient effect - v0.75.85 (2026-08-04) —
orchestrate moe(sparse top-k Mixture-of-Experts) - v0.75.84 (2026-08-04) —
orchestrate moa(Mixture-of-Agents) - v0.75.43 — copy-and-patch JIT (no external deps, always compiled)
- v0.50 — Pregel BSP + Checkpoint (Memory / SQLite)
- v0.45 — ToolPlane Core/Extension adapter
- v0.42 — Capability Token + SHA-256 hash-chained Audit Sink
- v0.22 — first-class AI primitives (
p"..."/with/ai.chat)
BSD-3-Clause — see LICENSE. Copyright (c) 2026 Microbiosis.