Pseudo-interpreter of KM-like text-based actions

I'm so excited that I just got a new macro working, (it's still in prototype stages) that I have to share my progress. Perhaps there's no need for a macro like this, but I just wrote a macro that can "execute text-based code." To do this I had to create two macros:

  1. A macro called Parse that splits/stores code with braces into "atoms" of code in a dictionary;
  2. A macro called Interpret that executes the parsed code.

Here's how it works. You can create some "code" in a variable that looks like the following, and you pass the variable to the Parse macro.

The Parse macro is primarily concerned with finding all matched parentheses and placing individual statements into a dictionary called Code. The keys used for each block of code are random UUIDs. The root of the Code is remembered in a variable called LocalCodeRoot. In the example above, the dictionary that will be created will contain the following items, and the first line of this dictionary is the root.

358AE1F7-DB0F-4E65-B193-F03E2A10A0BF=set a1 to 3
if 5421EFCA-CA3E-46E3-885F-7D7FEAEA8088 then 9C676483-DAC9-4C54-A99B-CC96971EE0DE else 395C125E-14A9-4BAA-800E-009520C5F975
while 16AE0508-A4F2-48BB-998F-61623D19C411 do BD8DA623-69BE-414C-8ED4-43E1286A1FCE
repeat 489C6D12-F912-4EDA-A8FD-E2EEF72066EA until 9ABB13EF-9D2D-4DF5-BCB5-CBB681FF84B6
9ABB13EF-9D2D-4DF5-BCB5-CBB681FF84B6=RAND(3)=1
9C676483-DAC9-4C54-A99B-CC96971EE0DE=set a2 to 3
16AE0508-A4F2-48BB-998F-61623D19C411=a2>0
358AE1F7-DB0F-4E65-B193-F03E2A10A0BF=set a1 to 3
if 5421EFCA-CA3E-46E3-885F-7D7FEAEA8088 then 9C676483-DAC9-4C54-A99B-CC96971EE0DE else 395C125E-14A9-4BAA-800E-009520C5F975
while 16AE0508-A4F2-48BB-998F-61623D19C411 do BD8DA623-69BE-414C-8ED4-43E1286A1FCE
repeat 489C6D12-F912-4EDA-A8FD-E2EEF72066EA until 9ABB13EF-9D2D-4DF5-BCB5-CBB681FF84B6
395C125E-14A9-4BAA-800E-009520C5F975=set a2 to 4
489C6D12-F912-4EDA-A8FD-E2EEF72066EA=say goodbye
5421EFCA-CA3E-46E3-885F-7D7FEAEA8088=a1<3
BD8DA623-69BE-414C-8ED4-43E1286A1FCE=say hello

I know that's hard to read because of all the UUIDs, but basically it's just a tree of statements, with each branch being named by a UUID. I'm not sure if this is the most elegant design, but it works.

Then the root of this tree is passed to the "Interpret" macro, which starts at the root and executes each line in each branch, but calls itself recursively if it comes across another UUID. Processing each line is also performed. I currently have about five different kinds of statements implemented (while, if, repeat, say, set) but this will expand now that the Parse macro works.

At the very highest level, the Interpret macro looks like this:

Then, as an example, here's the code that handles the While statement. The first thing it does is extract the two UUIDs representing the condition and the code of the While statement, like this:

image

Then the UUIDs for the condition and the code are passed recursively to another instance of the Interpret macro, like this:

image

So I'm not really creating or compiling a new language, it's more like using KM's abilities to implement text-versions of KM actions. I'm not sure if this approach has any usefulness at this time, it's just an area of research that I'm undertaking.

This wouldn't be possible if KM didn't have some amazing features, like the Calculate Filter. Without such features, this sort of thing would be much more difficult.

Ideally, I would like to implement dozens of KM actions using this approach, and also support a large number of KM's tokens. Why? Well, one of the last things I was working on in university before I found a full time job was a compiler. This is giving me a sense of accomplishment for failing to finish that task.

1 Like

Why?

.

Very impressive stuff! I'll have to play around with it this weekend.

On the subject of the UUIDs, if they're to make sure your actions can always be uniquely identified (which is why you're using them, I assume?), wouldn't a shorter UUID be both easier to read and quicker to search on?

A mix of six upper and lower case letters has something like one in 19 billion odds of generating the same code twice; change that to seven characters, and the odd are one in a trillion. Add in the digits, and it's one in 3.5 trillion. And you can generate such things with one shell command:

$ openssl rand -base64 6 | tr -dc 'a-zA-Z0-9' | head -c 7
eAz31tw

I found the above, sort of, in this GitHub repository. As I experimented with each of those commands, I found that the openssl version was the only one that returned a string of guaranteed letters, so I just munged together different parts of the various commands there to make mine work :). I just now added the numbers to the tr command's input to get an even more unique ID.

(I went looking for is for this recent macro that generates Obsidian note files using TextEdit; the random bit assures each file is unique.)

If this doesn't apply to your needs, I apologize for the wasted ink :).

-rob.

I found the post a bit too much to take in at once but, looking at it again, I gather that the UUIDs are generated, to make unique keys for detected statements.

This argument makes sense to me:

While you could just generate 32 random digits and call your home-grown ID format “good enough”, it’s nice to use standards that already exist. Aside from the fact that there are safe libraries you can use to work with standard UUIDs, it’s nice to look at the UUID format and know “hey, this is an ID”! If you roll your own format, you’ll likely confuse members of your team.

Source: What Are UUIDs, and Are They Better Than Regular IDs?

Well, I guess I'm lucky then as my team is one :). Seriously, thanks for the explanation, and it does make sense to me. In this case, though, if I were developing a macro that needed unique IDs and also needed to be easily readable and editable, I'd be inclined to pick the shortest string I though would guarantee uniqueness—at least to the level of guarantee that I felt I needed.

But clearly this wouldn't be the ideal solution for many situations.

-rob.

Aren't they just the identifiers internally generated by Keyboard Maestro itself – accessible in the GUI, for example, via Edit > Copy As > Copy UUID ?


FWIW when you do want to generate fresh UUIDs for yourself, you can reach for Apple's built-in NSUUID library.


AppleScript

use framework "Foundation"

current application's NSUUID's UUID's UUIDString as string

JavaScript for Automation

$.NSUUID.UUID.UUIDString.js
1 Like

That was my first thought, but they exist for lines within one KM action, at least based on how I'm reading what was posted: There are multiple UUIDs for the commands in the text box, i.e. "set a1 to 3" has a UUID, as does {a1<3}, etc. So I'm not sure what they are or where they're coming from.

-rob.

Yes. But when I'm developing new code, I prefer code that is "quicker to write." Also, there's a second reason that you didn't anticipate. My design (which I may change, because I'm not sure I've settled on a final design) is intended to distinguish between "token purposes" by its actual value. I.e., if a token is in the shape of a UUID, then it's a code pointer, but if it's shaped like your format, "eAz31tw", it's either a variable or a language keyword. If you want there to be overlap, then you have to come up with some way of distinguishing between the two,

1 Like

Why? What else would you have? These are "pointers." How would you implement pointers?

To be honest, my very first thought was that my first pointer would be "1", my second would be "2" and so on. But then I realized I would have to add some syntax to distinguish between pointers and other programming tokens. So I decided on simplicity.... a token's long length meant it was a code block pointer.

That makes sense. Given that design requirement, I probably personally (because I'm lazy and hate long numbers :)) would have used an identifier in front of my unique ID. tABC123 versus vABC123 versus kABC123, and then just check the first character to see which type of ID I'm dealing with.

But again, just personal preference :).

-rob.

Thanks for saying that.

At this "point"(er) in time, I think we're arguing about the "branches" instead of seeing the big forest.

Two jokes in one sentence. Shakespeare would have been proud. I read somewhere that he managed three jokes in one sentence only seven times in his life.

1 Like

As indicated imperfectly by my own reply to my question, I had not initially latched onto the UUIDs being automatically generated. I then wondered what advantages UUIDs had in particular, which is what lead me to the blog post I mentioned, 'What Are UUIDs, and Are They Better Than Regular IDs?'. So I hope I was by then thinking along the right lines, but please let me know if I missed the point after all. It just wasn't obvious to me from the opening of the thread why UUIDs were being used.

I would prefer not to. :wink:

You're fine with me. To be honest, there are a dozen experts on this website, and I can't keep them all straight. I just have you marked in my head as one of those experts.

Me? That's going to be a null pointer!

1 Like

Or, since we're all KM users and proud of it, the %RandomUUID% token.

1 Like