| audit | ||
| docs/implementation | ||
| spec | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| example.slc | ||
| FEATURES.md | ||
| README.md | ||
| TODO.md | ||
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 withspec/README.mdfor the recommended reading order. slc, a Rust compiler that lexes, parses, type-checks, and emits readable JavaScript, lives insrc/. 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!Tinstead of throwing. fail("message")returns aSimpleError!T.matchis 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
voidis inferred. - TypeScript interop is deliberately limited for now.
The tradeoff is deliberate: less ambient flexibility, more predictable programs.
Current Spec Highlights
?Tfor optionals, lowered toT | null.E!Tand!Tfor generic typed error unions.async E!Tfor async operations that resolve to explicit success/error values (no throw/catch).error Name { ... }declarations with built-in.nameand.message.errorsetfor closed sets of possible errors.Erroras catch-all set of any possible error.- Exhaustive
match matcherror arms use bare error-name patterns (FileNotFound => |e|); enum variant paths (Enum::Variant) remain available.- Explicit
trait/implconformance. - Readable ES2020+ JavaScript output as the compilation target.
Reading The Spec
The detailed spec lives in spec/.
Useful entry points:
spec/README.md— reading order and chapter rolesspec/01-overview.md— design goals and non-goalsspec/03-types.md— type system and runtime shapesspec/04-expressions.md— expression formsspec/07-errors.md— errors,try,catch,fail, and error matchingspec/11-grammar.md— formal grammarspec/appendix/b-examples.md— larger examples