A programming language that compiles to JavaScript
Find a file
koehr d40da4549a spec: fix more items and mark underspecified areas with TODOs
Discussion fixes:
- stdlib: error construction uses call syntax, not struct-literal (E1)
- traits: @derive(Ord) requires Eq, no implicit generation (E2)
- errors: return Err(...) in !T functions — any error qualifies (E5)
- stdlib: note sync fs targets Node.js, async planned (S2)
- stdlib: std:async inspect requires state-tracking wrappers (S4)
- expressions: rename unsafeAs "Any JS value" → "Scalar / wrapper" (T5)
- compilation: document Map/Set deep-freeze limitation (K3)
- expressions: explicit break-value type unification rule (C1)
- scoping: add print/debug to built-ins list (M7)
- lexical: add \$ escape sequence (M10)
- stdlib: unify AssertionFailed into AssertionError (S6)
- traits: specify Display/Debug format for ?T and !T (M8)

TODOs added for underspecified areas:
- standalone range representation
- deep-freeze aliasing rules
- method mangling cross-unit interop
- primitive trait dispatch in generic code
- std:math, std:number, std:fs, std:http, std:datetime, std:async
2026-06-16 01:28:50 +02:00
spec spec: fix more items and mark underspecified areas with TODOs 2026-06-16 01:28:50 +02:00
src cleanup session transcripts and obsolete source files 2026-05-08 11:39:17 +02:00
.gitignore archive old audit 2026-06-15 18:06:21 +02:00
AGENTS.md update coercions in AGENTS.md 2026-06-10 00:42:41 +02:00
example.slc spec: update example.slc to current syntax 2026-06-03 00:33:34 +02:00
FEATURES.md From/Into traits, and unsafeAs type casting escape hatch 2026-06-06 18:11:00 +02:00
PLAN_new_and_skipped.md docs: consolidate new and skipped issues from external review 2026-06-15 18:10:15 +02:00
README.md spec: apply 37 important obvious fixes from cross-check audit 2026-06-13 21:02:01 +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: ${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 HttpError!T
    let 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 {
    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.
  • 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 {
    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 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: