Let me describe Reverse Polish Cowgirl, the most poorly designed language I know about, and I know all about it because I'm the one who designed it.
I've been playing around lately with Scheme (mostly Chicken Scheme). One of the major features of Lisps and Schemes is that they seem to inspire one to do Real Computer Science type stuff like design programming languages and implement interpreters for them. I was bitten by this as well, and have hacked together what might be charitably described as an interpreter for a new programming language. I call it Reverse Polish Cowgirl, or RPC, because it is a stack-based language, therefore using Reverse Polish Notation, and I'm living in Texas at the moment. The universe essentially forced me to make this stupid joke, and so I did, but I do apologize.
The language doesn't really have a grammar, exactly. It is a stack-based language, like Forth. The only thing the interpreter does is to seperate its input into strings based on whitespace. It then turns those strings into a list of Scheme atoms, and pops them off the stack according to some simple rules. The result is a horrible mess of a language, but here are the basics:
-> Numbers are any string that Chicken Scheme's
string->number
procedure will accept, but prefixed with a hash symbol ("#"). It means "number," get it?. Examples: #1
#1.344e21
#500010
-> Built-in functions begin with an exclamation point ("!"), because, you know, action!!!!. Examples:
!add
!eq?
!swap
!set
!obliterate
!get
-> Variables begin with "@" because that's what I decided at some point. I don't have a cute explanation or mnemonic device for this one. Examples:
@a
@b
@_almost.anything,except&^__white,.\<\>
.
-> There are some special keywords that aren't really built-in functions, but which probably should be for cleanliness' sake, although quibbling about the inelegance of this language is like complaining about the wetness of water. Anyway, here are the special keywords:
->
true
and false
are what you would expect them to be.
->
{
and }
and used for delimiting strings, as described below. { This is a string in RPC }
->
func
and unfunc
delimit user-defined functions (which, because this is a horrible language, are treated much differently from built-ins). Here I define a function that adds 2 to the value on the top of the stack, and assign it to a variable named @add2
:
func
#2 !add
unfunc @add2 !set
->
ifelse
is the only conditional in RPC. If the top thing on the stack is true
, it executes the user-defined function two beneath it. Otherwise it executes the one directly beneath it. It actually does this by removing the function it doesn't want to use and then pushing the !do
function on top of it. !do
is the built-in function to execute a user-defined function. Here's what it looks like in action:
func
{ That's right! } !put
unfunc @truthmessage !set
func
{ That's wrong! } !put
unfunc @falsehoodmessage !set
@truthmessage @falsehoodmessage
#1 #1 !add #2 !eq?
ifelse
Strings are set off by
{
and }
. Any "words" that begin with @
, !
, or #
, or which are also reserved keywords, should be prefixed with ~
, or things will likely fail in ways that are hard to debug, since RPC doesn't produce error messages that are very useful, even to me, the author.
So that's the basic syntax. Now let's talk about some of the other ways it is horrible.
Many languages say that they treat functions as first class objects. This means that you can pass functions around as functions to other functions and possibly even do neat stuff like create lexical closures. RPC functions can be bound to variables, like Javascript and the Lisps, but don't mistake that for it being powerful or having closures. In fact, the only way define functions for later use is to assign them to variables. This means that user-defined functions are second-class citizens when compared to the built-in functions. User-defined functions can be called by pushing a copy of the variable that holds them onto the stack and then the
!do
builtin, or with the ifelse conditional construct as described above. Here's an example of !do
(note that !mul
is the multiplication built-in and !put
pops off the top of the stack and displays it)
func
!dup !mul
unfunc @square !set
#5 @square !do !put
This bifurcation between built-in functions and user-defined functions sucks, and it means that if you want to use a built-in function in an
ifelse
statement, you have to wrap the built-in in a user-defined function. This won't work, for example (!kill
is the builtin that destroys the topmost value on the stack):
{ That's a big number! } !put !kill @numpizzas #100 !biggerthan? ifelse
You have to do this instead:
func
!put
unfunc @put !set
func
!kill
unfunc @kill !set
{ That's a lot of pizzas! }
@put @kill
@numpizzas #100 !biggerthan?
ifelse
The next thing that sucks about RPC there is no such thing as variable scope. Actually, more accurately, variable scope is always global. Also, you can't even define functions that use variables if they haven't been !set globally yet. I don't mean that you need to declare them, like in Pascal or something. You actually have to give them values, because the only way to instantiate a variable is to
!set
it. Like all mistakes in RPC, failing to respect this will produce a cryptic error message that's very little help in determining what went wrong unless you go back and read the source code to the interpreter, and even then it's probably not that helpful unless you are also very familiar with Chicken Scheme's error messages.
On top of these broken and missing features, the language does not allow comments, though you can workaround this by pushing strings to the stack and then immediately removing them with
!kill
. I call these "suicide comments," and they are a perfect metaphor for trying to write anything in RPC.
{ TODO: figure out why this is broken } !kill
So that's a taste of the kind of a mess you can get yourself into if you decide it might be neat to write a programming language. I plan on getting myself into even more trouble in the future by trying creating a more sophisticated language with a real grammar and parser. I may even include helpful error messages, but that may be a stretch.
I'm not including the Scheme source for the RPC interpreter, though maybe I will in a later post. In the meantime, if you have need of an (barely) interpreted language with almost no features and which barely works, let me know and I'll send to a copy of the source.