šŸ’¾ 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

View Raw

More Information

ā¬…ļø Previous capture (2024-05-10)

-=-=-=-=-=-=-

Slip and slurp

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

Concatenative languages look backwards

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

if [Factor documentation]

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

Let's make it worse

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]

Slip

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.)

Slurp

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!

Swanson

Concatenative programming languages