Emela is an experimental functional language that compiles to WebAssembly
(Tier 1) and JavaScript (Tier 2). This repository holds the CLI, the compiler
for the current core subset, and the editor tooling (LSP and syntax highlighting).
The full language spec lives in
emela-lang/specification.
The language is pre-1.0 and moving quickly: within the 0.y.z range a minor bump
may include breaking language changes. Stable releases are published from main
(see the CHANGELOG); dev prereleases are published from dev.
- Install · Quick start · CLI · Language features · Syntax by example · Editor support · Packages & Pomes
Prebuilt binaries are published for macOS (Apple Silicon) and Linux (x86_64). The installer downloads the latest stable release:
curl -fsSL https://raw.githubusercontent.com/emela-lang/emela/main/install.sh | sh
emela --versionBy default this installs into $HOME/.emela/bin. Environment variables:
| Variable | Effect |
|---|---|
EMELA_INSTALL_DIR |
install location (default $HOME/.emela/bin) |
EMELA_VERSION |
pin an exact release tag, e.g. 0.7.1 |
EMELA_CHANNEL |
stable (default) or nightly (latest dev prerelease) |
The emela-lang/emela.nix flake
provides emela with no Rust toolchain required. By default it fetches the
prebuilt release binary; emela-source builds main HEAD from source with the
pinned toolchain.
nix run github:emela-lang/emela.nix # run the prebuilt binary
nix profile install github:emela-lang/emela.nix#emela
nix run github:emela-lang/emela.nix#emela-source # build main HEAD from sourceUse it as an overlay to get pkgs.emela (and pkgs.emela-source), e.g. on NixOS
or home-manager via environment.systemPackages = [ pkgs.emela ];. Prebuilt
binaries cover aarch64-darwin and x86_64-linux; aarch64-linux and
x86_64-darwin fall back to a source build. See the flake's README for the
overlay, override { fromSource = true; }, and the Rust devShell.
Build with Cargo (edition 2024, Rust 1.85+) — see Building.
- To install a release: nothing beyond
curlandtar(or Nix). - To run generated JavaScript: Node.js.
- To build from source: a Rust toolchain with Cargo (edition 2024, Rust 1.85+).
- Optional: a WASI runtime (
wasmtimeor WAMR'siwasm) — only to run a built.wasmartifact directly.emela runembeds its own pure-Rust WASI runtime (wasmi), and building needs no external wasm tools.
emela new hello # scaffold hello/ with a Pome.toml
cd hello
emela run src/main.emel # build to wasm and run it in-processemela run builds with the wasm-wasi backend and executes the module in
process — no external runtime needed. main's Int result is the process exit
code:
emela run examples/add.emel; echo $? # 42
emela run examples/hello.emel # Hello, Emela!The same program builds to either backend. main's Int result is the exit code
for wasm; the JS backend prints to stdout via Node:
emela build --backend js-node examples/add.emel | node # 42
emela build --backend wasm-wasi -o /tmp/add.wasm examples/add.emel
wasmtime /tmp/add.wasm; echo $? # 42The generated .wasm is a plain WASI preview1 module, so the built artifact also
runs under wasmtime or WAMR's iwasm. Programs that do real I/O import the
embedded std modules (spec 0038) — the Io effect ships inside the compiler, so
no --package is needed. Every file under examples/ type-checks and builds.
Run the installed emela binary (or cargo run --bin emela -- <args> from a
source checkout):
emela new <name> # scaffold a new Pome
emela check [--library] FILE # type-check only
emela run [--package DIR] FILE # build to wasm and run it in-process
emela build [--backend NAME] [-o OUT] FILE
emela ir FILE # print the typed IR
emela backends # list backends (wasm-wasi, js-node)
emela pome <add|remove|list|update|install|search> ... # dependencies
emela lsp [--package DIR] # LSP server over stdio (docs/lsp.md)
--emit text prints WAT for the wasm backend; emela ir prints the IR.
Implemented in the current compiler:
- top-level
fn, amainentry point, block expressions, immutablelet - primitives
Unit,Bool,Int,Float,String,Char, andArray<T> - arithmetic
+ - * /(and%onInt), comparisons== != < > <= >=, short-circuiting&& || !,Stringconcatenation++ - the pipeline operator
|>:x |> f |> g(a)isg(f(x), a)(spec 0019) if / elseas an expression- first-class functions: function values,
fnlambdas, closures, higher-order - generic functions
fn f<T>(...)— type arguments inferred, then monomorphized recorddeclarations with named fields and.field access (spec 0006)enum+ exhaustivematchwith pattern guards, including generic and recursive enums (enum List<T> { Nil, Cons(T, List<T>) })trait/implwith monomorphized dispatch, a Core Prelude (Show, operator traits), and receiver method-call sugarx.f()forf(x)(spec 0020/0021)- error handling:
throws E,throw, the?propagation operator,try/catch,panic;Option<T>for absent values (there is no built-inResult) - first-class effects:
effect Name { ... }, granted withuses { Name }and called asName.op(...), checked against each function body (spec 0037) - the
Httpcapability for synchronous HTTP requests (specs 0043–0046) module/pub/importacross files and source packages, and Pomes for Git-based distribution and dependencies (spec 0032)- WebAssembly and JavaScript backends (in-process or external plugin), with deterministic reference counting (ARC) on the wasm backend (spec 0048)
Conventions: enum variants and trait/record types are type paths written with
:: (List::Nil, Color::Red); . is reserved for module, field, and
receiver access. Identifiers use snake_case; types, records, and enum variants
use PascalCase. The primitive conversions and array operations are bare
intrinsic functions (char_from_code, string_from_char, array_get, …).
Not yet implemented: explicit type arguments, generic function values, effect/error-row polymorphism, and a native backend.
Functions, let, and blocks (a block is an expression; its last line is the value):
fn add(x: Int, y: Int) -> Int {
x + y
}
fn main() -> Int {
let base: Int = 20
let doubled = {
let stepped = base + 1
stepped * 2
}
add(doubled, 0)
}
if expression, operators, and Char / String:
fn label(n: Int) -> String {
if n < 10 && n >= 0 {
"digit " ++ string_from_char(char_from_code(48 + n))
} else {
"other"
}
}
Function values, closures, and generics (type arguments are inferred):
fn make_adder(n: Int) -> (Int) -> Int {
fn (x: Int) -> Int { x + n }
}
fn identity<T>(x: T) -> T { x }
fn main() -> Int {
let add10 = make_adder(10)
identity(add10(32))
}
Records — named fields, constructed with a record literal, read with .:
record User {
id: Int
name: String
}
fn greet(u: User) -> String {
"hi " ++ u.name
}
fn main() -> String {
greet(User { id: 7, name: "ada" })
}
Enums and exhaustive match (variants are constructed with ::):
enum Color {
Red
Green
Blue
}
fn code(c: Color) -> Int {
match c {
Red -> 1
Green -> 2
Blue -> 3
}
}
fn main() -> Int {
code(Color::Red)
}
Generic, recursive enums:
enum List<T> {
Nil
Cons(T, List<T>)
}
fn length<T>(xs: List<T>) -> Int {
match xs {
Nil -> 0
Cons(h, t) -> 1 + length(t)
}
}
fn main() -> Int {
let xs: List<Int> = List::Cons(1, List::Cons(2, List::Nil))
length(xs)
}
Traits and impl, dispatched by the argument type — a value c can call an
impl method as either to_string(c) or the receiver form c.to_string():
enum Color {
Red
Green
Blue
}
impl Show for Color {
fn to_string(c: Color) -> String {
match c {
Red -> "red"
Green -> "green"
Blue -> "blue"
}
}
}
fn main() -> String {
let c = Color::Green
c.to_string()
}
Error handling with throws / throw / try / catch, plus Option:
enum ParseError {
Empty
BadDigit
}
fn parse_digit(s: String) -> Int throws ParseError uses {} {
throw ParseError::BadDigit
}
fn parse_or(s: String, fallback: Int) -> Int uses {} {
try {
parse_digit(s)
} catch {
ParseError::Empty -> 0
ParseError::BadDigit -> fallback
}
}
fn unwrap_or(opt: Option<Int>, fallback: Int) -> Int uses {} {
match opt {
Some(value) -> value
None -> fallback
}
}
Effects are first-class (spec 0037): effect Name { ... } declares one, its
operations are called as Name.op(...), and uses { Name } grants a function
access. Effects propagate to callers, which must declare at least the same set:
effect Stdout {
pub fn log_line() -> Unit { () }
}
fn announce() -> Unit uses { Stdout } {
Stdout.log_line()
}
fn main() -> Unit uses { Stdout } {
announce()
}
Real side effects enter through platform functions (extern fn) inside a
stdlib effect, resolved by the selected backend's runtime — app code never names
a backend. std.io (embedded in the compiler) owns the Io effect:
import std.io
fn main() -> Unit uses { Io } {
Io.print("Hello, Emela!\n")
}
The compiler binary doubles as a language server, and this repository ships syntax definitions for several editors. Status:
| Editor / tool | Syntax highlighting | LSP (emela lsp) |
|---|---|---|
| Neovim / Vim | ✅ editors/nvim/ |
✅ built-in client |
| VS Code | ✅ TextMate grammar + extension | ✅ editors/vscode/ |
Sublime / bat |
✅ .sublime-syntax (syntect) |
— |
| Any LSP client | — | ✅ stdio, .emel files |
| Tree-sitter | ⬜ not yet (tree-sitter-emela) |
— |
LSP (emela lsp, spec 0033) speaks LSP over stdio and provides:
- Diagnostics on open/change/save, covering every compiler error — lexer,
parser, imports, and the type checker (type errors, missing trait methods,
unhandled effects,
throwsmismatches, non-exhaustive matches, …). Errors are collected across declarations, so independent mistakes all show at once, and unsaved buffers drive cross-file resolution. - Completion, context-aware:
importpaths,match/catchvariants (typed from the scrutinee), effect names inuses { … }, variants afterEnum::, and the default mix of keywords, types, functions, and locals.
Setup instructions per editor are in docs/lsp.md and docs/syntax-highlight.md.
--package DIR adds a source root; DIR needs an emela-package.json:
{ "name": "math", "source": "src" }Then import math.ops.add_one loads DIR/src/ops.emel (which must declare
module ops) and imports the pub function add_one. Imports without a package
name resolve relative to the importing file.
A Pome is Emela's unit of distribution — one or more modules supplied as a
Git repository (spec 0032). There is no central registry: a Pome is identified by
its source path host/path and fetched straight from that repository, versioned
by v-prefixed semver git tags.
emela new hello # scaffold hello/ with a Pome.toml
cd hello
emela pome add github:emela-lang/stdlib # fetch, pin in Pome.lock, audit capabilities
emela pome list # print the resolved dependency tree
emela build src/main.emel # deps are on the import path automaticallyOnce a Pome is a dependency, building any file inside your Pome puts its modules
on the import path — no --package needed. The import root is the dependency's
source-path leaf by default (a Pome may override it with [pome].module, spec
0032 M2) and its modules live under src/, so github.com/emela-lang/stdlib
declaring module = "std" and exposing src/io.emel (module io) is used as:
import std.io.print -- callable as print, io.print, or std.io.print
emela pome add records the dependency in Pome.toml under its canonical source
path, pins the resolved tag + commit + content hash in Pome.lock, and — since a
Pome's required capabilities are computable from source (spec 0025) — prints the
capability set the added Pome and its transitive dependencies require before
committing, so net/fs/clock growth is auditable at add time.
# Pome.toml
[pome]
name = "github.com/emela-lang/json"
version = "1.2.0"
emela = "0.1"
[dependencies]
"github.com/emela-lang/parser" = "^2.0"Publishing is just tagging: git tag v0.1.0 && git push origin v0.1.0. Several
Pomes developed together can share a workspace via Bushel.toml. To resolve
against a local checkout or mirror (offline development, CI), set
EMELA_POME_REPLACE="host/path=/local/or/url"; EMELA_POME_CACHE redirects the
fetch cache.
cargo build
cargo test
cargo fmt # formatBuilding needs no external wasm tools, and emela run bundles a WASI runtime, so
an external runtime is only needed to run a .wasm file yourself.
The language design lives in numbered SPECs under
emela-lang/specification. This
repository implements the current core subset; the SPEC referenced next to each
feature above is the authoritative description of its semantics.