\                          |
    _ \    __ \    _` |  __ \   |  /   _ \
   ___ \   |   |  (   |  |   |    <    __/
 _/    _\ _|  _| \__,_| _|  _| _|\_\ \___|

And why it is awesome

Philosophy

Ananke is a reasonable-batteries included systems programming language, with an imperative core, but strong hints of functional and array based programming, and a high-level sheath. It sits somewhere between Algol and ML. With some snippets, you’d be forgiven if you thought it was a dynamic language (it's actually statically duck-typed and compiled). It follows the C ethos: “the programmer knows best” and does everything it can to empower you, the programmer.

It compiles fast, it runs even faster

Ananke is a compiled language, meaning it cuts out a lot of computational middle men. It's very resource efficient, CPU and RAM wise, and using Ananke will save you on server costs.

LOC/s compile time: 999999 loc/s (made up number)

Targets: (to be done)

Note: Ananke is currently not implemented. Work is ongoing to get a bootstrapping interpreter. From there, a compiler is planned. To see the progress, check out the repo.

For syntax examples, check out below or the editor, work in progress.

Everything is an expression

This leads to great syntactical freedom and expressivity. Oh, and no semicolons. A terse yet friendly syntax in general designed for programmer ergonomics.

/. this is a
multi-line comment ./
; a line-end comment
use std
; random rational number between 0 and 1 inclusive
let relation = std.rand[rat](0, 1) > 0.5
    then "larger than"
    else "smaller than or equal to"
printl("the random value was \{relation} 0.5.")

Type system

Static duck type system: feels dynamic, doesn’t get in your way, great for prototyping, but protects against shooting yourself in the foot. Ananke tries to generate the appropriate algebraic datatypes inferred from how and where you use variables. For robust software you can constrain and specify the types as much as you want, and the assume and where clauses are particularly nifty. There are also sum types, product types, parametric types and the unit type void for those who enjoy total specification, as well as ad-hoc polymorphism for those who know what that means.

let x = 5
int y = x
str z = x; type error, int != string
str w = x to str; type conversion
let f = \a print("\{a+1}"); lambda (function) of one arg
f(w); type error, operator string + int doesn't exist
f(x); ok
f(4.5); works as well: duck typing provides ad-hoc polymorphism for floating point/integer types
let g = fun(int a){print("\{a+1}")}
; we can use g to force an integer usage
g(4.5); type error, 4.5 is not int
g(4); ok

; ad-hoc polymorphism:
fun poly(str x){printl("A string: "..x)}
fun poly(int x){printl("An int: \{x}")}
poly("hello world!"); "A string: hello world"
poly(5); "An int: 5"

Pattern matching

The powerful structural pattern matching syntax is like switch' older brother and works great with the type system.

let mixed_arr = ["test", 4, 23.3, nil]
let elem = std.pick_rand(mixed_arr);
printl("The element is ".. elem of {
    str x: "a string: \{x}",
    int x: "an integer: \{x}",
    rat x: "a rational: \{x}",
    _: "something else"
})

; `x of {}` is the same as `case x {}`
fun fizzbuzz(var n){
	printl( case 0 {
		n%15: "fizzbuzz", ; 0 == n%15
		n%5:  "buzz",
		n%3:  "fizz",
		_:    "\{n}"
	})
}
for n in 0..15 {fizzbuzz(n)}

; trigonometry stuff
fun quadrant(var x, var y){
	case true, true {
		x≥0, y≥0: 1,
		x≥0, y<0: 2,
		x<0, y≥0: 3,
		x<0, y<0: 4
	}
}

Continuations and pointers

continuations, the cradle of all control flow, like pointers are for data structures. You can re-implement all control flow including function returns, but also coroutines, exceptions, resumable exceptions, get creative!

In other languages, break breaks out of the current loop and continues execution to just past where it ends. In Ananke, break is generalized to break out of any scope and continue where the current scope {} ends. Exception: scopes following branches (check, if, else, elif, then, case, of) are not counted, the parent is broken out of instead. You can also use labeled breaks.

; by putting break in a function, we create a continuation
var i = 0
fun continuation(var x){
    printl("got \{x}")
    break; goto the end of this scope
    printl("this is never printed")
}; continue execution from right here

continuation(i+=1); increment i at every call
; this will loop forever

Oh, Ananke has raw pointers as well. All power to you.

var x = "this"
let ptr = x@; postfix address-of operator
ptr$ = "that"; postfix dereference operator
printl(x); "that"

Meta-programming

compile time evaluation of any expression, as well as procedural macros that can manipulate the AST (abstract syntax tree). This means the language is open ended and extensible, and you can extend Ananke to cater to your exact needs by writing Ananke.

fun fib(i) {
    i < 3 then 1 else fib(i-1)+fib(i-2)
}
let x = eval fib(20)
; x is calculated completely at compile time. The code for the fibonacci function isn't even generated.

use ananke
; all macros are prefix operators
; `T::{x}` is the initializer operator
macro modify(ananke.ast.expression x){
    return ananke.ast.assign::{
        children: [x, ananke.ast.integer::{value: 4}]
    }
}
var y = 5
#modify y

is transformed into:

let x = 6765
var y = 5
y = 4

Concurrency/parallelism

Built-in primitives for concurrency and atomics, with a focus on awaitable futures. There are no colored functions, you can execute any expression asynchronously. In Ananke, async has nothing to do with state machines or coroutines, it is a completely orthogonal feature that concerns only the branching of timelines, i.e., parallelism. The implementation of #async and #await are open ended and you can make them yourself or import a libary. A thread for each or a threadpool? You decide! There's also a robust standard library of locks, monitors, signals, etc.

let future = #async fib(40); we calculate fib(40) concurrently.
let fib_40 = #await future
; for all numbers 2 to 12, calculate the fibonacci of that number, asynchronously per number
let array = 2..12.map(\x #async fib(x))
let array = array.map(\x #await x)
; transform `array` into an array of all results

Error handling

Error handling is up to you. You can do Go-style error handling where you check optional types for nil, or you can use continuations to create (resumable) exceptions, or you can use the more Rust-flavored error types error<T>. Ananke offers the nil-safe ? and nil-coalesce ?? operators which also work for error<T>.

; Showcase of resumable exceptions

; value is a mutable integer
; throw is a function that takes an integer (the bad value) and a function (the continuation) that takes another integer (the correction).

; "throw" by calling the exception

fun worker(var value, var throw){
    value < 0 then throw( value, \correction {
    	; The exception handler can call this lambda if it is able to issue a correction
        value = correction
        break; Resume normal execution...
    } )
    ; ...to right here, past the }
    
    ; We only reach this point if value ≥ 0,
    ; either through having resumed the exception,
    ; or through it being correct from the start
    printl("We have this nice value: \{val}")
}

; void: returns nothing
fun exception_handler(int bad_value, fun(int)void resume_normal){
	print("exception caught - ")
	if bad_value < -10 {
		; this is really bad
		exit("fatal error!")
	} else {
		printl("recovered!")
		; 3 seems like a sane correction
		resume_normal(3)
	}
}

worker( 10, exception_handler); no issues
worker( -5, exception_handler); recovered
worker(-99, exception_handler); fatal error

Unicode

A Unicode-first language, meaning identifiers and strings may safely contain unicode. The standard string type and library are geared for UTF-8, the encoding standard of today. Ananke has no nul-terminator, making string handling faster and raw manipulation safer.

; the cross product operator: ×
; Ananke uses a Pratt parser which uses "binding power" to solve operator precedence
opr <ananke.bind_pow[ananke.mult].left>(var u)
	×
	<ananke.bind_pow[ananke.mult].right>(var v) {
	[u[1]*v[2] - u[2]*v[1],
	 u[2]*v[0] - u[0]*v[2],
	 u[0]*v[1] - u[1]*v[0]]
}
; unit test (eval forces check at compile time rather than run time)
eval check [1,2,3]×[4,5,6] == [-3,6,-3] else "cross product isn't correct"

Lifetimes

Novel lifetime system - you can declare something with an explicitly passed lifetime parameter, such as the lifetime of the caller. This prevents some need for “output” parameters. Ananke also offers multiple return values.

fun f(){
    fun<life L> g(){
        int<L> x = 4
        return x$
    }
    ; this is all safe
    let ptr = g<now>()
    ptr@ = 5
    return 1, 3, ptr@
}
let a,b,c = f()
printl("\{a}, \{b}, \{c}"); 1, 3, 5

Ergonomic function handling

uniform call syntax, first class functions, function composition, lamdas (but regular function notation is also just an expression) and closures. It also has user defined operators.

; A closure for an adder-factory

fun make_adder(let a){
	return \b a+b
}
; fully lambda notation:
let make_adder = \a \b a+b

let add_5 = make_adder(5)
let seven = add_5(2)
let eight = make_adder(1)(7)

; uniform call syntax:
; the first argument may be lifted out with `.`
let three = 1.make_adder()(2); make_adder(1)(2)

Linear algebra - vectors, matrices are native

let M = [[1,2,3],
         [4,5,6],
         [7,8,9]]
let I = [[1,0,0],[0,1,0],[0,0,1]]
let R = M*I; matrix multiplication
let U = [1,2,3]
let V = [4,5,6]
let W = U.cross(V); cross product
let x = U*V; dot product

Unit system with dimensional analysis

Do a lot of computational physics? You'll enjoy this.

; 2-way unit equivalence: <>
; allows inline declaration of units
unit ms <> 1000*(unit µs)
unit s <> 1000*ms
unit m <> (unit km)/1000

; speed of light in meters per second
let c = 299_792_458'm/s

; automatic unit conversion (because they are equivalent)
let x'km = c*400'ms; how many km does light travel in 400 milliseconds?
printl("\{x} km!")
unit N <> (unit kg)*m/s^2

let mass = 4000'kg
let force = mass*(C/100's); inferred unit N
unit K <> (unit °C) - 273.15
°C <> ((unit °F) -32)*(5/9) 

fun print_temp(var temp'°C){
	printl("It's \{temp}°C!")
}
print_temp(-40'°F); -40F = -40C
print_temp(500'°F); 260C
unit J <> (unit g <> kg/1000)*c^2; E = mc²
unit A <> (unit C)/s
unit V <> J/C; Joules per Coulomb

let N_a = 1.602176634e19; Avagadro's number
unit eV <> N_a*J; electron volt
1*eV/c^2 <> 1.78266192e-36*kg

Built in hash tables and array primitives

let animal_counts = ["dog":4, "cat":2, "mouse":2]
animal_counts["ferret"] = 3
for count, animal in animal_counts {
    printl("\{animal}: \{count}")
}

; you can use any type as a hash/index
type person {
	str name,
	str occupation,
	int age where 200 > it > 0; age between 0 and 200
}
person Archie = {"Archie", "Programmer", 20}
let Bill = person::{"Bill", "Designer", 34}
let salaries = [Archie: 3000'EUR, Bill: 3400'EUR]

; by default, Ananke will just hash the bytes of the data using a standard algorithm. Don't like it? Override the hash function for your type!
fun hash(person p){
	return (p.name.len()*7883 ~ p.occupation.len()*7901 ~ p.age*7907)%7919
	; bitwise xor operator: ~
	; modulo operator: %
}

Exact, bit for bit, data layout precision and execution

The exact and litend/bigend keywords are a godsend. Yes, you can finally overlay/pun several data types by using exact layouts, without any nasty surprises or undefined behavior. This is super useful for sending and receiving binary buffers of data. Write out the spec for a data structure, fill it in, write it directly to disk then read it back again, it just works, providing zero overhead data (de)serialization, so no more parsing or marshalling.

You can also tell the compiler to not optimize your code by prefixing an expression (that includes code blocks!) with exact, which is important to cryptographic libraries - no more volatile hacks!

exact u8[] data = {0x34, 0x30, 0x00, 0x2c, 0x34, 0x39, 0x38, 0x96}
exact type my_struct {litend i32 x, litend f32 y}
; little-endian members
let translated = data as my_struct; bit reinterpret cast
; translated now holds a copy of data
translated.x; we can access this without UB
translated.y; same here
; we could also safely alias `data` and `translated` by using pointers.

Why not write Ananke?