Files
beginner-friendly-lang/spec.md

11 KiB

Arithmetic

1.2 * 4 + 1
# result: 5.8 

Few types to learn

Most important ones:

  • Num: big-decimal
  • List[T]: (linked-) list of T
  • Char: unicode codepoint
  • Unit: nothing
  • i -> o: function
  • template a, b: t

Advanced (uncommon) types:

  • Int8, Int16, ...
  • Uint8, ...
  • Flt32, Flt64

Functions

# the type of msg is: List[Char] -> List[Char]
def msg(username: List[Char]) -> List[Char] {
  "Hello, " ++ username ++ "!"   # last expr is returned
}

# the type of main is: Unit -> IO[Unit]
def main() -> IO[Unit] {
  print("hi")
}

Anonymus functions

The type of List.map is List[t] -> (t -> t) -> List[t]
List.map(li, x:Num -> x * 2)

Simple, forward type-inference

def zero () -> Flt32 {
  3.1       # error: got Num, but expected Flt32
}

Partial function applications

# type of add is   Num -> Num -> Num
def add(a: Num, b: Num) -> Num {
  a + b
}

let a = add(1)   # type of a is   Num -> Num
let b = a(2)     # type of b is   Num
# b is 3

Bindings

let name = "Max"
let passw = "1234"

no confusing function or operator overloading

all operators:

  • Num + Num (has overloads for fixed width number types)
  • Num - Num (has overloads for fixed width number types)
  • Num * Num (has overloads for fixed width number types)
  • Num / Num (has overloads for fixed width number types)
  • Num ^ Num: raise to the power (has overloads for fixed width number types)
  • List[t] ++ List[t]: list concatenation
  • value :: t (explicitly specify type of value, useful for down-casting structs, or just code readability; does not perform casting)
  • list[index]
  • a => b: lens compose

non-nominal struct types

# `type` creates a non-distinct type alias
type User = { name: List[Char] }
type DbUser = { name: List[Char], pass: List[Char] }

def example4(u: User) -> List[Char] {
  u:name    # colon is used to access fields
}

def example(u: User) -> DbUser {
  u with pass: "1234"
  # has type { name: List[Char], pass: List[Char] }
}

def example2() -> {name: List[Char], pass: List[Char]} { 
  {name:"abc", pass:"123"}
}

def example3() -> User {
  example2()     # {name:.., pass:...} can automatically decay to {name:...}
}

(tagged) union types

type Option[t] =
  'Err        # If no type specified after tag, defaults to Unit
| 'Some t

# the tags of unions are weakly attached to the types, but won't decay unless they have to
def example(n: Num) -> Num {
  let x = 'MyTag n  # type of x is 'MyTag Num
  x                 # tag gets removed because target type is Num
}

def example2(n: Num) -> Option[Num] {
  'Some n
}

def example3-invalid() -> Option[Num] {
  Unit   # error: can't convert type `Unit` into type `'Err Unit | 'Some Num`
         #        Either label the expression with 'Err,
         #        or change the return type to Option[Unit], and label the expression with 'Some
}

def exampe4() -> Option[Num] {
  'Err Unit
  # type of this expression is: `'Err Unit`
  # enums can automatically cast, if all the cases from the source enum also exists in the target enum,
  # which they do here: `'Err Unit` is a case in `'Err Unit | Num`
}

def example5-error() -> Option[Num] {
  let x = ( 'Err Unit ) :: Option[Unit]
  x
  # error: can't convert type `'Err Unit | 'Some Unit` into type `'Err Unit | 'Some Num`
  #        The case `'Some Unit` does not exist in the target `'Err Unit | 'Some Num`
}

def example6-error() -> Option[Unit] {
  let x = 'Error Unit
  x
  # in this case, the enum tag does not decay, like in `example`,
  # because we are casting to an enum

  # error: can't convert type `'Error Unit` into type `'Err Unit | 'Some Num``
  #        1st possible solution: manually cast to just `Unit` (via `expr :: Unit`), so that it can convert to the second case of the target
  #        2nd possible solution: pattern match against the enum, to rename the tag from 'Error to 'Err
}

pattern 1: labelled arguments

def [t] List.remove_prefix(prefix: List[t], list: 'from List[t])

List.remove_prefix([1,2], 'from [1,2,3,4])

automatic return types

def add(a: Num, b: Num) -> _ {
  a + b
}

templated generics

# Type of add is:   template a, b: a -> b -> _
def [a,b] add(a: a, b: b) -> _ {
  a + b
}

add(1,2)

add(1)(2)  # error: partial function application of templated functions not allowed

add(1,"2")    # error: in template expansion of add[Num,List[Char]]: No definition for `Num + List[Char]`

pattern matching

type Option[t] = 'None | 'Some t

def [t] Match.`a++b`(
  # matching against this value
  value: List[t],
  # left hand side of operator
  l: List[t],
  # right hand side of operator
  r: MatchUtil.Var[List[t]]
) -> Option[{ r: List[t] }] {
  match List.remove_prefix(l, 'from value) {
    'Some rem -> 'Some { r: rem }
    'None -> 'None
  }
}

then you can do:

type Token = 'Public | 'Private | 'Err
def example(li: List[Char]) -> {t:Token,rem:List[Char]} {
  match li {
    "public" ++ rem  -> {t: 'Public Unit, rem:rem}
    "private" ++ rem -> {t: 'Private Unit, rem:rem}
    _ -> {t: 'Err Unit, rem: li}
  }
}

recursive data types

type List[t] = 'End | 'Cons {head:t, tail:List[t]}
# now you might notice an issue with this
# `type` defines non-distinct type alisases
# so what is the type of this...
# Introducing: type self references
# the above example is the same as this:
type List[t] = &a ('End | 'Cons {head:t, tail:a})

# example 2:
# a List[List[t]] is just:
&b ('End | 'Cons {head: &a ('End | 'Cons {head:t, tail:a}), tail: b})

Infinitely sized types are not allowed:

&a {x:Num, y:a}

However, infinite types without size are allowed:

&a {x:a}

This is not allowed:

&a a

module system

Each file is a "compilation unit"

When compiling a compilation unit, the following inputs have to be provided:

  • any amount of files containing signatures of exported definitions. only definitions of the compilation unit that are in one of the signature files will get exported.
  • any amount of other files containing imported definitions

Note that there is no practical difference between signature and source files.

Example

Export signature file List.li:

type List[t] = 'End | 'Cons {head:t, tail:List[t]}

# not providing a function body makes it a function signature definition
def [t] `a++b`(a: List[t], b: List[t]) -> List[t]

def [t] Match.`a++b`(
  value: List[t],
  l: List[t],
  r: MatchUtil.Var[List[t]]
) -> Option[{ r: List[t] }]

Import signature file Option.li:

type Option[t] = 'None | 'Some t

Compilation unit List.lu:

def [t] `a++b`(a: List[t], b: List[t]) -> List[t] {
  # ...
}

def [t] Match.`a++b`(
  value: List[t],
  l: List[t],
  r: MatchUtil.Var[List[t]]
) -> Option[{ r: List[t] }] {
  # ...
}

Notes

Each compilation unit gets compiled to implementation-specific bytecode.

Templated functions can only be compiled partially during a compilation unit. This will impact compile speeds.

Avoid templated functions wherever possible.

Hide record fields in module signatures

Signatue:

type User = {name: List[Char], ...}
  # the ... is used to indicate that this is a partial type definition
  # User, as given here, can not be constructed, but name can be accessed

Compilation unit:

type User = {name: List[Char], password: List[Char]}

Hide union variants in module signatures

Signature:

type DType = 'Int | 'UInt | 'Byte | ...
  # users of this can never do exhaustive pattern matching on this

Compilation unit:

type DType = 'Int | 'UInt | 'Byte
  # this compilation unit can actually do exhaustive pattern matching on this

Note on hidden union variants / record fields

To make these work, the following is legal:

def example() -> 'Int | 'UInt | ...

def example() -> 'Int | 'UInt | 'Byte | 'Char {
  # ...
}

Extensible unions

extensible union Plan

extend Plan with 'ReadlnPlan Unit
extend Plan with 'WritelnPlan Unit

# pattern matching against these is always non-exhaustive.
# can only pattern match with the imported extensions 

Any type

in the stdlib:

extensible union Any

type Any.LambdaCalc = 'Apply {fn: Any.LambdaCalc, arg: Any.LambdaCalc}
                    | 'Scope {idx: Uint}
                    | 'Abstr {inner: Any.LambdaCalc}
def Any.toLambda(a: Any) -> Any.LambdaCalc

It gets automatically extended with every type ever used.

Lenses

In the stdlib:

type Lens[t,f] = {get: t -> f, set: t -> f -> t}

def [a,b,c] `a=>b`(x: Lens[a,b], y: Lens[b,c]) -> Lens[a,c] {
  {
    get: t:a -> y:get(x:get(t)),
    set: t:a -> f:c -> x:set(t, y:set(x:get(t), f))
  }
}

Since a:f1:f2 is the field access syntax, the lens creation syntax is similar: &Type:field1:field2

So you can do:

type Header = {text: String, x: Num}
type Meta = {header: Header, name: String}

&Header:header:text (myHeader, "new meta:header:text value")
# which is identical to:
(&Header:header => &Meta:text) (myHeader, "new meta:header:text value")

However, it is cleaner to use with:

myHeader with header:text: "new meta:header:text value"

Pure IO

the IO[t] type contains IO "plans" which will be executed by the runtime, if they are returned by the main function.

def main() -> IO[Unit] {
  print("hey")    # warning: result (of type IO[Unit]) was ignored. expression can be removed 
  print("hello")
}
# this will only print "hello"

To make the above example print both "hey" and "hello", we need to chain the two IO types:

def main() -> IO[Unit] {
  await _ = print("hey")
  await print("hello")
}

# or just remove the _
def main() -> IO[Unit] {
  await print("hey")
  await print("hello")
}

# if you don't put in the second await:
def main() -> IO[Unit] {
  await print("hey")
  print("hello")
  # error: expected IO[Unit], got IO[IO[Unit]]; did you forget an `await`?
}

await is kinda weird. here is the syntax:

# ( await x: a = ( expr1::IO[a] ) await (expr2::IO[b]) ) :: IO[b]
expr |= 'await', (identifier, '='), expr, 'await', expr

# ( await x: a = ( expr1::IO[a] ) ( expr2::b ) ) :: IO[b]
expr |= 'await', (identifier, '='), expr, expr

# ( await (expr1::a) ) :: a
expr |= 'await', expr

Pure IO implementation

Something like this is done in the stdlib:

extensible union IO.Plan[r]
type IO[t] = 'Just {value: t}
           | 'Map template r: {of: IO[r], map: r -> IO[t]}
           | 'More template r: {plan: IO.Plan[r], then: r -> IO[t]}

def [a,b] `await a (a->b)`(io: IO[a], then: a -> b) -> IO[b] {
  'Map {of: io, map: r -> 'Just {value: then(r)}}
}

def [a,b] `await a (a->await b)`(io: IO[a], then: a -> IO[b]) -> IO[b] {
  'Map {of: io, map: r -> then(r)}
}

# in stdio:
extend IO.Plan[Uint8] with 'stdio.ReadByte {stream: Int32}
def stdio.getchar : IO[Uint8] = 'More {plan: 'stdio.ReadByte {stream: 0}, then: by -> 'Just by[0]}

def main() -> IO[Unit]

the runtime does something like this:

def [a] RUNTIME_EVAL(io: IO[a]) -> a {
  match io {
    'Just {value} -> value
    'Map {of, map} -> RUNTIME_EVAL(map(RUNTIME_EVAL(of)))
    'More {plan, then, finally} -> RUNTIME_EVAL(then(match plan {
      'ReadStream {stream} -> 'ReadStreamIOResult {data: impure perform the io here lol}
      _ -> impure error here "this runtime doesn't support this kind of IO" or sth
    }))
  }
}

def RUNTIME_ENTRY() {
  RUNTIME_EVAL ( main() )
}