100 Languages Speedrun: Episode 97: Quackery

Quackery is a simple stack-based languge embedded in Python. It describes itself as "lightweight language for recreational and educational programming, inspired by Forth and Lisp", so let's see how fun it is, without considering its production use.

Hello, World!

Installation is quite messy, I needed to checkout the git repo, add #!/usr/bin/env python3 on top of quackery.py, chmod +x and symlink it. It really doesn't take that much work to provive a pip package, so hopefully the author does it. Otherwise it's difficult to recommend it even for casual play.

say "Hello, World!"
cr

It works just fine, but annoyingly always displays two extra newlines at the end, to show its empty stack.

$ quackery hello.qky
Hello, World!

cr prints a newline. It used to be called "carriage return" in the olden days and some old languages had it as cr, it feels silly nowadays, nl would be a far better name.

say is a bit more interesting. As Quackery is generally stack-based you'd think it should be something like "Hello, World!" say. That actually doesn't work - Quackery doesn't have strings. say is a special kind of "word" (a "builder"), that does some special things while parsing, intstead of just normally working with what's on the stack.

Working with Strings

Quackery has three types. Numbers, lists ("nests"), and functions. Notably no booleans (it's just 1 and 0), and no strings (just "nests" of numbers).

We can just do a stack-based Hello, World!

72 emit
101 emit
108 emit
108 emit
111 emit
44 emit
32 emit
87 emit
111 emit
114 emit
108 emit
100 emit
33 emit
13 emit

Annoyingly Quackery does not actually print what we send, it does some weird filtering, and it insists that 13 (CR) is the newline character, even though it's actually 10 (NL) on every system. I have no idea why, it's baffling. The last system that used CR-based line ending remembers the Cold War.

We can put the string in a nested list (and quote it to make it clear we don't want to execute it), then use witheach emit to iterate it:

' [ 72 101 108 108 111 44 32 87 111 114 108 100 33 13 ] witheach emit

There are macros for both these $ followed by a string puts that nested list of byte values on top of the stack, and echo$ does the witheach emit loop. There's no escape codes like \r or \n, so we need to do cr separately:

$ "Hello, World!" echo$
cr

This was all surprisingly painful for a Hello, World!

Unicode

Quackery has the absolutely worst Unicode support of any language so far. All others have at least decency of passing through any characters they don't care about:

say "Żółw eats 🍨"
cr
$ quackery unicode.qky
???w eats ?

We can what goes on the stack with this program:

$ "Żółw eats 🍨"
$ quackery unicode2.qky
[ 379 243 322 119 32 101 97 116 115 32 127848 ]

So the codepoints get pushed onto the nested stack just fine, but then emit replaces them with ? for some crazy reason.

This is baffling af. This isn't some language from the 1980s, it was created in December 2020, and keeps getting regular updates. Why is it actively trying to fight Unicode, while just not giving af and sending whatever number we have out would have worked! Here's what Quackery does if I remove that stupid ASCII check in emit:

$ quackery unicode.qky
Żółw eats 🍨

It's not much, but at least them we can build our own Unicode support.

Math

So far we encountered more exceptions than cases where it actually did, but Quackery is mostly a stack-based language:

20 18 3 + * echo cr
$ quackery math.qky
420

Each number pushes itself to the stack, and each mathematical operator pops two numbers off the stack, and pushes the result back. echo pops the top number from the stack and prints it as a decimal number (unlike emit which prints it as ASCII code).

Then we need to add cr for extra newline.

Hello, Name

[
  $ "Hello, " echo$  ( print "Hello, " )
  echo$              ( print the name )
  $ "!" echo$ cr     ( print "!\n" )
] is hello

$ "What is your name? " input
hello
$ quackery name.qky
What is your name? Bella
Hello, Bella!

We cat define functions with [ ... ] is <name>. Functions take some arguments from stack and return any number of values to the stack.

Comments use ( ... ) - note the spaces, everything in Quackery (as well as most other stack-based languages need spaces as separators).

input takes prompt (in form of list of numbers) as argument and returns user input (again, in form of list of numbers) to the stack.

Loop

There are a few ways to loop. The most obvious is <n> times [ ... ], but it's weird. It doesn't push counter on the stack as one would expect, you need to call i to get the counter. And it does count down (9 to 0), not count up (0 to 9).

10 times [ i echo cr ]
$ quackery loop.qky
9
8
7
6
5
4
3
2
1
0

If we want nice loop, we'd need to translate that. 10-i is the number we want:

10 times [ 10 i - echo cr ]

$ quackery loop2.qky
1
2
3
4
5
6
7
8
9
10

Fizzbuzz

[
  dup 15 mod 0 =
  iff [
    drop say "FizzBuzz"
  ] else [
    dup 5 mod 0 =
    iff [
      drop say "Buzz"
    ] else [
      dup 3 mod 0 =
      iff [ drop say "Fizz" ]
      else [ echo ]
    ]
  ]
  cr
] is fizzbuzz

100 times [ 100 i - fizzbuzz ]

Other than two extra empty lines at the end, it displays the usual FizzBuzz sequence.

Fibonacci

Let's do a very reasonable Fibonacci sequence:

[
  dup
  3 < iff [
    drop 1
  ] else [
    dup
    1 - fib
    swap
    2 - fib
    +
  ]
] is fib

[
  $ "fib(" echo$
  dup echo
  $ ")=" echo$
  fib echo
  cr
] is display-fib

20 times [ 20 i - display-fib ]

Unfortunately it doesn't work, because Quackery doesn't support recursion:

$ quackery fib.qky
Unknown word: fib

The second idea is to replace fib with recurse, but that just hangs, I'm guessing because it wouldn't recurse the funciton, only the else-block.

Somehow this weird code works, but I suspect only for this specific nesting level (recurse for first and ]this[ do for second):

[
  dup
  3 < iff [
    drop 1
  ] else [
    dup
    1 - ]this[ do
    swap
    2 - ]this[ do
    +
  ]
] is fib

[
  $ "fib(" echo$
  dup echo
  $ ")=" echo$
  fib echo
  cr
] is display-fib

20 times [ 20 i - display-fib ]
$ quackery fib2.qky
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34
fib(10)=55
fib(11)=89
fib(12)=144
fib(13)=233
fib(14)=377
fib(15)=610
fib(16)=987
fib(17)=1597
fib(18)=2584
fib(19)=4181
fib(20)=6765

There are ways to rewrite this to not use recursion, or only use it from the top level, but it's all just ridiculous.

Side Stacks

Quackery doesn't have "variables", but it has "side stacks", which are basically the same thing, except you can pop them to restore previous variable.

[ stack ] is name
[ stack ] is surname

[
  surname put
  name put
] is push-person
[
  name release
  surname release
] is pop-person
[
  $ "Hello, " echo$
  name share echo$
  $ " " echo$
  surname share echo$
  $ "!" echo$
  cr
] is display-person

$ "Harry" $ "Potter" push-person
$ "Hermione" $ "Granger" push-person
$ "Ron" $ "Weasley" push-person

display-person
pop-person
display-person
pop-person
display-person
$ quackery sidestacks.qky
Hello, Ron Weasley!
Hello, Hermione Granger!
Hello, Harry Potter!

Step by step:

  • we declare two side stacks, name and surname
  • we define push-person to push name and surname to the side stacks
  • we define pop-person to remove top name and surname from the side stacks
  • we define display-person to print Hello, #{name} #{surname}!
  • then we push 3 people, and display them in backwards order
  • <value> <side-stack> put pushes to a side stack
  • <side-stack> release> drops top value from a side stack
  • <side-stack> share copies top value from a side stack onto the main stack

This is how a lot of functionality in Quackery is implemented. For example that magic i for current loop iteration, that just takes times.count share. And that's why nested loops can work - and if you need iterator of some outer loop, well, you'll need to access times.count side stack directly.

This part of Quackery design is quite clever.

Should you use Quackery?

Considering how broken everything was for even the most basic things, no. And although I don't particularly love it, I think Factor (which changed how autoloading works after I posted the review, it's much better now) might currently be the best Forth-like to play with.

My advice to the creator of Quackery, to make the language more friendly:

  • package it so people can pip3 install quackery and then run quackery hello.qky without any problems
  • remove anti-Unicode features, just make emit print whatever
  • fix recursion to just work, this is dumb
  • support #! - basically ignoring the first line of any script if that's what it starts with
  • make cr use 10 (\n) not 13 (\r), it's just a weird pointless distraction

Code

All code examples for the series will be in this repository.

Code for the Quackery episode is available here.