⣄⠔⠄⡨⣀⣹⣥⣣⡚⣿⣓⣾⣫⣷⠮⡧⣬⣬⣑⢤⠤⡉⣿⡥⣂⢟⣕⡴⠬⠆⠸⡈⡆⠀⠀⠀⡋⠄⠂⠨⡆⠀⠠⠃⡀⠀⠀⠈⢰⣿ ⠩⣽⢟⢭⠶⢷⢵⣻⠿⢍⣟⡅⡧⡭⣽⡯⡯⣶⠭⣽⡢⠱⢿⢧⠓⢥⠨⠧⠉⣡⠐⠀⢑⣀⠂⠈⠆⡅⠀⡀⢥⠤⢁⠀⢣⠀⠀⢘⠽⡏ ⠬⡓⣼⢟⢜⡧⣷⣇⣧⡷⢯⣿⣿⣿⡶⣭⠍⣯⣻⢥⠞⣮⣬⣩⠅⢏⠆⠁⡀⠌⢩⡁⢁⠅⠡⢀⠠⠀⢉⢈⠀⠀⡅⠀⡇⠀⠀⠃⠭⣯ ⡎⣟⣵⠅⣾⢭⣿⣾⣯⣿⣿⢿⣿⣿⣿⣿⣧⡳⣥⢜⣟⡤⠉⣁⢧⢨⠌⡂⠀⠒⠬⡪⣊⠄⠀⠘⠀⠢⡁⠀⡀⠅⠅⠀⡄⠀⠈⠀⢕⡟ ⡕⣧⡗⠎⠷⣯⣿⣿⣷⣿⡻⢵⣿⣿⣿⣭⣿⡿⣟⣇⡧⣿⡭⢨⠫⠮⠄⠖⠀⠆⠦⠕⠁⠅⠀⠆⡁⠁⡀⠀⠄⡉⠁⠀⠄⡀⡂⠀⢼⣇ ⠃⣶⣯⠢⡗⣟⣿⡿⣿⣯⢿⣷⢾⢿⣛⣿⣿⣯⡻⣾⡾⣾⢵⣏⣩⠅⠅⠥⠀⠃⠠⠅⠃⠀⡈⠀⠭⠅⠀⠀⠁⠔⠀⠀⢂⠅⠅⠀⠽⡇ ⠱⣻⣻⡥⢫⢯⢿⣷⣷⣿⣿⢾⢿⣷⢯⣉⣟⣟⣾⣿⠳⣿⣇⣃⠯⠘⠔⡇⠒⠅⡖⠁⠄⠡⠑⣀⠀⡁⠀⡀⠃⠈⠀⠀⠅⠀⠀⠂⣋⡇ ⢂⠵⣓⣿⣍⢯⢯⡯⣟⣝⠷⣟⢝⣋⡏⣧⠯⣿⣿⢯⣼⣯⡂⡂⣟⡧⠐⣅⠂⠥⠏⠀⠂⡅⠅⡁⠁⠄⠠⠊⠀⠨⠀⢈⠀⠄⠉⢘⡟⡆ ⡥⣯⢌⡤⢽⣗⣏⠗⢿⣻⡭⡞⡷⣭⢿⡻⡯⡟⣕⣾⢽⢯⡭⠂⡍⠀⠏⠀⢌⠴⠂⠊⡄⢗⠂⠰⠀⠤⠣⠀⠀⠀⠁⠀⠀⠀⠅⠀⢕⡽ ⠵⡭⡩⠋⠽⣽⡛⡹⢯⠍⡛⡏⣯⢟⠟⠿⣩⣳⡿⡟⢗⣭⡤⡯⠁⡬⠠⠂⢄⠂⢤⠀⢖⠀⠀⢀⠂⢖⠀⠘⠀⠀⠂⠡⢤⠠⠀⠀⣫⡿ ⠀⢰⡇⢂⣃⠙⣧⡵⡙⠇⡷⠶⡷⠶⡟⡙⣩⡌⠯⡺⡍⡲⡸⣆⠼⠅⡱⠌⠡⠂⢄⣠⠃⠀⠀⡂⠈⠂⠠⢀⠀⠀⠀⠔⠀⠂⠂⠀⠨⣯ ⡔⠰⠼⣝⢪⢋⢋⠣⣧⡦⣦⡁⡭⠽⡛⠁⡫⢡⡄⠿⡫⣕⣔⢳⠌⢡⡸⠁⠁⠂⣠⡇⠀⠀⠀⠍⠈⠄⠀⠀⢀⠀⠔⠆⠁⠈⠠⠀⡹⡏ ⡖⣠⠄⠀⠯⢜⢨⠊⣒⢡⠨⡀⠢⢭⠴⡊⢕⠐⣄⡓⡾⠋⠗⠉⢀⠚⠀⢃⠂⠥⠲⠀⠀⠀⠄⠐⠀⠀⠐⠀⠁⠄⠱⠁⠊⠈⡀⠀⠊⣽ ⠀⡉⠒⣃⠰⡀⢁⠘⡉⠪⠋⡲⡅⠍⡎⡪⢣⠽⢭⠍⡥⠁⠂⡰⠠⣄⠥⠐⠀⠣⠄⢀⠐⠁⡀⠀⢐⠐⠀⠠⠁⠂⠄⠠⠀⠁⡀⠐⣫⡟ ⠐⢔⠁⠀⠬⠑⠗⠴⠀⠢⡁⢌⠅⢘⠓⡛⠈⡀⠄⠆⠠⠠⠢⢂⠕⠷⠈⢀⠈⡒⠄⠂⠀⠀⠈⠀⡀⠡⠈⠀⠆⡐⠀⠀⠓⠀⠀⠀⢐⢼ ⡀⠀⠈⠐⠠⠀⠀⠁⠁⠣⢀⡁⡄⠂⠔⠂⠂⢉⠁⡌⠄⠜⠈⠡⠐⠀⡐⠀⢨⠀⠁⠀⠀⡀⠍⠄⠠⠁⠀⠠⢄⠈⠀⠀⡄⠀⠀⡀⠒⣸ ⢘⠉⡒⠤⠌⢀⠑⠕⠀⠄⠄⠄⠀⠄⠑⠉⠨⠀⠄⠀⠂⠃⠀⠀⠈⠄⠄⠠⠈⠀⠀⡀⠠⠁⢠⠁⠁⠀⠐⠱⠀⠀⠀⡐⠀⠉⠄⠀⢸⠾ ⠀⠘⠈⡀⠈⠀⠈⠨⠐⠠⠠⠐⡀⡄⢠⠀⢀⠨⠀⠃⠀⠀⡁⡠⠀⠩⠀⠂⠁⠀⠀⠀⢒⡄⠊⠀⠀⢀⠈⠁⠀⠀⠀⠆⠘⠁⠄⠀⠄⣿ ⠄⡄⠀⠈⠠⠀⡀⠀⠀⠄⠀⠂⠀⠀⠄⠀⢀⠁⠁⢁⠁⠆⠀⠀⠀⠀⡀⠀⠈⠀⡀⠀⠱⠀⠀⠀⠀⠂⢄⠈⠀⠀⡀⡨⠠⡀⠠⠀⠄⡼ ⠠⠀⠁⠊⢀⠀⠠⠁⠁⠂⠂⡄⢄⠐⡀⢀⠊⠂⡀⡁⡀⢀⢀⠠⠀⠄⠂⠡⠠⠁⠀⠀⠀⠂⠀⢠⠒⠀⠀⠀⠐⠢⠀⢀⠀⠀⠄⠄⣿⣿ ⠁⠋⠙⠊⠐⠕⠁⡂⠈⠅⠠⠈⠐⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠀⠉⠀⠐⠀⠀⠀⠀⠀⣈⠀⠀⠀⠀⠈⡂⠈⠀⠀⠘⠌⢀⠥ ⠀⠀⠀⠀⠔⠔⠀⡀⡉⠀⠀⠀⠂⠠⠠⠀⠄⠄⠀⣀⢀⠀⡈⠀⠌⡐⠈⠈⠀⠀⠀⠀⠅⠀⣐⠉⠄⠀⠀⣂⠠⠁⡈⠀⠄⠄⢀⠣⢶⢿ ⠄⠀⠀⡁⠀⠄⠀⠀⠁⠈⠀⠈⠀⠁⠀⠁⡀⠁⠀⠁⠀⠀⠀⡀⠀⠀⠀⠀⠀⠠⡀⠑⠀⠀⠎⠀⢬⢠⠀⢂⠠⠢⠠⠈⡐⠸⠀⡐⣹⢷ ⠩⠥⠂⠆⡆⢠⠀⢄⠀⣀⡀⣄⠀⢈⠀⠀⣀⡀⡀⡀⠄⡄⠡⠀⠐⠀⠑⠀⠀⠁⠒⠒⠉⠡⡀⠈⠈⢀⠥⠈⠀⠀⠀⠀⠈⣞⣏⣿⡏⣿ ⠾⠝⠷⢿⣯⣿⠧⡧⠭⠣⠭⡭⡮⢭⠶⠶⠶⠦⣗⡶⠽⠭⣿⣶⠷⠵⠮⠽⠿⠮⠿⢷⡶⣖⡷⠲⠞⠷⠾⢿⢷⣿⣿⣿⣿⣯⢇⠟⡧⢈
pipes
clean data flow without nesting
things flow from top to bottom
"hello!!"
|> string.upper
|> string.sub(0, 4)
|> fn(s) s + ", world!"
|> inspect
# or with placeholders,
"hello!!"
|> _:upper() # obj:method() == obj.method(obj)
|> _:sub(0, 4)
|> do
# you can even put any expression here
# even do-end blocks
_ + ", world!"
end
|> print
# this is the same, but look at the order of operations
print(log("hello":upper():sub(0, 4) + ", world!"))
errors-as-values
nil and booleans are replaced by atoms
you can’t use a value without handling an error
all crashes are explicit (WIP)
aided massively by pattern matching, ?, orelse, and :unwrap()
# f might be
# (:ok, "file-contents")
# or (:err, :IoError)
const f = fs.open({path = "./readme.md"})
# `?` unwraps :ok and panics on :err at top-level
const f2 = fs.open({path = "./readme.md"})?
# crashes if :err
const f = fs.open({path = "./readme.md"}):unwrap()
everything is an expression
no statements, everything (really) always returns a value
…but the code still looks procedural
let x = 10 # this line evaluates to 10
let label = if x > 0 "positive" else "zero"
let a = let b = 5 # this whole line evaluates to 5
fn is_true() 5 + 5 == 10
# both x and is_true are the same function
const x = fn is_true() do # do-end is one too
# return and break are special
return 5 + 5 == 10
end
comp
execute any (really) expression at compile time
any script can be compiled into bytecode and get any value baked in
revo -b script.rv
revo script.rvo
the compile-time VM does not differ from the runtime one
# asks for a ling of input at build-time
# then keeps the result at run-time
const x = comp read()
const long = do
let t = 0
for x in 0..100
t += x
t # similar to rust's {},
# revo do-end blocks return the last value
end
procedural macros
along with an AST-substituting macro system,
this lets you just get an iterator over the raw ast tokens, run any code to transform them
, then return back a table of the new ast
# > num, num, num -> Sigma^4_n=1(a * b + c)
proc cmul!(iter) do
print("inner: ", add3!(10,20,12))
print("peek: ", iter:peek())
match iter:peek()
| (:number, n) => print("is number", n)
| (other, n) => print(other, n)
| x => print("not tuple: ", x)
let a = 10 + (iter:next_of(:number))
let b = iter:next_of(:number)
let c = iter:next_of(:number)
let acc = 0
for i in 1..5 do
acc += a * b + c
end
{(:number, acc)}
end
print(cmul!(10,20,30))
pattern matching
destructure and branch in place
you will be using atoms and tuples, they are beautiful solutions to their problems
match (:ok, 42)
| (:ok, v) => v
| (:err, e) => panic(e)
| _ => panic()
| _ => panic()
# _ is wildcard, when nothing else matched
# if you want to grab the actual value
# , just put any binding name there
const response = match "hello!"
| "hello!" => "hi!"
| x when (x:len() > 10) => ""
| x when string?(x) => x + " to you too!"
| _ => ":("
let f = match read({path = "./readme.md"})
| (:ok, file) => file
| (:err, error) when error == :FileDNE
=> panic("file does not exist")
| (error) => panic("error")
| x => panic("unknown: ", x)
fibers
i made all your blocking code become non-blocking by just adding a spawn before it
fn serve(peer, message) do
peer:send(message)?
end
while :true do
# accept the next connection; if none is ready, this fiber parks until the
# runtime sees a connection on the listening socket.
let conn = server:accept()?
# the only thing you have to do to make it async is to add `spawn` here!
spawn serve(conn, port - 1)
end
tables
represent everything
used for
- module exports
- arrays
- maps
let t = {1, 2, 3, key = "value"}
let rec = {name = "revo", version = 1}
rec.name
t[0]
let mt = {
name = fn(self) self.name,
set_version = fn(self, v) self.version = v,
DELTA = 0.0,
}
# a metatable is just a "table overlay"
# which you can slap onto other tables
set_metatable(rec, mt)
let rec = {name = "revo", version = 2}
set_metatable(rec2, mt)
# name does not exist in rec or rec2,
# but you can still call them
assert_eq(rec:version(), 1)
assert_eq(rec2:version(), 2)
assert_eq(rec.DELTA, rec2.DELTA)
convenient typing
the type system is optional, but very well-integrated
untyped code works just fine, but
typed code is faster and gets optimized better (and ensures code correctness at compile-time!)
most of your code is going to be inferred automatically
type Result =
(:ok, any)
| (:err, atom)
struct User {
name: string = "me",
age: int = 21,
fn get_age(self) -> Result
(:ok, self.age),
}
# type is inferred
let user = User{}
print(user:get_age())
first-class tests
they’re just closures and they fail when you return an error. the ? postfix operator propagates errors, giving you a pretty simple experience
fn add(a, b) a + b
suite "add" do
test "addition" do
expect(add(20, 22) == 42)?
expect(add(20, 22) != 22)?
end
test/skip "adds two tables" do
expect(add({1,2}, {3, 4}) == {4,6})?
end
end
quick start
the only dependency is zig version 0.16.0:
the repository is also available on codeberg
cd /tmp
git clone https://github.com/if-not-nil/revo
zig build -Doptimize=ReleaseFast # static build with vendored
./zig-out/bin/revo -e 'print("hello " + "world")'
# then, put that binary in your path
cp ./zig-out/bin/revo ~/.local/bin/revo
usage
please check revo -h first
revo script.rv # run script
revo # start repl
revo -e "1 + 2" # inline code
revo -b script.rv # script.rv -> script.rvo
revo -b -o output.rvo script # custom output path
revo --bench1 script.rv # benchmarks script.rv
revo --test script.rv # runs with test blocks
revo --dis script.rv # bytecode disassembly
lsp
see ./lsp