Copyright © 1998, 2001 by Ian Horswill, see comment within this file.
The GRL compiler is written for the Scheme48 system.
When you type at scheme, you're typing at the command processor. It prints a prompt, reads a command, runs it, and prints the response. As with C++, Java, or Common Lisp, Scheme48 allows you to structure your code into modules called packages. The Scheme prompt tells you what package you're in, and whether you're debugging an error:
| Prompt | Meaning |
|---|---|
> |
Everything's normal. You're entering code into the default package and there are no errors. |
1> |
You're in the normal package and you've generated an error. To
get out of the error, type ,pop or ,reset. |
2> |
You generated an error, and then while debugging that error, you
generated another error. Higher numbers mean higher levels of
nesting of errors. To return to the previous error, type ,pop,
to stop debugging entirely, type ,reset. |
packagename> |
Everything's normal, but you're entering code into the package named packagename.
To go back to the default package, type ,user. |
1 packagename> |
Some code residing in the package packagename generated an
error. Again, higher numbers mean nested errors. Type ,pop
to return to the previous error or ,reset to quit debugging
altogether. |
inspect: |
You're in the object browser (the "inspector"):
|
The command processor reads lines from the keyboard and runs them. Usually, the things you type are definitions to be compiled, or expressions to be evaluated. However, there are also a few special commands that get handled specially by the command processor. These start with a comma:
| Command | Meaning |
|---|---|
,help |
Prints a list of all the commands |
,inspect expression |
Evaluates expression and allows you to browse the slots of
whatever data structure it returns. Return to normal execution
using ,pop or ,reset. |
,debug |
Allows you to browse the stack. Return to normal execution using
,pop or ,reset. If you are just running
GRL code, you shouldn't have to concern yourself with this unless you're
doing something arcane. |
,preview |
Displays the scheme equivalent of the call stack. Stack frames in scheme are called "continuations" (for good reasons with which you need not concern yourself). The implementors of Scheme48 thought of this command as showing you what things were going to be run if the program kept running, rather than showing what things had been run in the past to get to this point. Hence the name "preview". If you are just running GRL code, you shouldn't have to concern yourself with this unless you're doing something arcane. |
,load filename |
Evaluates all the code in filename as if it had been typed into the scheme prompt by hand |
,pop |
Aborts debugging of the current error and returns to the previous error, if any |
,reset |
Aborts all debugging and returns to the top-level command interpreter |
,in package |
Evaluates all subsequent commands within the environment of package |
,in package command |
Evaluates command within the environment of package |
,exit |
Kills scheme |
To interrupt your scheme or GRL program, or to kill the GRL compiler if it
somehow gets into an infinite loop, type Control-C Control-C (from inside emacs)
or type whatever you type on your host operating system to interrupt a process
(usually Control-C). To get rid of scheme, type ,exit.
You can type GRL definitions directly at the scheme
prompt. However, you'll almost always want to enter your code into a .scm
file and load it from there into scheme. To begin entering a GRL program,
go into emacs and type Control-x f (find file) to load a file into
an Emacs window. Type the name of the file you want to edit and hit return.
Make sure the that file name ends with .scm so that Emacs
understands that it's code destined for the Scheme interpreter. If the
file doesn't exist, Emacs will create a blank buffer with than name. Now
enter a GRL definition like (define-signal test #f), which says
that test should be a constant-valued signal with the value
false. Make sure the cursor is either inside of this definition or after
the closing parenthesis, then type Control-Meta-x (i.e. hold down
control and alt, then press x). Emacs will transmit this
definition to scheme. If you continue entering definitions, you can
continue to load them until you are ready to try compiling some code (see next
section). You can also change an existing definition and reload it by
typing C-M-x again.
If you're starting work on a program you've previously entered, you won't
want to go through the file and type C-M-x on each definition. You
can load all the code in the file into scheme by typing ",load filename"
(the comma is not a misprint) at the scheme prompt. Alternatively, you can
type Control-c l inside Emacs. For simple programs, this is
the simplest way to start up. For more complicated programs, you'll want
to use the Scheme48 package system to create a set of packages and load them as
a unit.
To compile your GRL program, type (compile signal-name
...) at the scheme prompt. This will compile the signals you
specify, along with all their inputs, their inputs' inputs, etc. The most
common structure for a GRL program is something like:
(define-signal sensor1 ... read the sensor ...) (define-signal sensor2 ... read the sensor ...) ... (define-signal sensorn ... read the sensor ...) (define-signal behavior1 (behavior activation-level-code motor-vector-code)) (define-signal behavior2 (behavior activation-level-code motor-vector-code)) ... (define-signal behaviorn (behavior activation-level-code motor-vector-code)) (define-signal base-controller (drive-base (behavior-or behavior1 behavior2 ... behaviorn)))
It has a set of interfaces to the sensors at the top, some behaviors that
compute commands and activation levels from the sensor signals, and then a call
to some interface transducer (drive-base in this example) to
transmit the final motor command to the base. To compile this program, you
would only need to type (compile base-controller), since the
behaviors are inputs to base-controller and the sensor signals are
inputs to the behaviors. If you change the program so that it doesn't use
one of the sensor signals, the compiler will automatically omit its code from
the compiled image.
Caveat: If you try to type (compile (drive-base (behavior-or
behavior1 behavior2))) rather than (compile base-controller)
you will get an error. The reason is that drive-base and behavior-or
are not scheme procedures and so the scheme interpreter doesn't know how
to process calls to them. They are data structures representing a
transducer and a signal procedure, respectively. The reason you can
"call" them in the definitions above is that define-signal
is really a macro that transforms the "calls" into scheme code that
creates data structures to represent the new signals you're defining. To a
first approximation, you can pretend that you can only call transducers and
signal procedures inside of define-signal and you can only pass raw
names like base-controller to compile.
The compile procedure compiles signals to scheme code and prints
it on the screen. This isn't very useful for most users, so you'll
generally want to use some procedure other than compile. For
example, compile-to-c in package c-generator (type ,open
c-generator to access it) will compile the signals to C code and display
it on the screen, while compile-to-c-file will compile it to C code
and write it to a file. You can use try-signals in package scheme48-run-time
(type ,open scheme48-run-time to access it) to compile signals to
scheme code and run them immediately within Scheme48 itself. This can be
useful to hand-test transducers from the keyboard. If your robot has
enough of a computer to run Scheme48 itself, then you can also have Scheme48
drive the robot directly and and never have to cross-compile anything.
Packages (also called structures) are the equivalent of the namespaces of C++ or Java or the packages of Common Lisp. They're lexical environments that can import and export bindings. A given package is defined by the set of packages it imports from, the set of bindings it exports, and the set of files that contain its definitions. The GRL distribution is divided into the following packages. To access a package, type ",open package-names ...".
At any given time, the Scheme48 command processor is operating "inside" some specific package. That is, it will attempt to resolve any variable names you reference by checking for bindings in that package and in the export lists of those packages it imports from. You can change the package you're in by typing:
,in package-name
at which point, the command processor's prompt will change to "package-name>".
You can also type ",in package-name command"
to just run command in package-name and then return to the package
you came from.
You can add a package to the import list of your package by saying ",open
package-names ...".
Packages are bound to names, so of course there has to be a package that
contains those bindings. It's called the configuration package or just
"config" for short. Config doesn't have a binding
for itself, so you can't get at it with the ,in command. To
evaluate something in the config package, you say ",config command"
instead.
To create a new package, you use the define-structure macro,
which is available inside the config package:
(define-structure package-name export-list (open package-names ...) (files source-file-names ...))
For example:
(define-structure my-package (export my-proc my-signal) (open scheme grl-language grl-library) (files my-code))
This says that my-package is a new package that opens scheme,
grl-language, and grl-library, exports the procedure my-proc
and the signal my-signal, and whose code is found in my-code.scm
in the current directory. Notice that:
export
procedure.Important: if you reevaluate the define-structure macro,
the command processor will actually create a new package and bind it to
the package name you provided, blowing away your only pointer to the old version
of the package. So you almost never want to evaluate define-structure more
than once per session. If you want to load more code into the package, you
can just say ",load filenames ...". If you
want to import something new, you can just use the ,open
command. Adding exports is more tricky, however. What you need to do
is to not use the export procedure directly. Instead, you
change the above definition to:
(define-interface my-package-interface (export my-proc my-signal) (define-structure my-package my-package-interface (open scheme grl-language grl-library) (files my-code))
the define-interface macro is written so that when you
reevaluate it, it mutates the old interface object rather than creating a new
one, thus updating your package without having to create a new package.
In practice, no one ever types code into the config package directly.
Instead, you typically create a separate file to hold your package and interface
declarations, typically called packages.scm and placed in the same
directory as the code for those packages. You can then just say:
,config ,load packages.scm
which means: "go to the config package, then compile and run all the
commands in packages.scm, then return to the package I'm already
in."
The package you start up in is called the "user package".
Like the config package, it doesn't have a binding in the config package.
To get back to it after you leave it, you have to use the ,user
command.
Scheme48 includes a simple object inspector that you can use to look at the
data structures used by GRL to represent your signals, or to inspect Scheme's
call stack (see below). You can enter the inspector by typing ,debug
(to inspect the stack) or ",inspect expression" to
inspect the value of an arbitrary scheme expression. The inspector then
prints the fields of the object along with numbers:
> ,inspect default-polly-pipeline
'#{Compound-signal (139) default-polly-pipeline}
[0: source-code] '((polly-pipeline # 15 # 30 ---))
[1: name] 'default-polly-pipeline
[2: inputs] '(#{Constant-signal # side-width-percent} #{Constant-signal # 15} #{Constant-signal # center-width-percent} #{Constant-signal # 30} #{Constant-signal # edge-threshold} ---)
[3: operator] '#{Signal-procedure polly-pipeline}
[4: other-required-signals] '()
[5: type] #f
[6: declared-type] #f
[7: initial-value] '#{Unspecific}
[8: location] '#{Compound-signal (827) default-polly-pipeline}
[9: parent] #f
[10: part-of-expansion-of] #f
[11: plist] '()
[12: force-expression?] #f
[13: force-inline?] #f
[14: gatherer] #f
[m] more...
inspect:
(Note that the prompt changes to "inspect:").
This shows that default-polly-pipeline evaluated to the object #{Compound-signal (139)
default-polly-pipeline}. It's one of the objects used by the
compiler to represent signals. It has a bunch of fields: source code,
name, inputs, etc. You can get a better look at the values of those fields
by typing the number of the field. For example, to see the inputs of the
signal, type 2:
inspect: 2
'(#{Constant-signal # side-width-percent} #{Constant-signal # 15} #{Constant-signal # center-width-percent} #{Constant-signal # 30} #{Constant-signal # edge-threshold} ---)
[0] '#{Constant-signal (133) side-width-percent}
[1] '#{Constant-signal (134) 15}
[2] '#{Constant-signal (135) center-width-percent}
[3] '#{Constant-signal (136) 30}
[4] '#{Constant-signal (137) edge-threshold}
[5] '#{Constant-signal (138) 25}
inspect:
This says that the value of the inputs field is a list of six elements.
To see the first input to the signal, side-width-percent, type 0:
inspect: 0
'#{Constant-signal (133) side-width-percent}
[0: source-code] '('side-width-percent (polly-pipeline # 15 # 30 ---))
[1: name] "constant"
[2: inputs] '(side-width-percent)
[3: operator] '#{Primitive identity}
[4: other-required-signals] '()
[5: type] #f
[6: declared-type] #f
[7: initial-value] '#{Unspecific}
[8: location] '#{Unspecific}
[9: parent] #f
[10: part-of-expansion-of] #f
[11: plist] '()
[12: force-expression?] #f
[13: force-inline?] #f
[14: gatherer] #f
[m] more...
inspect:
To go back to the list, type "u" for "up":
inspect: u
'(#{Constant-signal # side-width-percent} #{Constant-signal # 15} #{Constant-signal # center-width-percent} #{Constant-signal # 30} #{Constant-signal # edge-threshold} ---)
[0] '#{Constant-signal (133) side-width-percent}
[1] '#{Constant-signal (134) 15}
[2] '#{Constant-signal (135) center-width-percent}
[3] '#{Constant-signal (136) 30}
[4] '#{Constant-signal (137) edge-threshold}
[5] '#{Constant-signal (138) 25}
inspect:
Typing "u" again, brings you back to default-polly-pipeline.
inspect: u
'#{Compound-signal (139) default-polly-pipeline}
[0: source-code] '((polly-pipeline # 15 # 30 ---))
[1: name] 'default-polly-pipeline
[2: inputs] '(#{Constant-signal # side-width-percent} #{Constant-signal # 15} #{Constant-signal # center-width-percent} #{Constant-signal # 30} #{Constant-signal # edge-threshold} ---)
[3: operator] '#{Signal-procedure polly-pipeline}
[4: other-required-signals] '()
[5: type] #f
[6: declared-type] #f
[7: initial-value] '#{Unspecific}
[8: location] '#{Compound-signal (827) default-polly-pipeline}
[9: parent] #f
[10: part-of-expansion-of] #f
[11: plist] '()
[12: force-expression?] #f
[13: force-inline?] #f
[14: gatherer] #f
[m] more...
inspect:
Notice the the operator (the procedure being applied to the inputs) is a
signal procedure, polly-pipeline. That means that at
compile-time the compiler calls that signal procedure with the input signals as
arguments, that signal expanded it into some other network of signals and
returned it to the compiler. The compiler then tried to compile that,
until it expanded it into a network of signals that didn't have any signal
procedures. When it expands a signal, the compiler stored the expanded
version into the location field of the object, which here is field
number 8:
inspect: 8
'#{Compound-signal (827) default-polly-pipeline}
[0: source-code] '((make-polly-internal input smoothed-image dangerous-pixels depth-map ---) (let* # #) (#) (#))
[1: name] 'default-polly-pipeline
[2: inputs] '(#{Transducer-signal # "default-polly-pipeline-input"} #{Compound-signal # smoothed-image} #{Transducer-signal # dangerous-pixels} #{Transducer-signal # depth-map} #{Constant-signal # 112} ---)
[3: operator] '#{Signal-procedure make-polly-internal}
[4: other-required-signals] '()
[5: type] #f
[6: declared-type] #f
[7: initial-value] '#{Unspecific}
[8: location] '#{Signal-group (1249) default-polly-pipeline}
[9: parent] '#{Compound-signal (139) default-polly-pipeline}
[10: part-of-expansion-of] '#{Compound-signal (139) default-polly-pipeline}
[11: plist] '()
[12: force-expression?] #f
[13: force-inline?] #f
[14: gatherer] #f
[m] more...
inspect: 8
'#{Signal-group (1249) default-polly-pipeline}
[0: source-code] #f
[1: name] 'default-polly-pipeline
[2: inputs] '(#{Transducer-signal # "default-polly-pipeline-input"} #{Transducer-signal # "default-polly-pipeline-smoothed-image"} #{Transducer-signal # dangerous-pixels} #{Transducer-signal # depth-map} #{Constant-signal # 112} ---)
[3: operator] '(group input smoothed-image dangerous-pixels depth-map ---)
[4: other-required-signals] '()
[5: type] '(group input smoothed-image dangerous-pixels depth-map ---)
[6: declared-type] #f
[7: initial-value] '#{Unspecific}
[8: location] #f
[9: parent] '#{Compound-signal (827) default-polly-pipeline}
[10: part-of-expansion-of] '#{Compound-signal (827) default-polly-pipeline}
[11: plist] '()
[12: force-expression?] #f
[13: force-inline?] #f
[14: gatherer] #f
[m] more...
inspect:
So this shows us that default-polly-pipeline, a signal whose
operator was polly-pipeline, was expanded into a signal whose
operator was make-polly-internal, and that expanded into a signal
group with fields named input, smoothed-image, dangerous-pixels,
etc. To see the components themselves, we could then inspect the inputs of
this signal, if we wanted to.
When you trigger an error, Scheme goes into the debugger. The debugger
is just the normal command processor, except that it puts a number at the
beginning of the prompt, for example: "1 >".
If you trigger another error while debugging the first one, the number
increments. To give up on debugging an error, type ",pop".
That will bring you to the error before it. To give up debugging entirely,
type ",reset". At NWU, we have bound ,pop
and ,reset to the F3 and F2 keys,
respectively.
When you're in the debugger, you can print a stack trace by typing ,preview
(bound to F11 at NWU). You can inspect individual stack
frames by typing ,debug (F12). This places you
in the scheme48 object inspector:
Here's an example debugging session. Just to get started, we'll define
a function to debug, foo:
> (define (foo a b)
(+ a (/ 1 (- b 7))))
>
Now let's make it break:
> (foo 3 7)
Error: rational division by zero
1
1>
This tells us that we divided by zero and that the thing we tried to divide by zero was 1. So let's find out what called the division procedure:
1> ,preview integer/ in ratnums / in scheme-level-0 foo evaluate-and-select in command-processor loop thread-start in thread-top-level in threads 1>
This says the the procedure that died was integer/ in the
package ratnums (don't worry about packages if you don't know what
they are). It was called by the procedure /, which was called by foo,
which was called by a procedure evaluate-and-select which was part
of scheme48's command processor. Now let's look at the arguments.
1> ,debug
'#{Exception-continuation (pc 69) (integer/ in ratnums)}
[0] 1
[1] 0
inspect:
Now we're inspecting scheme48's stack.
inspect: d
'#{Exception-continuation (pc 15) (/ in scheme-level-0)}
inspect: d
'#{Continuation (pc 38) foo}
Waiting for (/ 1 (- b 7))
in (+ a ^^^)
[0] 3
[1: a] 3
[2: b] 7
inspect: ,reset
Top level
>