In the last couple of months I got the itch once again to work on Ginger, a graph structured programming language that might become releasable before I die. As usual it's been more than a year since my last sprint of work on the language, and as usual I spent the majority of time fixing things where weren't broken.
One change which is very noticeable but which was actually pretty trivial to implement was the renaming of "operations" to "functions" across the board.
My intention has always been that Ginger should be completely approachable as a first programming language, and my decision around using the term "operation" stemmed from that. My reasoning now though is that most people are familiar with functions from math class, whereas "operation" is such a generic word that it almost means nothing. Better to keep the word which is more applicable within Computer Science generally, and whose alternate meanings aren't so far off.
In truth this wasn't really the first time in over a year I had worked on Ginger. I had started a fresh branch sometime around last Christmas where I was rewriting the existing VM in Rust (all work is currently done in Go). This was more of a Rust learning exercise for me, and I decided that it wasn't worth continuing with it. The purpose of the VM is only to bootstrap the language, so the extra development overhead of Rust isn't exchanged for long-term gain.
One thing which I did keep from the Rust rewrite was a BNF file which formally describes the
gg serialization syntax. This was accompanied by a new understanding of how a parser could work, which I brought back to the Go version of the VM.
(E)BNF at this point in time.
With these gains in hand I rewrote the whole parser... again. The result is much cleaner and easier to extend in the future, both manually and programmatically (an important future goal). As a byproduct of this work I also ended up with a generic parsing package whose elements map 1:1 to a BNF.
The generic grammar package, useful for parsing any BNF-described syntax.
The top-level example in those docs show how to use the package to both lex and parse an input string based on a statically described grammar. There's still improvements to be made, so don't expect a stable API.
For one, currently it's not possible to pass any state from one previous term to the next within Reduction. I won't go into details, but the result is effectively that the stack size grows to be the number of terms between two parenthesis/curly braces, rather than to be the depth of embedded parenthesis/curly braces.
Despite its shortcomings, the grammar package does work well for its purpose, and it allowed me to easily clean up the
gg syntax, removing the requirement for trailing
; terms in tuples and graphs, and better handling edge-cases generally. Refactoring to use the grammar package also helped me to formally realize a limitation of the graph structure which I hadn't fully noticed before: it's not possible to have a tuple as an edge-value.
* This is not possible !out = (func-factory < arg) < !in;
This is not a limitation of the syntax, but of the graph structure itself. At the moment I'm going to leave it as-is, as I can't think of any pleasant solutions. I have a vague idea around some kind of macro which would take advantage of a scoping builtin, such as would be used for creation of closures, but that's a ways off. In the meantime I guess I'll just have to give all runtime functions a name.
If you've been following Ginger development (or, more likely, if you're binging through previous posts all in one session) you may have noticed this in my example above: the exclamation point has been introduced as a prefix for all builtin names. A line like the following now generates an error (as it always had before):
!foo = anything
But all existing builtins (
recur, etc...) have had an exclamation point added as a prefix (
!recur). The reasoning here is pretty simple: I want to retain freedom now and going forward to establish new builtins without needing to worry about existing usage of those names.
In a previous post I had opined a bit on Ginger's name primitive, and within there left some open-ended discussion of implicit vs explicit evaluation of variables.
In summary, the question centered around how does Ginger know if a name is intended to mean just itself, the name primitive, or to mean whatever value that name has been made to resolve to. For example:
foo = bar; !out = foo;
Should the output here be
bar? We want
foo to be able to resolve to
bar should always be just
My solution which I've implemented for now is to simply say that, within any scope, if a name can't be resolved to anything then it resolves to itself. This neatly handles the question without introducing any new syntax or builtins. It may end up being confusing during actual writing of code, if someone tries to use a name which they forgot they set to be something else somewhere else in the same scope, but perhaps that won't be so common, and in any case Ginger will have a type checker (of sorts) which should make such cases immediately evident.
There were also some minor changes made, mostly around better tests and cleaning up unused code, but this was essentially it for this round. It's hard to say what I'll feel like tackling in a year from now when I get the itch to work on Ginger again, but off the top of my head I think the next lowest hanging fruit is to start to introduce builtins for traversing and constructing graphs and tuples, and from there perhaps to expound on the mysterious type system (not really) that I've alluded to multiple times.
Or perhaps I'll decide a new syntax is in order.
This post is part of a series.
Previously: Ginger: A Small VM Update
This site can also be accessed via the gemini protocol: gemini://mediocregopher.com/
What is gemini?