Workpad
April 17th, 2024

Lists, Hashs, and Loops

UCL

A bit more on TCL (yes, yes, I've gotta change the name) last night. Added both lists and hashes to the language. These can be created using a literal syntax, which looks pretty much looks how I described it a few days ago:

set list ["a" "b" "c"]
set hash ["a":"1" "b":"2" "c":"3"]

I had a bit of trouble working out the grammar for this, I first went with something that looked a little like the following, where the key of an element is optional but the value is mandatory:

list_or_hash  --> "[" "]"        \# empty list
                | "[" ":" "]"    \# empty hash
                | "[" elems "]"  \# elements

elems --> ((arg ":")? arg)*      \# elements of a list or hash

arg --> <anything that can be a command argument>

But I think this confused the parser a little, where it was greedily consuming the key arg and expecting the : to be present to consume the value.

So I flipped it around, and now the "value" is the optional part:

elems --> (arg (":" arg)?)*

So far this seems to work. I renamed the two fields "left" and "right", instead of key and value.  Now a list element will use the "left" part, and a hash element will use "left" for the key and "right" for the value.

You can probably guess that the list and hash are sharing the same AST types. This technically means that hybrid lists are supported, at least in the grammar. But I'm making sure that the evaluator throws an error when a hybrid is detected. I prefer to be strict here, as I don't want to think about how best to support it. Better to just say either a "pure" list, or a "pure" hybrid.

Well, now that we have collections, we need some way to iterate over them. For that, I've added a foreach loop, which looks a bit like the following:

\# Over lists
foreach ["a" "b" "c"] { |elem|
  echo $elem
}

\# Over hashes
foreach ["a":"1" "b":"2"] { |key val|
  echo $key " = " $val
}

What I like about this is that, much like the if statement, it's implemented as a macro. It takes a value to iterate over, and a block with bindable variables: one for list elements, or two for hash keys and values. This does mean that, unlike most other languages, the loop variable appears within the block, rather than to the left of the element, but after getting use to this form of block from my Ruby days, I can get use to it.

One fun thing about hashes is that they're implemented using Go's map type. This means that the iteration order is random, by design. This does make testing a little difficult (I've only got one at the moment, which features a hash of length one) but I rarely depend on the order of hash keys so I'm happy to keep it as is.

This loop is only the barest of bones at the moment. It doesn't support flow control like break or continue, and it also needs to support streams (I'm considering a version with just the block that will accept the stream from a pipe). But I think it's a reasonably good start.

I also spend some time today integrating this language in the tool I was building it for. I won't talk about it here, but already it's showing quite a bit of promise. I think, once the features are fully baked, that this would be a nice command language to keep in my tool-chest. But more of that in a later post.