A programming language that compiles to JavaScript
Find a file
2026-09-02 10:55:07 +02:00
audit clean up audit files 2026-08-04 00:45:06 +02:00
docs/implementation fix some widening soundness issues, clarifications, and so on 2026-09-02 10:55:07 +02:00
spec fix some widening soundness issues, clarifications, and so on 2026-09-02 10:55:07 +02:00
src fix: normalize pre-propulated array construction 2026-08-31 19:35:59 +02:00
tests fix: normalize pre-propulated array construction 2026-08-31 19:35:59 +02:00
.gitignore better naming for the implementation docs and README updates 2026-09-01 15:01:35 +02:00
AGENTS.md fix some widening soundness issues, clarifications, and so on 2026-09-02 10:55:07 +02:00
Cargo.lock chore: ignore build artifacts and add Cargo.lock 2026-08-26 00:50:04 +02:00
Cargo.toml slc: add crate skeleton and source spans 2026-08-26 00:49:29 +02:00
example.slc spec: update example.slc to current syntax 2026-06-03 00:33:34 +02:00
FEATURES.md spec: remove __deepFreeze mechanism 2026-07-21 02:18:01 +02:00
README.md better naming for the implementation docs and README updates 2026-09-01 15:01:35 +02:00
TODO.md docs: update AGENTS.md and TODO.md to reflect all audit decisions 2026-07-31 12:06:35 +02:00

Solace

A language for finding solace after JavaScript.

Solace is a strongly typed, expression-oriented language that compiles to readable JavaScript. It keeps JavaScript's deployment target, module model, and runtime reach, but replaces several common sources of uncertainty with explicit language constructs: optionals instead of silent nulls, error unions instead of exceptions, exhaustive matching instead of unchecked branching, and explicit traits instead of accidental structural conformance.

Git Forges

This spec is hosted on git.koehr.ing and mirrored on codeberg and radicle.

Status

Solace is a language specification and a reference compiler under active, milestone-by-milestone development.

  • The language design lives in spec/ — start with spec/README.md for the recommended reading order.
  • slc, a Rust compiler that lexes, parses, type-checks, and emits readable JavaScript, lives in src/. It currently implements the subset of the language covered by the completed milestones.

Implementation progress, scope per milestone, and remaining work are tracked in docs/implementation/ROADMAP.md (with a PRD per milestone in docs/implementation/specs/ and plans in docs/implementation/plans/).

Building and running

cargo build                 # build the slc compiler
cargo test                  # unit tests + golden fixtures (needs node on PATH)
cargo run -- run file.slc   # compile file.slc and execute it with node
cargo run -- build file.slc # emit file.js next to the source (-o to override)

A Taste

For a more comprehensive example, see example.slc.

struct User {
    id: string,
    name: string,
    secret: string,
}

impl Display for User {
    fn format(self) string {
        return "${self.name} (id: ${self.id}, password hash: ${hash(self.secret) /* user-defined */})";
    }
}

error UserNotFound {
    userId: string,
}
error HttpError {
    code: number,
}

errorset LoadErrors {
    PromiseRejectionError,
    UserNotFound,
    HttpError,
    SimpleError,
}

fn getUser(userId: string) LoadErrors!User {
    // lets assume, fetch returns !User (open error set)
    let userResult = await fetch("api/users/${userId}");

    return match (userResult) {
        Value => |user| user,
        HttpError => |err| {
            if (err.code == 404) { UserNotFound(userId) }
            else { err }
        }
        // fail makes SimpleError construction actually simple
        Error => |err| {
            fail("Something weird happened: ${err.name}: ${err.message}")
        }
    }
}

fn main() !void {
    let user = try getUser("foo123"); // propagate errors upward
    print(user); // uses the Display trait
}

@test("Fetch User by id")
fn test_fetchUserById() !void {
    let userId = "foo123";
    let user = try getUser(userId);

    assert(user.id == userId); // or something like that
}

The example shows the main flavor:

  • Fallible functions and async return E!T instead of throwing.
  • fail("message") returns a SimpleError!T.
  • match is exhaustive for known error sets.
  • Error arms name the error type directly — HttpError => |err| binds the concrete error.
  • annotation for tests

Why Not JavaScript?

JavaScript is flexible, universal, and easy to start with. Solace is for the point where that flexibility becomes expensive.

Absence Is Explicit

JavaScript often represents absence with null, undefined, missing fields, or sentinel values. Solace uses ?T.

fn findUser(id: string) ?User {
    return users.get(id);
}

if (findUser(id)) |user| {
    greet(user);
} else {
    promptLogin();
}

There is no implicit nullable User. If a value can be absent, the type says so.

Errors Are Values

JavaScript exceptions can cross large parts of a program invisibly. Solace makes fallibility part of the return type.

fn parsePort(raw: string) !number {
    let port = try parse(raw);

    if (port < 1 || port > 65535) {
        return fail("port out of range");
    }

    return port;
}

A caller can see from !number that parsing may fail.

Control Flow Produces Values

Blocks, if, match, for, and while are expressions.

let port = if (env == "dev") { 3000 } else { 8080 };

let found = for (items) |item| {
    if (item.matches(query)) break item;
} else {
    None
};

Why Not TypeScript?

TypeScript improves JavaScript enormously, but it is still a type layer over JavaScript. Solace chooses a smaller, more opinionated source language and a specified lowering model.

More Explicit Than Structural Typing

TypeScript often accepts values because their shapes happen to match. Solace requires explicit trait conformance.

// built-in trait, defined here only for the example
trait Display {
    fn format(self) string;
}

struct User {
    name: string,
    age: number,
}

impl Display for User {
    fn format(self) string {
        return "${self.name} (${self.age} years old)";
    }
}

A type implements a trait only when an impl says so.

Exhaustive Error Surfaces

TypeScript can model many unions, but JavaScript exceptions and promise rejections are still easy to miss. Solace error sets make a finite failure surface explicit.

errorset NetworkErrors {
    Timeout,
    ConnectionRefused,
}

fn fetchUser(id: string) async NetworkErrors!User;

Observable Solace async values do not reject; await yields an error union value that must be handled or propagated.

What Solace Gives Up

Solace is intentionally not a more powerful TypeScript.

  • No arbitrary union types like string | number.
  • No function overloading.
  • No classes or prototype inheritance.
  • No exposed exception flow on the Solace surface.
  • No compile-time execution or macros.
  • Return types are required except when void is inferred.
  • TypeScript interop is deliberately limited for now.

The tradeoff is deliberate: less ambient flexibility, more predictable programs.

Current Spec Highlights

  • ?T for optionals, lowered to T | null.
  • E!T and !T for generic typed error unions.
  • async E!T for async operations that resolve to explicit success/error values (no throw/catch).
  • error Name { ... } declarations with built-in .name and .message.
  • errorset for closed sets of possible errors.
  • Error as catch-all set of any possible error.
  • Exhaustive match
  • match error arms use bare error-name patterns (FileNotFound => |e|); enum variant paths (Enum::Variant) remain available.
  • Explicit trait / impl conformance.
  • Readable ES2020+ JavaScript output as the compilation target.

Reading The Spec

The detailed spec lives in spec/.

Useful entry points: