👨‍🔬Alpha #1: a newborn programming language for extensible systems

This post is a quick update on what I am cooking—Alpha.

Alpha is a programming language inspired by Julia, Rust, Clojure, Haskell, and likely some more from the back of my mind. “Alpha” is obviously a working title (and you should have noticed that you’re dealing with a very creative developer here).

I’ve been working on Alpha for 2 or 3 weeks now—it’s still sketchy but I want to share the update.

Why start a new language?

To exercise and gain experience.

I’ve built some simple languages before but this one is the first potentially-usable one. It’s much larger in scope and provides many learning opportunities.

I want to experiment with language design: multiple dispatch, first-class types (types as values), gradual typing, parametric types, and procedural macros. Maybe: laziness, multi-threading and asynchronous runtime (Hakell-like), effect system.

This is also my first language built with LLVM, JIT, and GC.

Language design

The main theme of Alpha is extensibility.

If you have an “almost perfect” library, you shouldn’t have to jump through hoops to add a feature, fix a bug, or modify the behavior slightly. Extending others’ code shouldn’t be harder than writing your own.

Emacs Lisp is the most extensible system I know. It allows you to explore any code and redefine any function in the system. It has an Advice system to alter functions behavior, and there is el-patch library to patch functions at runtime.

There is a feeling of freedom knowing that you can hack any piece of the system. I want you to experience the same feeling, although in a saner environment.

Here are bits of Alpha’s preliminary syntax.

Types:

# All types in Alpha are subtypes of Any:
type Any = abstract

# Abstract types can subtype other abstract types:
type Number: Any = abstract
type Integer: Number = abstract

# Specific types can subtype abstract types only:
type i64: Integer = integer(64)
type f32: Number = float(32)
type bool: Integer = integer(1)

# struct types:
type Unit = {}
type Point2D = { x, y }

# enums can be implemented with type hierarchy:
type Shape = abstract
type Rock: Shape = {}
type Paper: Shape = {}
type Scissors: Shape = {}
# ...that's somewhat verbose but can be simplified with a macro.
#
# The bonus is that types are open, so you can add Lizard and
# Spock later if you need to.

type GameResult = abstract
type Win: GameResult = {}
type Lose: GameResult = {}
type Tie: GameResult = {}

# Alpha also has parametric types:
type Point{T} = { x: T, y: T }

# Types are first-class, so you can pass them as values:
let p = random(Point{f64})
print(p)  #=> Point { x: 42.13422, y: -123.12245 }

typeof(pi)         #=> f64
typeof(pi) == f64  #=> true
typeof(f64)        #=> DataType
typeof(DataType)   #=> DataType

Functions and methods:

# functions can have multiple implementations (methods)
# selected at runtime

# default implementation (least priority)
fn game_result(a: Shape, b: Shape) = negate(game_result(b, a))
fn game_result{T}(a: T, b: T) = Tie
fn game_result(a: Scissors, b: Paper) = Win
fn game_result(a: Paper, b: Rock) = Win
fn game_result(a: Rock, b: Scissors) = Win

fn negate(_: Win) = Lose
fn negate(_: Lose) = Win
fn negate(_: Tie) = Tie

# x.f(y1, y2) is a syntax sugar for f(x, y1, y2)

# x.field is a syntax sugar for field(x)

# operators are functions, too:
fn *(x: i64, y: i64) = i64_mul(x, y)

2.0 * 3.0        #=> 6.0

fn *{T}(n: T, p: Point{T}) = Point(n * p.x, n * p.y)
fn *{T}(p: Point{T}, n: T) = Point(p.x * n, p.y * n)

Point(1, 3) * 3  #=> Point { x: 3, y: 9 }

Bindings and variables:

# new bindings are created with "let"
let pi = 3.14159265
# Bindings are immutable, so you cannot modify them.
pi = 3.14       #=> Error: no method =(f64, f64)

# To create a mutable variable, use atom:
let counter = atom(0)

counter.get()   #=> 0
counter.set(42) #=> 42
counter.get()   #=> 42

# = is a shorthand for set:
fn =(x: Atom, value) = x.set(value)

counter = 13    #=> 13
counter.get()   #=> 13

Quoting, s-expressions, macros (heavily inspired by Julia):

# you can call "parse" to parse alpha code:
parse("x == y") #=> (:call :== :x :y)

# Alpha code is represented as s-expression, so you can easily
# inspect and modify/construct it from Alpha itself.

# "eval" function allow evaluating s-expressions:
eval(Sexp([:call :print "hello!"])) #=> hello!

# There is a special "quote" form to help constructing s-expressions:
quote(print("hello!")) #=> (:call :print "hello!")

# and you can create macros:
macro enum(name, ...values) = {
  quote {
    type ${name} = abstract

    ${values.map((v) ->
        quote {
          type ${v}: ${name} = {}
        }
      )}
  }
}

enum(GameResult, Win, Lose, Tie)

Well, the syntax becomes vague at this point, so I’d rather stop. I’ll post later when I clarify it more.

Current status

There is JIT-compiled REPL using LLVM JIT (spent quite some time learning LLVM and wrapping bindings in Rust).

You can define types (no type hierarchy yet). Types are first-class. There is typeof function.

You can define functions but no multi-methods yet—I am currently working on multi-methods.

The code is available on GitHub. And I’ll try to post weekly on my progress here.

Next in series: Alpha #2: multi-methods, type hierarchy, and dot desugaring

Backlinks