Introduction
Pipescript is a new programming language I created, with the intent of using it as an in-game scripting language in a computer game I'm developing. Many games allow user-generated "mods," which externally modify the behavior of the game through scripts, but few games incorporate user-code in the gameplay mechanic.
Why not create a sort of programming game? The premise: you are a programmer inside a programmable universe. You advance through levels by a combination of conventional gameplay (weapons, etc.) and programming. For example, you could "hack" into an enemy robot, read its source code, and modify it to malfunction. Or you could program your own robots to assist you. Barriers could require users to solve programming puzzles, and locked doors could require exploiting bugs in the door's source code... in-game programming unlocks numerous interesting gameplay mechanics.
But of course, in order to incorporate a programming language in a game, we first need a programming language (and a corresponding compiler/interpreter). And so, Pipescript was born.
Implementation
I set about writing Pipescript mainly as a learning experience, since I had never written a compiler or interpreter before, and was intrigued by the algorithms involved. The Pipescript interpreter is written from scratch in C#, and uses an recursive-descent parser for its LL(k) grammar.
I have written a prototype interpreter for Pipescript, which is incomplete and meant for testing only, but can be accessed here: http://naksvr.nakkiran.org/pipenet/.
Design
I meant to design Pipescript as a clean, functional language, but inevitably it became corroded by the influence of C-style syntax. Nevertheless, here are the original principles I tried to follow:
- Data manipulation should be easy
- Functions are first-class objects
- Readable for C programmers
- Tolerable for functional programmers
- Not difficult to write an interpreter for
Usage
Pipescript is not nearly complete, but is already capable of some interesting functionality (to try this out, type lines into the textbox at the bottom left of the interpreter here: http://naksvr.nakkiran.org/pipenet/):
To create a list, the primary data object in Pipescript (no, this is not LISP!):
x = [1,2,3]
Now we have the symbol "x" bound to the list of numbers {1,2,3}. Let's map each element in the list to its square root (the output of the evaluation is marked by "#...")
x => sqrt #[1, 1.4, 1.7]
That was easy! And now we see the reason behind Pipescript's name: it relies heavily on "pipes". Map-Pipes are the "=>" operator, and they map each element on the left side by the function on the right side. Of course, they can be chained:
x => sqrt => print #1 #1.4 #1.7
Okay, what if I want to square each element. Hmm, there's no built in function to do that, but let's try a labmda:
x => (a):a*a #[1,4,9]
Lamba syntax defines a function by first listing its arguments in parentheses, followed by a colen, then an expression. We can define a plus function, for example, like so:
plus = (a,b):a+b
And call it:
plus(1,2) #3
But there's another way!
(1,2) -> plus #3
The single-pipe is the "->" operator, and they simply call a function on the RHS with arguments on the LHS. Why two ways of calling function? To simplify chaining functions for data processing. For example, one can imagine (psuedocode):
getEnemyPositions() => rotateTurret => fire
Now for more complex things:
Functions can be defined either with or without closures. "Isodef" means within an isolated scope, "def" means including the enclosing scope (closure).
Here is a simple factorial function:
isodef factorial(x)
{
if x == 0
{
return 1
}
return x*factorial(x-1)
}
And now, a more complex function that calculates the length of a list using a closure:
isodef len(L)
{
x = 0
def inc(a)
{
x = x + 1
}
L => inc
return x
}
Finally (for now), a function that takes another function, and returns a function that filters a list using the other function as a predicate (ah, the beauty of first-class functions):
isodef filter(func)
{
def filt(list)
{
filtered = []
list =>
(a):
{
if func(a)
{
filtered = rpush(filtered, a)
}
}
return filtered
}
return filt
}
Here is how to use the filter function to find even numbers:
[1,2,3,4] -> filter((a):a % 2 == 0) #[2,4]
Notice that "filter((a):a % 2 == 0)" actually returns another function, which is then passed the list. The filter function itself acts as a function factory.
Addendum
More details to come as I finish Pipescript. I am aware of several existing bugs, including some scoping issues. For reference, here is the semi-formal grammar I drafted as I was designing Pipescript:
<block> := (<statement>)*
<statement> := <fullexpr>
| <name> '=' <fullexpr>
| WHILE <fullexpr> LCURL <block> RCURL
| IF <fullexpr> LCURL <block> RCURL (ELIF <fullexpr> LCURL <block> RCURL)* [ELSE LCURL <block> RCURL]
| (DEF|ISODEF) <name> <list OF NAMES ONLY> LCURL <block> RCURL
| RETURN <fullexpr>
<fullexpr> := <compexpr> ('->' | '=>' <compexpr> )*
<compexpr> := <expr> ( < | > | == <expr>)
<expr> := <term> (PLUS | MINUS | MOD <term>)*
<term> := <factor> (MULT | DIV <factor>)*
<derefFactor> := <factor> ( DOT <name> optionalindexer optionalparanlist)*
<factor> :=
| <atom> (LBRACK <fullexpr> [: [<fullexpr>]] RBRACK)* (<paranlist>)* #atom indexer, function call
<atom> :=
FNUM
| LPAREN <fullexpr> RPAREN
| <name> #variable
| <list>
| <parenlist OF NAMES ONLY!> COLEN <fullexpr> #lambda
| <parenlist OF NAMES ONLY!> COLEN LCURL <block> RCURL #lambda (explicit func)
<paranlist> := LPAREN <expr> (',' <expr>)* RPAREN
| LPAREN RPAREN
<list> := LBRACK <expr> (',' <expr>)* RBRACK