
\ |
_ \ __ \ _` | __ \ | / _ \
___ \ | | ( | | | < __/
_/ _\ _| _| \__,_| _| _| _|\_\ \___|
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)
- WASM
- x86-64(bare, linux, windows)
- ARM64(bare, linux, mac)
- Local interpreter
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?
- Obscure language
- No support for "traditional" object oriented programming
- Not a lot of libraries available
- In fact, no working implementation exists
- In super fact, the language specification is still under construction
- Your boss will likely shoot you if you suggest using it in prod