Keemun: a curious micro-language
Feb. 4th, 2008 11:17 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Saturday evening I started making a language.
I had a working lexer (better: a lexer generator) by the time I went to sleep, and I knocked together a parser on Sunday. I added some more advanced features to it tonight, and it's to a point where it can be played with, or at least described.
Funny thing is, I had no real plan going in, and even less of one now. I was just noodling around; I don't even have a use for a new language. Both projects that I am working on will be perfectly fine with JSON/Script.
Keemun
The language is used to walk a tree of objects. You don't really write programs in it so much as you write paths. I was thinking of it more as a messaging protocol than a language, so it's designed around the idea of short single statements.
There is currently no way to build an object tree in-language, you have to build it in Script and then use Keemun to query it. Queries can have side-effects, also determined by the structure of the tree in Script.
BasicsTrees are made up of nodes. Each node has a key and a value. Keys are always strings. Paths are made up of dot-separated key names:
players.1.name
Key names can also have funny characters or multiple words in them if you quote them:
journalists.'Robert McX'.network
Internally in the object tree, some nodes can be functions. If a path reaches a function, then that function gets called with the rest of the path as parameters:
board.move.g1.f3
is equivalent to (in Script)
board['move']('g1','f3');
There are three other things to learn; subexpressions, lists (two ways to use them), and a special hook for dynamic object structuring.
SubexpressionsI said that paths can contain things other than keys. One possible thing is a subexpression:
players.send.(players.find.Joe).'Smack talk!'
The part in parentheses will be evaluated first, in the same environment as the outer expression, and its value will be plugged into that expression. If players.find.Joe
evaluates to 3, say, then the outer expression players.send.3.'Smack talk!'
gets evaluated. Subexpressions can be nested, of course.
Subexpressions are evaluated before function calls! players.send
gets '3' and 'Smack talk!' as parameters, not the subexpression.
Lists are like subexpressions, but they use brackets ([ ]
) instead of parens. Lists, unlike subexpressions, are not evaluated before function calls, they are evaluated like any other key. This means that one possible use for lists is to logically group function parameters:
people.find.[[name.'Callahan, Gary'].[occupation.'Senator']]
This is useful because it means you can easily have a function tell its arguments apart from the rest of the path (the first argument, in a list, is the function arguments, the rest something else). So, your function can return a value by recursively calling keemunEval:
function findFeed(criteria,_args_){ var Feed=findWithCriteria(criteria); return keemunEval( unit, cdr(arguments) ); // first arg is environment, second is expression }
Something like that would make this work:
findFeed.[name.'The Hole'].posts.latest
The posts.latest
won't be evaluated until after the findFeed, and it will be evaluated on what findFeed wants it to.
What happens if you give a list in a path and it's not evaluated by a function? Well, in that case, it splits the evaluation, and returns a list. This is a little hard to explain without an example, so imagine the object tree is this:
{ x: { 1: 'a', 2: 'b' }, y: { 1: 'c', 2: 'd' } };
Then, here are some expressions with lists:
[x.y].1 -> a, c x.[2.1] -> b, a [y.x].[1.2] -> c, d, a, b
Neat, huh? Of course, functions can return lists (they're normal Script arrays) and cause keemunEval to be called on them, so you can pretty quickly build up interesting constructions.
method_missingThere is one special key in every object, called ?
. This key is a function that, if it exists, is called when you reference a member that doesn't exist (think Ruby's method_missing
). It receives the entire rest of the path as arguments (including the missing element) so it can do what it likes with them, from print out an error message (boooring) to something like this:
function selfBuildingObject(){ return { '?':function(name,_args_){ this[name]=selfBuildingObject(); if(arguments.length===1){ return null; // Stop here, no further to build }else{ keemunEval( this[name], cdr(arguments)); } } }; }
With an object made with this, anything you reference will magically appear. You can use it to quickly build up an object tree, or to map out what some program expects your object tree to be, or whatever.