Solace - a language to find solace from JavaScript.
Find a file
2025-06-17 15:39:38 +02:00
src more grammar and some interpreter 2025-06-17 15:39:38 +02:00
.gitignore initial 2025-06-09 21:50:12 +02:00
index.js initial 2025-06-09 21:50:12 +02:00
package.json more grammar and some interpreter 2025-06-17 15:39:38 +02:00
pnpm-lock.yaml more grammar and some interpreter 2025-06-17 15:39:38 +02:00
README.md docs: clarify shapes 2025-06-10 18:53:56 +02:00

Solace

A language to find solace after using JavaScript

Proposal: A strongly typed, programming language that compiles to JavaScript, with the following features:

  • Error unions: Error!string
  • Optional values: ?string
  • scoped blocks
  • exhaustive pattern matching
  • "unreachable" keyword
  • built-in reactivity system (using signals?)

Syntax example (very early proposal, might change):

  //imports work just as in ES6
  import { func1, func2, func3 as funFunc } from './MyCoolModule'
  import Cool from './MyCoolModule'

  // Declarations can be constant, variable or live (a.k.a. reactive):

  // Constants can never change
  const c = 299792468 // neither type nor value will ever change
  c++ // Error! Constant values cannot change.

  const car = struct{
    tires: 4,
    engine: true
  }

  car.tires = 5 // Error! Unlike in JS, struct values cannot be changed, either

  // Variables can be changed, as long as their type and shape stay the same:
  var vehicle = struct{
    tires: 2
    engine: true
  }
  vehicle.engine = false // no problem
  vehicle = struct{ // also fine
    tires: 2
    engine: false
  }
  vehicle = struct{ // Error! Type cannot be changed
    color: 'red'
  }

  /* Types and Shapes?
   *
   * Solace differentiates between types (as in string, number, boolean)
   * and shapes. While most types don't have shapes, structs ("objects") have.
   * Solace is strict about shapes of structs and doesn't allow any undefined
   * properties. Instead, property names should be known from the beginning
   * and can be explicitely optional and set to null, if the value isn't known
   */

  // TODO: introduce a dict type, that can be used without shape constraints?

  // Live values can be changed and their changes are tracked
  live speed = 50
  track speed {
    console.log(`Driving ${speed} km/h now`)
  }
  speed++ // logs speed automatically

  speed = 'fast' // gives an error, because the type cannot change

  // Computed values are basically getters (similar to Vuejs' computed)
  computed apparentSpeed {
    // pattern matching is another useful language feature that is explained in
    // further detail later
    return match speed {
      10 -> 'walking speed'
      _ < 50 -> 'inside town, maybe'
      _ -> 'outside town, hopefully'
    }
  }

  // Values can be optional
  var userName: ?string = None // we don't know the name, yet

  // Errors can be involved as well
  var userName: !string = Error('user not found')
  if (userFound) {
    userName = 'Hans'
  }

  /* Optional values and Error Unions explained:
   *
   * ?string would be string | null in Typescript,
   * !string would be string | Error,
   * !?string would be (string | null) | Error
   * Error unions can be specific: NetworkError!string
   * Optional values can either be a value or None (there is no null in Solace)
   */

  // undefined
  var userName: string // undefined, because it is not initialized
  userName = undefined // Error! You cannot set anything to undefined

  // In Solace, a value can either be explicitely optional and set to None,
  // or it can be undefined, because there was no assignment, yet.
  // This makes the distinction between undefined and null very clear:
  if (userName is undefined) // not initialized at all
  if (userName is None) // explicitely set to None

  // Guards: certain places can make use of guards to tighten their matches:
  match userName when Some(name) {
    name => console.log('User is named', name)
  } else {
    console.log('User has no name?!')
  }

  // defer allows you to run a code block after a function finished
  async fn deleteUser(userId: string) {
    const user: ?User = await fetch(`/api/users/${userId}`)

    // lets not forget to send the user a mail
    defer when user is Some(_) {
      sendEmail(userId, `Greetings ${user.name}, you have been deleted!`)
    }

    await deleteUser(userId)
  }


  // functions look like this
  fn doMath(): ?number {
    if (!computer.ready) return None
    return 42
  }

  //