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
andsurname
- 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 printHello, #{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 runquackery 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
use10 (\n)
not13 (\r)
, it's just a weird pointless distraction
Code
All code examples for the series will be in this repository.
Top comments (1)
Hi, sorry for the slow reply, but I don't often vanity google.
Short version. I'd all but finished Quackery when my friends pushed me into putting it on GitHub. It was only ever a personal project, not intended for anyone's use other than my own, so everything is just the way I like it.
I'm a relic who remembers the cold war. So it's basically recreating my experience of using an Apple 2 at college, when a language came on a disk with a ring bound manual and a small amount of demo code. That's exactly what you get. A language that fits my requirements exactly, and the book I wish I'd had when I was 20. Exept now it's a download from GitHub with a pdf.
Thank you for your review. I very much enjoyed reading it. :-) And would like to address some points.
I'll look into this pip package thing.
' [ 72 101 108 108 111 44 32 87 111 114 108 100 33 13 ] witheach emit
Yes, that is painful. Exactly why there's the syntactic sugar of
$
andsay
. Extending the compiler with "builders" is as simple as I could make it, if you want more sugar. :-)No, it doesn't have "the absolutely worst Unicode support". It just doesn't support Unicode. I prefer it that way, because sending random numbers (which is what characters are) to Python and saying "print these characters" is a fairly good way of crashing Python abruptly and without a helpful error message. When that happened several times during development, and I was reading news reports about text messages that can brick your phone because of certain unicode sequences, I rather went to the other extreme and decided, "actually, newline, space and the ASCII printables are all I need, so everything else is a
?
and that's fine." Go right ahead and remove the filtering inemit
if you prefer. I've made the Python code as easy to understand as I can, so it is a trivial task to make trivial changes. Likewise if you want to have escape characters, go right ahead.If you want to put a
\n
into a string, it's calledcarriage
and use whatever nest editing words you like.join
,poke
orstuff
spring to mind.Incidentally, Forth still calls it
CR
.times
doesn't put the index on the stack because a lot of the time it isn't required, so you'd have to explicitlydrop
it.Yeuch. Just put an
i
or ani^
where it's required and keep the stack clutter down.If you want to count down to zero, use
i
. If you want to count up from zero usei^
(Pronounced "aye-up", which is amusing if you're from Yorkshire, which I am. Just be glad I stopped calling "print to screen"quack
and changed it to the more subtle joke ofecho
.)Yes, Quackery does have recursion. You can't automatically mention the name of a word you're defining until it has been named, and the naming word
is
comes after the definition. So you have to forward reference it. Like this.There, fixed it. :-)
I'm glad you like the side stacks. I was well pleased with them and gave them a fancy name. "Ancillary" stacks.
The bit that pleased me most was the mix and match control flow wordset. I still get a buzz from writing code like
(context)
And even more so the meta-control flow words. (You can add new control flow words easily, and it doesn't even require extending the compiler.) (example: case)
Anyroad up, things that would please me, from least to most.
TTFN, and BCNU, Gordon.