Scripting and Domain-specific Languages

In this assignment, you will use Scheme macros to implement a domain-specific programming language for controlling robots. You will write scripts that read from the robot's sensors (such as a camera and touch-sensitive bumpers) and send commands to the motors. This is the brain of the robot, and so we call the language "Brainscript".

Perhaps infamously, robots never die. Thus, the basic building block in Brainscript is the infinite loop, and Brainscript has special operators for combining infinite loops into larger loops.

Note: For this assignment, you have to use the language PLAI - Pretty Big, otherwise SYNTAX-RULES and SYNTAX-CASE are not defined. You will work in your third team for this.

The support code is in three files: robot.ss, robot-sim.ss, and default-robot.ss.

The Brainscript Language

What does Brainscript look like? The following script plays a little game of soccer. It runs on a robot that can move around, detect the ball and kick the ball (if it is close enough).

(brain
 (ball-distance ball-direction)
 (go-forward go-left go-right kick)
 (do 
     (while (> (ball-distance) 10)
	    (cond-flip [(eq? (ball-direction) 'forward) (go-forward)] 
		       [(eq? (ball-direction) 'left) (go-left)]  
		       [else (go-right)]))
     (timed 1 (kick)))

Brainscript programs begin with a call to the macro brain, followed by two lists of identifiers, followed by one or more body expressions. Each name in the first list is bound to a sensor predicate of type void -> any. In this case, ball-distance returns an integer representing the distance between the robot and the ball (in centimeters), and ball-direction returns one of 'left, 'forward or 'right, depending on the position of the ball relative to the robot.

In the call to brain, each identifier in the second list is bound to an action loop. Actions loops are procedures of no arguments that, when called, take control of the robot's motors. Action loops are infinite loops: first they refresh the sensor sensor cache by using the robot's low-level API, second they set the robot's motors positions, then they repeat -- ad infinitum.

The sensor cache make sure that during a particular iteration of the brain all sensor readings are consistent with one another. Without the sensor cache, the robot would read its sensors at slightly different times and could end up with a self-contradicting view of its environment.

Action loops are useful in robots that require complex and careful motions. For example, in a walking robots, the go-forward action loop maintains the balance of the robot by updating the force of the motors every milliseconds.

However, infinite loops are not as much fun when you get stuck in one. For this, Brainscript has a number of control operator that break out of nested action loops. The body of the soccer-player script uses four of the five control operators that you will implement: do, while, cond-flip and timed (the fifth one is flip). The do statement creates an infinite loop: it execute each statement of its body in order, until the last, then restarts with the first one. In contrast, the four other operators have a guard condition that breaks out of infinite loops. At first, while, flip, cond-flip and timed act just like do: they execute each statement of their body in order, repeating when necessary. However, if their guard expression ever becomes false, they will break the current action loop, no matter how deeply nested, and escape out.

Let's see how this works with an example statement. The statement

(timed 1 (go-forward))
runs the go-forward action loop for one second then escapes out of timed expression. To do this, the Brainscript support code keeps a single global stack of active guard thunks, each with a corresponding escape thunk. (A thunk is a function of no arguments.) The guard of this timed expression returns true at first, but if it is called after one second has passed since execution entered the timed statement, it returns false. The corresponding escape thunk simply exits the timed statement.

When the execution enters the timed statement, your Brainscript implementation will push this guard-thunk/escape-thunk pair onto the stack, execute its body, and, when execution exits the body (that is, when the escape continuation is invoked), pop the pair from the stack. The action loops are responsible for calling the guard functions and invoking the escape thunks. That is, the go-forward action loop (and all other action loops for the matter) execute three steps, in cycle:

  1. Update sensors.
  2. Check each guard on the stack, from oldest to newest, and invoke the continuation of first guard that returns false.
  3. If all the guards return true, adjust the motors to move the robot forward, then jump to step 1.

The other action loops work similarly.

The right way to implement escape thunks is with continuations. Since Brainscript statements do not have return values, the escape functions are thunks. Your escape thunks can simply invoke continuations with (void) as the argument to signify no useful return value. Remember that you can capture the current continuation with Scheme's let/cc statement. Look in the lecture notes on continuations for more details.

Formal definition

Formally, the Brainscript language has the following grammar:

<brain> ::= (brain (<id> ...) (<id> ...) <brainscript> ...)  

<brainscript> ::= (do <brainscript> ...)
                | (while <scheme> <brainscript> ...)
                | (flip <scheme> <brainscript> <brainscript>)
                | (cond-flip [<scheme> <brainscript>] ...)    
                | (timed <scheme> <brainscript> ...)  
                | <scheme> 

The grammar forbids the usage of the brain macro from within a brain. Also, only standard Scheme expressions are legal in the guard position of a Brainscript statement -- Brainscript statements are not allowed as guards. This will simplify your implementation somewhat, since it guarantees that there is only one brain active at any given time. As usual, your implementation does not have to do error detection. All the programs we will test with will respect the Brainscript grammar.

You have to implement the following six macros. It will pay off greatly if you think ahead and write some macros in terms of others. For example, you only need to write infinite-loop code once if your other macros expand to code that contains a do block. Only implementing what you need to is part of this assignment. Don't copy your code!

The Low-level Robot API and Support Code

The support code we give you implements the global stack required for Brainscript evaluation model. To load the support code, write the following line at the top of your code:

(require "robot.ss") 

This will define the following data-type and procedures:

Debugging macros is notoriously difficult, since you usually do not see the result of the expansion. The support code give you two procedures that expose the result of the macro expansion. They will help find bugs in your macro expanders.

  1. view-macro-result : sexp -> sexp -- consumes a sexp and produces a sexp with all the macros invocations completely expanded. Note that many Scheme primitives are themselves macros. For example, and, or, and cond are macros which expand into instances ofif and let (as we have seen in class.) The return value of view-macro-result will reflect this, and you will find the expanded form of and, or, and cond in the result of view-macro-result.
  2. watch-macro-expansion : sexp -> (listof sexp) -- consumes a sexp and produces a list containing each intermediate result of the macro expension.

The Robots

We provide you with two different robot to test your brain macros againts. The first one does not do anything interesting beside exercising your brain macros. The second one is a physical simulation of a 2d robot that play with a bouncing ball.

The default robot
The default robot has two sensors (ball-distance and ball-direction) and four actions (go-forward, go-left, go-right and kick). The default robot responds to commands according to simple deterministic rules.

The details the rules are not important, what matters is that they are simple and deterministic. It will make your first round of debugging easier. You should test the soccer player script sample (at the top of this page), with your Brainscript macros, against the default robot. When the sample script runs against the default robot, it produces the following output:

      ((ball-distance 30) (ball-direction forward)) : sending go-forward
      ((ball-distance 29) (ball-direction forward)) : sending go-forward
      ((ball-distance 28) (ball-direction left)) : sending go-left
      ((ball-distance 28) (ball-direction forward)) : sending go-forward
      ((ball-distance 27) (ball-direction forward)) : sending go-forward
      ((ball-distance 26) (ball-direction forward)) : sending go-forward
      ((ball-distance 25) (ball-direction right)) : sending go-right
      ((ball-distance 25) (ball-direction forward)) : sending go-forward
      ((ball-distance 24) (ball-direction forward)) : sending go-forward
      ((ball-distance 23) (ball-direction left)) : sending go-left
      ((ball-distance 23) (ball-direction forward)) : sending go-forward
      ((ball-distance 22) (ball-direction forward)) : sending go-forward
      ((ball-distance 21) (ball-direction forward)) : sending go-forward
      ((ball-distance 20) (ball-direction right)) : sending go-right
      ((ball-distance 20) (ball-direction forward)) : sending go-forward
      ((ball-distance 19) (ball-direction forward)) : sending go-forward
      ((ball-distance 18) (ball-direction left)) : sending go-left
      ((ball-distance 18) (ball-direction forward)) : sending go-forward
      ((ball-distance 17) (ball-direction forward)) : sending go-forward
      ((ball-distance 16) (ball-direction forward)) : sending go-forward
      ((ball-distance 15) (ball-direction right)) : sending go-right
      ((ball-distance 15) (ball-direction forward)) : sending go-forward
      ((ball-distance 14) (ball-direction forward)) : sending go-forward
      ((ball-distance 13) (ball-direction left)) : sending go-left
      ((ball-distance 13) (ball-direction forward)) : sending go-forward
      ((ball-distance 12) (ball-direction forward)) : sending go-forward
      ((ball-distance 11) (ball-direction forward)) : sending go-forward
      ((ball-distance 10) (ball-direction right)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending kick
      ((ball-distance 110) (ball-direction forward)) : sending go-forward
      ((ball-distance 109) (ball-direction forward)) : sending go-forward

And so on.

You can install the default robot by evaluating the expression:

(set-current-robot! (make-default-robot))

The simulated soccer robot

The simulated robot has three sensors: ball-in-sight, ball-distance and ball-direction; and five actions: go-forward, go-backward, go-left, go-right and kick.

You can install a simulated robot by evaluating the expressions:

       (require "robot-sim.ss")
       (set-current-robot! (make-frtime-robot))

When you install the simulated robot, a window will open with a display of the simulated robot.

You can control this robot both via the keyboard and via your Brainscript implementation. On the keyboard, the arrow keys make the robot move, the shift key stops it, and the control key activates the kicking device. The goal of the game is to kick the ball with the kicker many times.

Of course, it is more fun if the robot kicks the ball autonomously, via Brainscript. In Brainscript, (do-action-loop 'go-forward) makes the robot go forward (similarly for 'go-backward, 'go-left and 'go-right). (do-action-loop 'kick) runs the kicker device. With the simulated robot, (read-sensor-cache 'ball-in-sight) will return either true if the ball is within the range of vision of the robot, otherwise it will return false. When the ball is not in sight, (read-sensor-cache 'ball-direction) returns 'out-of-sight. In that respect, the game is harder from Brainscript's perceptive, since you do not always know where the ball is.

FAQ

  1. If an infinite loop does not contain a robot action, Brainscript never breaks out of the loop. This is because, if there are no robot actions, nothing checks if the guards have become false. That's ok. This is how Brainscript is supposed to work.
  2. cond-flip breaks out if any one of the guards before the current branch switches from false to true. The invariant is that we execute the branch of the first guard that returned true during the latest stack check.

Changes

Sat Dec 4 2004 : Wed Dec 1 2004 :