š¾ Archived View for dcreager.net āŗ swanson āŗ slip-and-slurp.gmi captured on 2024-06-16 at 12:13:57. Gemini links have been rewritten to link to archived content
ā¬ ļø Previous capture (2024-05-10)
-=-=-=-=-=-=-
Swanson uses a linear, continuation-passing, concatenative language called Sā as its assembly language. (As of right now, at leastāI've gone back and forth more times than I'd like to count on most of those!)
There shouldn't be any names in Sā
Continuation-passing Sā: The return
In a typical concatenative language, programs look ābackwardsā, since to invoke a quotation, you have to push the inputs onto the stack first, then the quotation, and then you invoke some kind of āapplyā instruction. Take this example from the Factor documentation for comparing two numbers:
USING: io kernel math ; 10 3 < [ "Math is broken" print ] [ "Math is good" print ] if Math is good
We first evaluate the conditional, then push quotations for the āthenā and āelseā clauses, and then we apply the āifā word.
But the program is only backwards for individual calls. Higher-level logicāthe sequencing of calls you want to makeāstill appear in āforwardsā program order:
USING: io kernel math ; 10 3 < [ "Math is broken" print ] [ "Math is good" print ] if 20 7 > [ "Math is still good" print ] [ "Math is still broken" print ] if Math is good Math is still good
Just as a warning, this example is going to look MUCH WORSE in Sā! Here it is:
# Ā¹uint_constants Ā³print { # ā°ten Ā¹uint_constants Ā³print { # ā°ten three Ā¹uint_constants Ā³print { # ā°is_lt Ā¹uint_constants Ā³print ātoā # Ā¹uint_constants is_lt Ā³print &true { # Ā¹uint_constants Ā³print { # Ā³print ("Math is broken")ā # Ā¹message Ā³print :printā } # Ā¹uint_constants Ā³print Īŗ :dropā } &false { # Ā¹uint_constants Ā³print { # Ā³print ("Math is good")ā # Ā¹message Ā³print :printā } # Ā¹uint_constants Ā³print Īŗ :dropā } # Ā¹uint_constants is_lt Ā³print Īŗ :evaluateā } # ā°ten three Ā¹uint_constants Ā³print Īŗ :ltā } # ā°ten Ā¹uint_constants Ā³print Īŗ :threeā } # Ā¹uint_constants Ā³print Īŗ :tenā
So everything is backwards, just like in the Factor example. But it's also somehow āinside outā, with worse callback hell than anything you'd see in JavaScript!
This isn't an Sā reference, so I'm glossing over some details, but to help you follow along:
So, what can we do to make this less painful? I recently introduced two new operators that I call āslipā and āslurpā, which make this look much nicer.
Importantly, both of these operators are purely syntactic sugar. You can apply their transformations to the instruction stream at parse time; they are not part of Sā's AST or runtime model or anything like that.
Slip implemented at parse time [git.sr.ht]
First up is slip. A slip precedes an instruction, and consists of 1 or more ā>ās. Its job is to āundoā the backwardsness of having to push parameters before invoking things. You'll typically use slip with an invocation, and your intuition should be that the number of ā>ā tells you the number of parameters (including any continuation!) that you want to pass to the invocation.
How it works is that you parse the slip, then parse an instruction (the āslipped instructionā) and put it off to the side. Then you parse one instruction for each ā>ā in the slip, adding them to the parsed instruction stream as usual. Lastly you add the slipped instruction to the instruction stream.
(Or put another way, slipping an instruction means āgo ahead and parse the next instruction, but don't add it to the parsed result until after you've parsed N other instructionsā.)
Looking at a small snippet of our example, using slip we could transform
("Math is good")ā :printā
into
>:printā ("Math is good")ā
And stepping one level out, we could then transform
{ >:printā ("Math is good")ā } :dropā
into
>:dropā { >:printā ("Math is good")ā }
(Remember, the continuation quotation is a single instruction, and the ā:dropā invocation gets slipped past the whole thing.)
And as one last example, we can slip over multiple instructions, transforming
ātoā &true { >:dropā { >:printā ("Math is broken")ā } } &false { >:dropā { >:printā ("Math is good")ā } } :evaluateā
into
>>:evaluateā ātoā &true { >:dropā { >:printā ("Math is broken")ā } } &false { >:dropā { >:printā ("Math is good")ā } }
(There are two ā>ās, so we skip over two instructions: āātoāā and the multi-branch continuation quotation.)
The other new operator is āslurpā. Slurp is signified by a ā~ā, and lets you write out a quotation branch without introducing another level of braces. You replace the opening brace with the tilde, and leave out the closing brace.
For instance, we can transform
>:dropā { >:printā ("Math is good")ā }
into
>:dropā ~ >:printā ("Math is good")ā
Slurp turns out to be most useful when you're already using slip, and when invocations expect to receive their continuations as their ālastā input. Together the two let us turn our original Sā example into
# Ā¹uint_constants Ā³print >:tenā ~ # ā°ten Ā¹uint_constants Ā³print >:threeā ~ # ā°ten three Ā¹uint_constants Ā³print >:ltā ~ # ā°is_lt Ā¹uint_constants Ā³print >>:evaluateā ātoā # Ā¹uint_constants is_lt Ā³print &true { # Ā¹uint_constants Ā³print >:dropā ~ # Ā³print >:printā ("Math is broken")ā } &false { # Ā¹uint_constants Ā³print >:dropā ~ # Ā³print >:printā ("Math is good")ā }
There's still extra complexity relative to the Factor example, since everything is linear, and because we don't have integer literals built into the language. But it's much nicer with slip and slurp than without!