Getting to Know ParselTongue

This document is a tutorial and language guide for learning ParselTongue. You will get the most out of it if you stop to run the examples, read the questions, and tweak the examples to see what happens. Every time you do, you've created a new test. Keep in mind also that while your immediate goal is to test ParselTongue, you'll be implementing the whole language within a month, so details and corner cases you learn now (and test for!) will only help you later.

In the tutorial, we assume that some script called psl will run ParselTongue programs given as file arguments. Here's a simple bash script that you can use; copy-paste and store in a file named psl in the directory you unpack the binary to:

#!/bin/bash

./debian-dist/bin/assignment1-debian --interp < $1

Note that the above is just for brevity of examples; you are free to run

$ ./debian-dist/bin/assignment1-debian --interp < filename

each time if you like (or the equivalent command for your architecture).

ParselTongue by Example

Here are a few examples of classic programs in ParselTongue, to get you acquainted:

fibs.psl

# deffun defines a recursive function
deffun fib(n)
  # operators like + and == are prefix
  if <(n, 1) then 0 else
  if ==(n, 1) then 1 else
  +(fib(-(n, 1)), fib(-(n, 2)))
# The in ends the function body in a deffun, which can be followed by
# another deffun, a defvar, or a braced expression
in
# defvar creates a new variable with an initial value, bound inside the
# body of the defvar
defvar x = 0 in {
  # For loops have an initialization, a test, an update, and a body
  for(x = 0; <(x,10); x++) {
    # print takes any value and displays it
    print(fib(x));
    print(" ");
  };
  # The result of the program is the value of the last expression
  # evaluated.  It is printed, so we avoid printing it by terminating the
  # program with a ""
  "";
}

$ psl fibs.psl
0 1 1 2 3 5 8 13 21 34

list.psl

# When empty is called, a new object with the isempty field mapping to a
# function is created
deffun empty() { isempty: lambda(self) { true } } in
# Here we define a cons, which allows accessing its first element and the
# rest of its elements.  It reports that it is not empty
deffun cons(elt, lst) {
  first: lambda(self) { elt },
  rest: lambda(self) { lst },
  isempty: lambda(self) { false }
} in
# length() takes a list and computes the number of elements in the list.
# The @ syntax is the syntax for message passing
deffun length(lst)
  if (lst@isempty()) then 0
  else +(1, length(lst@rest()))
in {
  print(length(cons(5, cons(2, empty()))));
  print(" ");
  print(length(empty()));
  "";
}

$ psl list.psl
2 0

The Basics

We'll give a more in-depth tutorial by examining features one at a time. The smallest ParselTongue programs are just values; we'll start there.

5.psl

5

$ psl 5.psl
5

The example's formatting means that the file 5.psl contains just the character 5, and when we evaluate it with ParselTongue, we get 5 back.

Let's try it again:

str.psl

"a string"

$ psl str.psl
a string

Here, we printed out the string (with no quotes). That's how ParselTongue prints strings.

We can also evaluate booleans:

true.psl

true

$ psl true.psl
true

ParselTongue is, of course, Object Oriented. Objects are defined with curly braces:

object.psl

{x:5}

$ psl object.psl
object

ParselTongue doesn't print all the fields of an object; it just prints the string "object" when the program's answer is any object.

We can get fields out of objects, using the dot operator. For now, we'll just get fields directly:

getfield.psl

({x: "my field value"}).x

$ psl getfield.psl
my field value

(The parentheses are sometimes necessary in ParselTongue for annoying parsing reasons. ParselTongue version 0.2 will have better surface syntax.) We can see that by accessing the x field, we get back the string value "my field value" that we stored in the field.

We can also use the bracket operator to get fields via a string value:

getfieldstr.psl

({x: "my field value"})["x"]

$ psl getfieldstr.psl
my field value

What happens if we try to get a field that isn't there? Let's try it:

getnofield.psl

({x: "my field value"}).y

$ psl getnofield.psl
Field not found: y

ParselTongue tells us that the y field wasn't found, and prints an error message telling us so.

We can change the fields on objects after we create them, by using dot and equals:

addafield.psl

({x:"before"}).x = "after"

$ psl addafield.psl
object

Well, the result is an object... But we need to see something about the object to tell if our program worked. Let's try something a bit more sophisticated:

addafield2.psl

(({x:"before"}).x = "after").x

$ psl addafield2.psl
after

This requires a little unpacking. The first thing that happens here is the definition of the object {x:"before"}. Next, its x field is changed to "after". Then, its x field is accessed (the outermost .x). Now we see that the result of the dot-and-equals operation updated the x field.

What happens if we add a field that doesn't exist? Try it!

Printing things, and doing more than one thing

Sometimes, it's nice to see intermediate output from programs, rather than just the final answer. It would be nice to be able to see several different outputs, as well. In ParselTongue, we can chain sequences of statements by using semicolons, and print things using print:

print-sequence.psl

print("hello!\n");
print({x:2});
print("\n");
"end";

$ psl print-sequence.psl
hello!
object
end

Here, three print expressions evaluated one after another, and finally the program resulted in the string "end". ParselTongue printed the first three strings (one was interpreted as a newline by the shell), and then printed the answer of the program, "end". When print evaluates, its result is the string that it printed:

print-value.psl

print("hi!")

$ psl print-value.psl
hi!hi!

The string "hi!" prints twice, once for the effect of print, and once because the program's answer is "hi!".

Primitive Operations on Values

As a Web language, ParselTongue has basic support for arithmetic, string manipulation, and value comparison. The operators in ParselTongue are <, >, +, -, and ==. This sample program shows some of their uses. Note that all operators are prefixed in front of their arguments, which are enclosed in parentheses and separated by commas:

operators.psl

print(<(3,4));
print("\n");
print(>(4,5));
print("\n");
print(+(3,4,5));
print("\n");
print(-(9,8,7));
print("\n");
print(+("one ", "two ", "three"));
print("\n");
print(==(5,5));
print("\n");
==("foo",5);

$ psl operators.psl
true
false
12
-6
one two three
true
false

What happens if you apply these operators to mixed types of arguments, including objects? Try it!

Note that now we can compute the names of fields that we'd like to look up:

objfieldplus.psl

({longfield: 5})[+("long", "field")]

$ psl objfieldplus.psl
5

Naming and changing things

In our examples above, we could only use each value we made once. ParselTongue lets us save our values to use later by using defvar blocks:

defvar.psl

defvar o = {x: "myfield"} in { o.x }

$ psl defvar.psl
myfield

This code defines a variable o, which is bound to the object value {x: "myfield"}. The curly braces around o.x delimit the definition's body. Inside the braces, we can use o as a way to refer to the object we defined earlier, so we're doing an object field lookup just like before. We can chain defvar statements together for multiple definitions, with one final body. They can be used to bind any value, or even evaluate an expression first and store the result, as in this example:

defvars.psl

defvar o = {x: "myfield"} in
defvar f = o.x in { f }

$ psl defvars.psl
myfield

Here, we used two defvars, one after another, to first define the object that is referred to by o, and then to store the x field of o in the variable f. This body of the second defvar is the result of the whole program, which we see in the output.

Once we've defined a variable, it doesn't have to stay the same forever; ParselTongue is an Imperative language, after all. We can use the assignment operator, =, with variables to change their value:

changevar.psl

defvar x = "before" in {
  x = "after";
  x;
}

$ psl changevar.psl
after

The variable x starts with the value "before", but changes to "after" because of the assignment. We can check with a print statement to be sure:

changevar2.psl

defvar x = "before" in {
  print(x);
  print("\n");
  x = "after";
  x;
}

$ psl changevar2.psl
before
after

We can see for sure now: x was "before" until the assignment, and then its value changed.

Earlier, we saw that we could also use = to change fields on objects, and here we saw that changes to variables made with = are visible later. Is the same true for objects? Try it!

We can combine assignment with the + and - operators we saw earlier. By using += and -=, we can write a shortcut for performing the operation and an assignment:

changecombo.psl

defvar x = 24 in {
  x += 18;
  x;
}

$ psl changecombo.psl
42

The variable x started at 24, and its initial value was added to 18 and reassigned into it by the second line. Compound assignment works on objects, too, but has a slightly different result; it returns the entire object with the operation performed:

objectcombo.psl

defvar o = {x:"four"} in {
  (o.x += "score").x
}

$ psl objectcombo.psl
fourscore

Object assignment works this way because the assignment makes a new copy of the object, rather than overwriting the old field. For example:

objectcombo2.psl

defvar o1 = {x:"four"} in
defvar o2 = o1.x += "score" in {
  print(o1.x);
  print(" ");
  o2.x;
}

$ psl objectcombo2.psl
four fourscore

This shows an important feature of objects in ParselTongue: they are merely flat data structures, and adding and removing fields copies them and updates them, rather than making changes for all holders of the object to see.

Finally, as a Systems programming language, ParselTongue supports the increment and decrement operators. The operators ++ and -- can appear before or after a variable, and will either add or remove 1 to it. If they appear before, the result of the expression is the modified value; if after, it is the original value. Concretely:

inc-dec.psl

defvar x = 5 in {
  print(x++);
  print(" ");
  print(++x);
  print(" ");
  print(x--);
  print(" ");
  (--x);
}

$ psl inc-dec.psl
5 7 7 5

Making decisions and doing things more than once

Most interesting programs make decisions. ParselTongue supports this through if-then-else expressions. These expressions switch which branch they execute depending on if the test is false or not:

if.psl

if false then print("then branch\n") else print("else branch\n");
if true then print("then branch\n") else print("else branch\n");

$ psl if.psl
else branch
then branch
then branch

What happens if you give something other than false and true to if? Try it!

It's useful for programs to repeat the same operation multiple times. As a Procedural programming language, ParselTongue provides two mechanisms for doing so. The first is the while loop, which will repeatedly perform an operation until a test yields false:

while.psl

defvar x = 10 in {
  while(>(x,0)) {
    print(x);
    print(" ");
    x = -(x,1);
  }
}

$ psl while.psl
10 9 8 7 6 5 4 3 2 1 0

The body of the loop executed until the comparison >(x,0) failed (when x became 0). What was the result of the program? What happens if an error happens in the middle of the loop? Try it!

While loops are useful, but often end up repeating the same patterns. The for loop gives a convenient form for a number of common loops:

for.psl

defvar x = 0 in {
  for(x = 0; <(x, 10); x = +(x, 1)) {
    print(x);
    print(", ");
  }
}

$ psl for.psl
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ,

The for loop executes the first of the three statements in parentheses first. Then, it tests the middle expression, runs the body, and then finally the third expression in parentheses. As long as the middle expression keeps evaluating to true, it repeats this pattern. The result of the whole loop is the last expression that it evaluated; does this tell you why there is a trailing comma in the output? What would the result be if the test had been false to begin with? What about for while loops? Try it!

Functions and methods

Perhaps the most exciting facet of Parseltongue is its Functional capabilities. ParselTongue uses the keyword lambda to define functions. Functions do several important things. We'll start by seeing how to define them:

func1.psl

defvar f = lambda() { print("not til later\n") } in {
  print("now\n");
  f();
}

$ psl func1.psl
now
not til later
not til later

Here, we defined a function with lambda, and gave it a print expression for a body (the part in between curly braces). We stored it in f, and when we wanted to evaluate the expression we had stored in its body, we called it with f(). Then the print expression evaluated and we saw not till later.

Functions can also abstract over common computations. Much like defvar, lambdas can declare variables that they will use in their body. The values that get stored in those variables are provided when the lambda is called:

func2.psl

defvar f = lambda(x, y) { +("Chapter ", x, ": ", y, "\n") } in {
  print(f("One", "Object-Oriented Programming"));
  print(f("Two", "Functional Programming"));
  "";
}

$ psl func2.psl
Chapter One: Object-Oriented Programming
Chapter Two: Functional Programming

Here, the values for x and y were determined twice, once for each set of arguments. Each time, the body of the lambda stored in f was evaluated, and printed the concatenation of the static strings with the provided ones.

Finally, functions can capture variables that are not their arguments, and observe and change their state. This can be used to communicate between two functions, and facilitate a number of interesting patterns:

func3.psl

defvar x = "" in
defvar f = lambda(y) { x += +("f says: ", y, "\n"); } in
defvar g = lambda(y) { x += +("g says: ", y, "\n"); } in {
  f("hello!");
  g("did you add that?");
  f("who let you add to this string?");
  x;
}

$ psl func3.psl
f says: hello!
g says: did you add that?
f says: who let you add to this string?

The same variable, x, is closed over by both f and g. Thus, when either makes a change, it is apparent when the other goes to change the variable again, resulting in shared state.

What do you think happens if there's a defvar inside a function that binds one of the function's arguments again? What if you pass too many or too few arguments to a function? What if you pass expressions to a function instead of just values? Try it!

Functions can also be recursive, meaning that they can call themselves. To create a recursive function (or to create a named function with easy shorthand), use the deffun construct:

deffun.psl

deffun times(n, m)
  if <(n, 1) then 0 else +(times(-(n,1),m),m)
in {
  times(3,4)
}

$ psl deffun.psl
12

Here, times can call itself during its own execution, enabling a loop purely within a function. What would happen if you tried to emulate this with defvar?

ParselTongue has support for message passing as well. Methods in ParselTongue are merely lambdas that happen to be stored as fields on objects. The special @ form allows passing an object as the first parameter of the lambda. For example, we would call translate a method here:

methods.psl

defvar point = {
  x: 2, y: 3,
  translate: lambda(self, x, y) {
    (self.x += x).y += y
  }
} in
defvar point2 = point@translate(4, 5) in {
  print(point2.x);
  print(", ");
  print(point2.y);
  "";
}

$ psl methods.psl
6, 8

Here, point is initially set to an object with x and y fields, each set to a number. It also has a field called translate, which is a function of three arguments. The variable point2 is then set to the result of point@translate(4,5). The translate function produces a new object with both the x and y fields changed by the amount passed in. The @ operator made it so the self parameter was bound to the initial point.

What happens if the parameter isn't called self? What if the program used a dot before translate instead of an @?

Note that as with field access, we can also use obj@[expr]() to compute a string field name to access for message passing.