|
|
||
|---|---|---|
| spec | ||
| spec.old | ||
| src | ||
| .gitignore | ||
| AGENTS.md | ||
| example.slc | ||
| FEATURES.md | ||
| README.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 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!Tinstead of throwing. fail("message")returns aSimpleError!T.matchis exhaustive for known error sets.Err::HttpErrormatches 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
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 matchallows path arms such asErr::FileNotFoundfor nested matching on the root-level.- 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