Lucas Sifoni

Hosting a small language (Ovo2) from scratch in Elixir, pt 8
The end : the Ovo Optimal Personal System (oops), with liveview

elixirexplorations


full code hosted on github

This post allows me to find closure with Ovo, an experiment that went off-track but that gave me a lot of joy in pursuing it.

After the last post, I widened the standard library a bit, with a few additions :

I also made a graphical way of using Ovo with Liveview. It was the first time I used liveview, so the code isn’t really idiomatic, but it was fun to build. The graphical part will stay in this messy state as my experiment found its conclusion. This post features a “complex program” made with this clunky way of thinking about state.


Editing values

The graphical representation is rendered by walking the Ast, and value editing is made possible by keeping track of every node traversed up to a displayed node.

In the video just above, we can see how editing a graphical node edits the corresponding source code.


Chaining runners

You can easily select multiple runners to run them in chain. In a chain, their execution results are still pushed to their stacks. The chain has the arity of its first runner.

In the video just above, we can see how chaining two runners can allow to compose a bit more complex programs.


Popping values from other runners

Since rshake has been introduced, you can use values pushed to the stack of a runner, from within another runner (or program).

In the video just above, we can see how you can use the stacks of other runners in a third party program.


Remote invocation of other runners as functions

With invoke, you can easily make your own library of programs as functions, filling their stacks at the same time.

In the video just above, we can see how using invoke to run a runner as a function fills both the result stack of the caller and the callee.


Building a complete example

We will build the canonical 99 bottles of beer with the graphical editor of Ovo. Of course, this could be a single program by recursing from 100 to 0, but that wouldn’t take advantage of the OOPS’s capabilities of small, modular, atomic programs working together to build high-performing systems.

We will create a first runner, that I’ll call the register, who will be responsible of holding numbers.

# hashes to EWTiZxncF
arg(0)

It just returns its arg, but by being executed, it creates a stack of values.

We will not fill this register manually, but by creating another program, the filler :

# hashes to 5sxMB4BiC
foo = \n ->
    if equals(n, 0) then
      0
    else
      invoke(`EWTiZxncF`, [n])
      foo(subtract(n, 1))
    end
end
foo(arg(0))

It works by recursing from N to 0, invoking the register with invoke at each run. We will call it manually when the time has come.

We will then need to manipulate strings, so enter a third program : join. To avoid bloat, you can build your own atomically-available join instead of using a bland, standard-library given join.

# hashes to qjYwZaa3J
join = \list,  joiner ->
    reduce(\a, b ->
    concat(concat(a, joiner), b)
    end, list, ``)
end

join(arg(0), arg(1))

It isn’t a perfect join, but it’s ours to run with.

We will also need three programs, one for the plural verses, one for the singular verse, and a last one for the last verse, where all bottles have been consumed. Instead of having a single, bloated program with conditions and checks, we will build three runners, each one carefully holding its sentence (don’t forget qjYwZaa3J is join) :

 # hashes to pgeqCPg/3
terms = [arg(0), `bottle of beer on the wall`, arg(0), `bottle of beer.`, `Take one down and pass it around. No more bottles of beer on the wall.`]
invoke(`qjYwZaa3J`, [map(\a -> to_string(a) end, terms), ` `])

# hashes to QUl1z0wvi
terms = [arg(0), `bottles of beer on the wall`, arg(0), `bottles of beer.`, `Take one down and pass it around`, subtract(arg(0), 1), `bottle of beer on the wall.`]
invoke(`qjYwZaa3J`, [map(\a -> to_string(a) end, terms), ` `])

# hashes to SLtzGVzyT
terms = [arg(0), `bottles of beer on the wall`, arg(0), `bottles of beer.`, `Take one down and pass it around`, subtract(arg(0), 1), `bottles of beer on the wall.`]
invoke(`qjYwZaa3J`, [map(\a -> to_string(a) end, terms), ` `])

The final program takes advantage of all this modularity, to build the song, verse by verse, remaining focused in its responsibilities :

n_bottles = subtract(100, rshake(`EWTiZxncF`))

run = \n, out ->
    verse = if greater_or_equals(n, 3) then
        invoke(`SLtzGVzyT`, [n]) # more than two
    else
        if greater_or_equals(n, 2) then
            invoke(`QUl1z0wvi`, [n]) # two bottles
        else
            invoke(`pgeqCPg/3`, [n]) # no more bottles
        end
    end

    if equals(n, 0) then
        out
    else
        nout = invoke(`qjYwZaa3J`, [[out, verse], ``]) # join
        run(subtract(100, rshake(`EWTiZxncF`)), nout) # pop a value from the register
    end
end

run(n_bottles, ` `)

Would it have been responsible to just recurse from 100 to 1 without the brave register ? Would it have been clean to have join in the same program instead of giving the hash qjYwZaa3J to friends, so they can too join strings ? To me, it’s a clear no.


Running the system

We first run EWTiZxncF, our brave register, one time, just to pre-heat it. Then, calling 5sxMB4BiC with the argument 100 fills the result stack of EWTiZxncF with numbers from 100 to 1. Then, everything has been set, and running the final program generates the song lyrics.

In the video just above, we can see how an user can use the system to generate a song with their desired number of bottles.

Conclusion

Are you running ovo in production ? I’d love to hear from you.

This was fun, from start to finish. Every stage has quirks, but the whole experience has allowed me to reinforce past projects of this style, and push it further. I had a kind of childhood dream about building a language that could be graphically interacted with - this isn’t really usable since a lot of edge cases aren’t handled, but a fun journey anyway.

¯\(ツ)


Previous post : Hosting a small language (Ovo2) from scratch in Elixir, pt 7
Weird features, towards a global stateful machine

Next post : I would never allow users to upload PDFs in place of pictures