A programming language that compiles to JavaScript
Find a file
koehr 8d31b730ee spec: update example.slc to current syntax
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-03 00:33:34 +02:00
spec spec: expand unreachable specification 2026-06-03 00:31:45 +02:00
spec.old spec2 is now the spec 2026-05-25 15:21:49 +02:00
src cleanup session transcripts and obsolete source files 2026-05-08 11:39:17 +02:00
.gitignore clean plate 2026-04-30 12:25:21 +02:00
AGENTS.md spec: update AGENTS.md 2026-06-01 20:39:15 +02:00
example.slc spec: update example.slc to current syntax 2026-06-03 00:33:34 +02:00
FEATURES.md spec: update FEATURES.md with Iter trait and mut primitive rule 2026-06-01 20:35:22 +02:00
README.md update READMEs 2026-05-25 15:37:59 +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 currently a language specification, not an implementation.

This repository contains the working spec for the language design. There is no compiler, CLI, package manifest, test suite, or runnable toolchain here yet. The current goal is to make the language model coherent enough to implement.

Start with spec/README.md if you want to read the spec in order.

A Taste

For a more comprehensive example, see example.sol.

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

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

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

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

fn getUser(userId: string) LoadErrors!User {
    // lets assume, fetch returns HttpError!T
    const userResult = await fetch("api/users/${userId}");

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

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

@test("Fetch User by id")
fn test_fetchUserById() !void {
    const userId = "foo123";
    const 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.
  • Err::HttpError matches through the error-union variant into the concrete error type.
  • 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 {
    const 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.

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

const 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 allows path arms such as Err::FileNotFound for nested matching on the root-level.
  • Explicit trait / impl conformance.
  • Readable ES2020+ JavaScript output as the compilation target.

Reading The Spec

The detailed spec lives in spec/.

Useful entry points: