Chapter 2. Using Chez Scheme

Chez Scheme is often used interactively to support program development and debugging, yet it may also be used to create stand-alone applications with no interactive component. This chapter describes the various ways in which Chez Scheme is typically used and, more generally, how to get the most out of the system. The chapter is organized as follows. Sections 2.1 and 2.2 describe how one uses Chez Scheme interactively. Section 2.3 describes how to create and use compiled files. Section 2.5 covers command-line options used when invoking Chez Scheme. Section 2.6 covers support for writing and running Scheme scripts, including compiled scripts. Section 2.7 describes how one can customize the startup process, e.g., to alter or eliminate the command-line options, to preload Scheme or foreign code, or to run Chez Scheme as a subordinate program of another program. Section 2.8 describes how to build applications using Chez Scheme with Petite Chez Scheme for run-time support. Finally, Section 2.4 describes how to structure and compile an application to get the most efficient code possible out of the compiler.

Section 2.1. Interacting with Chez Scheme

Chez Scheme can be used interactively simply by running the executable image without arguments or by selecting the Chez Scheme icon from the application startup menu. Many experienced Scheme programmers prefer to use Chez Scheme via GNU Emacs, which provides a sophisticated editor while allowing the Scheme system to run in a subordinate window. Less experienced users may prefer the Scheme Widget Library (SWL), which provides an integrated development environment with a much simpler editor. While the discussion in this section is tailored specifically to interacting with the base Scheme system outside of SWL or Emacs, most of the discussion applies equally in all three environments.

When used interactively, Chez Scheme prompts the user with a right angle bracket ">" at the beginning of each input line. Any Scheme expression may be entered. The system evaluates the expression and prints the result. After printing the result, the system prompts again for more input.

Typically, a Scheme programmer creates a source file of Scheme forms using a text editor and loads the file into Chez Scheme to test them. The conventional filename extension for Chez Scheme source files is ".ss". A source file may be loaded during an interactive session by typing (load "filename"). Files to be loaded may also be named on the command line when the system is started. Any form that may be typed interactively may be placed in a file to be loaded.

You can exit the system by typing the end-of-file character or by using the procedure exit. Typing the end-of-file character is equivalent to (exit), (exit (void)), or (exit 0), each of which is considered a normal exit. Any other argument to exit is considered an abnormal exit and returns an error status to the invoking shell.

Interaction of the system with the user is performed by a Scheme program called a waiter, running in a program state called a café. The waiter is a read-evaluate-print loop, or REPL: it prompts for input, reads the input, evaluates the input, prints the result, and loops back for more.

Running programs may be interrupted by typing the interrupt character (typically Control-C). In response, the system enters a debug handler, which prompts for input with a "debug>" prompt. Several commands may be issued to the debug handler, including

"e"
or end-of-file to exit from the handler and continue,
"r"
to stop execution and reset to the current café,
"a"
to abort Chez Scheme,
"n"
to enter a new café (see below),
"i"
to inspect the current continuation,
"s"
to display statistics about the interrupted program, and
"?"
to display a list of these options.
While typing an input expression to the waiter, the interrupt character simply resets to the current café.

When an error occurs, the system prints an error message and resets. Typing (debug) after an error occurs places you into the debug handler, where you can inspect the current continuation (control stack) to help determine the cause of the problem.

It is possible to open up a chain of Chez Scheme cafés by invoking the new-cafe procedure with no arguments. Entering a new cafe is also one of the options when an interrupt occurs (see below). Each café has its own reset and exit handlers. Exiting from one café in the chain returns you to the next one back, and so on, until the entire chain closes and you leave the system altogether. Sometimes it is useful to interrupt a long computation by typing the interrupt character, enter a new café to execute something (perhaps to check a status variable set by the computation), and exit the café back to the old computation.

You can tell what level you are at by the number of angle brackets in the prompt, one for level one, two for level two, and so on. Three angle brackets in the prompt means you would have to exit from three cafés to leave Chez Scheme. If you wish to abort from Chez Scheme and you are several cafés deep, the procedure abort leaves the system directly.

Section 2.2. Expression Editor

When Chez Scheme is used interactively in a shell window, the waiter's "prompt and read" procedure employs an expression editor that permits entry and editing of single- and multiple-line expressions, automatically indents expressions as they are entered, and supports name-completion based on the identifiers defined in the interactive environment. The expression editor also maintains a history of expressions typed during and across sessions and supports tcsh-like history movement and search commands. Other editing commands include simple cursor movement via arrow keys, deletion of characters via backspace and delete, and movement, deletion, and other commands using mostly emacs key bindings.

The expression editor does not run if the TERM environment variable is not set, if the standard input or output files have been redirected, or if the --eedisable command-line option (Section 2.5) has been used. The history is saved across sessions, by default, in the file ".chezscheme_history" in the user's home directory. The --eehistory command-line option (Section 2.5) can be used to specify a different location for the history file or to disable the saving and restoring of the history file.

Keys for nearly all printing characters (letters, digits, and special characters) are "self inserting" by default. The open parenthesis, close parenthesis, open bracket, and close bracket keys are self inserting as well, but also cause the editor to "flash" to the matching delimiter, if any, and correct the close delimiter, e.g., if a close paren matches up with an open bracket.

Key bindings for other keys and key sequences initially recognized by the expression editor are given below, organized into groups by function. Some keys or key sequences serve more than one purpose depending upon context. For example, tab is used both for identifier completion and for indentation. Such bindings are shown in each applicable functional group.

Multiple-key sequences are displayed with hyphens between the keys of the sequences, but these hyphens should not be entered. When two or more key sequences perform the same operation, the sequences are shown separated by commas.

Detailed descriptions of the editing commands are given in Chapter 13, which also describes parameters that allow control over the expression editor, mechanisms for adding or changing key bindings, and mechanisms for creating new commands.

Newlines, acceptance, exiting, and redisplay:

enter, ^M accept balanced entry if used at end of entry;
else add a newline before the cursor and indent
^J accept entry unconditionally
^O insert newline after the cursor and indent
^D exit from the waiter if entry is empty;
else delete character under cursor
^Z suspend to shell if shell supports job control
^L redisplay entry
^L-^L clear screen and redisplay entry

Basic movement and deletion:

leftarrow, ^B move cursor left
rightarrow, ^F move cursor right
uparrow, ^P move cursor up; from top of unmodified entry,
move to preceding history entry.
downarrow, ^N move cursor down; from bottom of unmodified entry,
move to next history entry.
^D delete character under cursor if entry not empty;
else exit from the waiter.
delete, ^H delete character before cursor

Line movement and deletion:

home, ^A move cursor to beginning of line
end, ^E move cursor to end of line
^K, esc-^K delete to end of line or, if cursor is at the end
of a line, join with next line
^U delete contents of current line

When used on the first line of a multiline entry of which only the first line is displayed, i.e., immediately after history movement, ^U deletes the contents of the entire entry, like ^G (described below).

Expression movement and deletion:

esc-^F move cursor to next expression
esc-^B move cursor to preceding expression
esc-] move cursor to matching delimiter
^] flash cursor to matching delimiter
esc-^K delete next expression
esc-delete, esc-^H delete preceding expression

Entry movement and deletion:

esc-< move cursor to beginning of entry
esc-> move cursor to end of entry
^G delete current entry contents
^C delete current entry contents; reset to end of history

Indentation:

tab re-indent current line if identifier prefix not
just entered; else insert identifier completion
esc-tab re-indent current line unconditionally
esc-q, esc-Q, esc-^Q re-indent each line of entry

Identifier completion:

tab insert identifier completion if just entered
identifier prefix; else re-indent current line
tab-tab show possible identifier completions at end of
identifier just typed, else re-indent
^R insert next identifier completion

If at end of existing identifier, i.e., not one just typed, the first tab re-indents, the second tab inserts identifier completion, and the third shows possible completions.

History movement:

uparrow, ^P move to preceding entry if at top of unmodified
entry; else move up within entry
downarrow, ^N move to next entry if at bottom of unmodified
entry; else move down within entry
esc-uparrow, esc-^P move to preceding entry from unmodified entry
esc-downarrow, esc-^N move to next entry from unmodified entry
esc-p search backward through history for given prefix
esc-n search forward through history for given prefix
esc-P search backward through history for given string
esc-N search forward through history for given string

To search, enter a prefix or string followed by one of the search key sequences. Follow with additional search key sequences to search further backward or forward in the history. For example, enter "(define" followed by one or more esc-p key sequences to search backward for entries that are definitions, or "(define" followed by one or more esc-P key sequences for entries that contain definitions.

Word and page movement:

esc-f, esc-F move cursor to end of next word
esc-b, esc-B move cursor to start of preceding word
^X-[ move cursor up one screen page
^X-] move cursor down one screen page

Inserting saved text:

^Y insert most recently deleted text
^V insert contents of window selection/paste buffer

Mark operations:

^@, ^space, ^^ set mark to current cursor position
^X-^X move cursor to mark, leave mark at old cursor position
^W delete between current cursor position and mark

Command repetition:

esc-^U repeat next command four times
esc-^U-n repeat next command n times

Section 2.3. Creating and Using Compiled Files

Chez Scheme compiles source forms as it sees them to machine code before evaluating them. In order to speed loading of a large file or group of files, the files may be compiled with the output placed in separate object files via compile-file.

(compile-file "filename") compiles the forms in the file filename.ss and places the resulting object code in the file filename.so. Loading a pre-compiled file is essentially no different from loading the source file, except that loading is faster since compilation is already done.

If a file to be loaded from source or compiled and loaded from object code contains forms that are intended to affect the compilation of subsequent forms, however, some adjustments are necessary to make sure that these forms are evaluated at compile time. One way to do this is to remove the forms that affect compilation and place them into a separate file that is loaded prior to compilation. Another way is to use eval-when, which is discussed in detail in 11.3. It may also be that one file defines a set of syntactic abstractions or modules that must be present during the compilation of another file. In this case, the former file must be compiled first in the same session as the latter file, or "visited" via visit before the second file is compiled. visit is also described in 11.3.

For example, assume that the file frob.ss contains the following forms, with actual code in place of the dashes.

(case-sensitive #t)
(optimize-level 2)
(load "frob-helpers.ss")

(define parse-frob
  ---)

(define-syntax make-frob
  ---)

(define-syntax frob-case
  ---)

(define sort-frob
  ---)

(define process-frob
  ---)

Also, assume that make-frob and frob-case use the common helper parse-frob to parse their input at expansion time and that frob-helpers.ss defines some macro definitions that are used in the definitions of sort-frob and process-frob.

The calls to case-sensitive and optimize-level affect each of the subsequent forms when frob.ss is loaded from source, but since they won't be evaluated when the file is compiled, they will not have the desired affect when the file is compiled. The file frob-helpers.ss is also loaded before the rest of frob.ss when frob.ss is loaded from source, as desired, but not when frob.ss is compiled. Furthermore, when frob.ss is loaded from source, the procedure parse-frob is defined before it is needed for any uses of make-frob and frob-case in the definitions of sort-frob and process-frob, but it is not available until run time when frob.ss is compiled.

All of these problems can be solved by wrapping the first several forms in an eval-when as shown below.

(eval-when (compile load eval)
  (case-sensitive #t)
  (optimize-level 2)
  (load "frob-helpers.ss")

  (define parse-frob
    ---))

(define-syntax make-frob
  ---)

(define-syntax frob-case
  ---)

(define sort-frob
  ---)

(define process-frob
  ---)

The given eval-when form causes the forms in its body to be evaluated when the file is compiled and also when the compiled file is subsequently loaded. It also causes them to be evaluated if the file is ever loaded from source. It is possible to leave out one or more of the compile, load, and eval "situations" so that the forms are not evaluated when they are not needed. For example, you may not want to change case sensitivity or the optimization level when the file is loaded, so you can use the following instead.

(eval-when (compile eval)
  (case-sensitive #t)
  (optimize-level 2))

(eval-when (compile load eval)
  (load "frob-helpers.ss")

  (define parse-frob
    ---))

(define-syntax make-frob
  ---)

(define-syntax frob-case
  ---)

(define sort-frob
  ---)

(define process-frob
  ---)

Macro definitions are implicitly wrapped in an eval-when with situations compile, load, and eval, so there is no need to wrap them explicitly unless you wish to restrict when they are available, e.g., to avoid loading them when the compiled file is loaded if they are not needed at run time.

Another possibility is to move the first several forms into a separate file and load it before compiling frob.ss. In this case, they will all be evaluated only at compile time.

If the file frob-helpers.ss loaded by frob.ss is compiled, it may not be necessary to do a full load of the file both at compile time and run time. It may suffice to "visit" the file (load only compile-time information like macro and module interfaces) at compile time using visit instead of load and "revisit" the file (load only run-time information like variable definitions) at run time using revisit.

(eval-when (compile eval)
  (case-sensitive #t)
  (optimize-level 2))

(eval-when (eval) (load "frob-helpers.so"))
(eval-when (compile) (visit "frob-helpers.so"))
(eval-when (load) (revisit "frob-helpers.so"))

(eval-when (compile load eval)
  (define parse-frob
    ---))

(define-syntax make-frob
  ---)

(define-syntax frob-case
  ---)

(define sort-frob
  ---)

(define process-frob
  ---)

When compiling a file or set of files, it is often more convenient to use a shell command than to enter Chez Scheme interactively to perform the compilation. This is easily accomplished by "piping" in the command to compile the file as shown below.

echo '(compile-file "filename")' | scheme -q

The "-q" option suppresses the system's greeting messages for more compact output, which is especially useful when compiling numerous files. (Under Windows, the single quotes around the compile-file call should be omitted.)

When running in this "batch" mode, especially from within "make" files, it is often desirable to force the error handler to exit immediately to the shell with a nonzero exit status. This may be accomplished by setting the reset-handler to abort.

echo '(reset-handler abort) (compile-file "filename")' | scheme -q

One can also redefine the error-handler (see page 244) to achieve a similar affect while exercising more control over the format of the error messages that are produced.

Section 2.4. Optimization

To get the most out of the Chez Scheme compiler, it is necessary to give it a little bit of help. The most important assistance is to avoid the use of top-level bindings. Top-level bindings are convenient and appropriate during program development, since they simplify testing, redefinition, and tracing (Section 3.1) of individual procedures and syntactic forms. This convenience comes at a sizable price, however.

The compiler can propagate copies (of one variable to another or of a constant to a variable) and inline procedures bound to unexported, unassigned variables within a single top-level expression. For the procedures it does not inline, it can avoid constructing and passing unneeded closures, bypass argument-count checks, branch to the proper entry point in a case-lambda, and build rest arguments (more efficiently) on the caller side, where the length of the rest list is known at compile time. It can also discard the definitions of unreferenced variables, so there's no penalty for including a large library of routines, only a few of which are actually used.

It cannot do any of this with top-level variable bindings, since the top-level bindings can change at any time and new references to those bindings can be introduced at any time. This is true for primitives like car, cons, and + as well as for user-defined procedures.

Fortunately, it is easy to restructure a program to avoid top-level bindings. It is best to do this incrementally as an application is being developed by moving the more well-tested portions of the program out of the top level and into a "local" scope. For portable code, the best way to make bindings local rather than top level is to wrap them in a "let nil" expression.

(let ()
  (define a ---)
  (define b ---)
  (define c ---)
  (define d ---))

If one or more of the bindings must remain visible at top-level, they should be created via a dummy definition before the let and assigned the actual value within the let.

(define d #f)
(let ()
  (define a ---)
  (define b ---)
  (define c ---)
  (define local-d ---)
  (set! d local-d))

References to the value of d within the let should be replaced with references to local-d so that the only references to the top-level d are outside of this block of code.

Assuming there are no assignments to a, b, c, and d within the let, the compiler can now perform a variety of optimizations that are disabled when top-level bindings are used, like copy propagation and inlining.

Chez Scheme provides a module form that can be used to achieve the same results without the dummy definition and assignment.

(module (d)
  (define a ---)
  (define b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d))

Modules may be named and explicitly imported so that the exported bindings do not pollute the top-level namespace.

(module abcd (d)
  (define a ---)
  (define b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d))

(let () (import abcd) (d ---))

Either way, a major source of inefficiency has been eliminated. On the other hand, the fact that references to primitives like car, cons, and + are still top level may cost even more efficiency. Addressing this is also straightforward. All primitive bindings are available via the scheme module. When the base boot files are loaded, these bindings are copied into the interactive top-level when where they can be redefined, but the original bindings in the scheme module are immutable, i.e., cannot be redefined or assigned. These immutable bindings can be made visible by importing from the scheme module within the top-level let or module form.

(module abcd (d)
  (import scheme)
  (define a ---)
  (define b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d))

(let () (import abcd) (d ---))

Now, all references to primitives are known by the compiler to have their original values, which allows them to be open-coded (coded inline in machine code) or even folded to constant values at compile time.

The best possible code is obtained when the all of the code for the entire application is contained in a single top-level let or module form. It is often more convenient, however, to place portions of the application code in different files, especially for large applications. The solution is to use include rather than load to bring the code into the system. While load reads and executes code at run time, include reads the code at compile time and splices it into to the current lexical context as if it had been present in the source code. To be effective, of course, the include forms must be inside of a top-level let or module form.

(module (d)
  (import scheme)
  (include "file1.ss")
  (include "file2.ss")
  (include "file3.ss")
  (include "file4.ss")
  (include "file5.ss"))

Incidentally, (import-only scheme) may be used in place of (import scheme) to prevent access to the interactive top-level environment.

(module (d)
  (import-only scheme)
  (include "file1.ss")
  (include "file2.ss")
  (include "file3.ss")
  (include "file4.ss")
  (include "file5.ss"))

This causes the expander to report attempts to reference or assign unbound identifiers, i.e., any identifier not bound in the scheme module, bound in some other module imported within one of the files, or bound locally within one of the files. The generated code is the same regardless of the form of import used.

With an application structured in this manner, we have done most of what we can do to help the compiler, but there are still a few things we can do.

First, we can allow the compiler to generate "unsafe" code, i.e., allow the compiler to generate code in which the usual run-time type checks have been disabled. We do this by using the compiler's "optimize level 3." The following allows the compiler to generate unsafe code when the application is compiled but forces it to generate safe code when it is loaded from source.

(eval-when (compile) (optimize-level 3))

(module (d)
  (import scheme)
  (include "file1.ss")
  (include "file2.ss")
  (include "file3.ss")
  (include "file4.ss")
  (include "file5.ss"))

The default optimization level is 0. Another useful optimization level is 2, but its use is no longer recommended. Optimize level 2 is safe, like optimize level 0, but allows the compiler to assume that primitives like car always have their original value. While this is important, as described above, it is usually clearer and more convenient to use (import scheme) rather than set the optimization level. If you wish to get a quick boost without wrapping the program in a top-level let or module form and inserting (import scheme) where appropriate, however, setting the optimization level to 2 might still be useful. Optimize level 1 is presently identical to optimize level 0.

It may also be useful to experiment with some of the other compiler control parameters and also with the storage manager's run-time operation. The compiler-control parameters are described in Section 11.5, and the storage manager control parameters are described in Section 12.1.

Finally, it is often useful to "profile" your code to determine that parts of the code that are executed most frequently. While this will not help the system optimize your code, it can help you identify "hot spots" where you need to concentrate your own hand-optimization efforts. In these hot spots, consider using more efficient operators, like fixnum or flonum operators in place of generic arithmetic operators, and using explicit loops rather than nested combinations of linear list-processing operators like append, reverse, and map. These operators can make code more readable when used judiciously, but they can slow down time-critical code.

Section 11.6 describes how to use the compiler's support for automatic profiling. Be sure that profiling is not enabled when you compile your production code, since the code introduced into the generated code to perform the profiling significant to run-time overhead.

Section 2.5. Command-Line Options

Chez Scheme recognizes the following command-line options.

-b path, --boot path   load boot file
-c, --compact   toggle compaction flag
--eedisable   disable expression editor
--eehistory off|path expression-editor history file
-h path, --heap path   load heap file
-q, --quiet   suppress greeting and prompt
-s[npath, --saveheap[npath   save heap file
--script path   run as shell script
--verbose   trace boot/heap search process
--version   print version and exit
--help   print help and exit
--   pass through remaining args

Any remaining command-line arguments are treated as the names of files to be loaded before Chez Scheme begins interacting with the user.

The "-c", "-h", "-s", and "-verbose" options are described in detail below. The "-script" option is described in Section 2.6. The "-eedisable" and "-eehistory" options are described in Section 2.2. The others should be self-explanatory.

When Chez Scheme is run, it looks for one or more boot files and/or one or more heap files to load. Boot files contain the compiled Scheme code that implements most of the Scheme system, including the interpreter, compiler, and most libraries. Heap files contain prebuilt heap images, i.e., saved heap images into which the boot code has already been loaded. Heap and boot files may be specified explicitly on the command line via "-b" and "-h" options or implicitly. In the simplest case, no "-b" and "-h" options are given and the necessary heap or boot files are loaded automatically based on the name of the executable.

For example, if the executable name is "frob", the system looks for "frob.heap" in a set of standard directories, then for "frob.boot". It also looks for and loads any subordinate heaps or boot files required by "frob.heap" or "frob.boot".

Subordinate heap and boot files are also loaded automatically for the first boot file and any heap files explicitly specified via the command line. When boot and heap files are specified via the command line, all heap files must come before all boot files, and each file must be listed before those that depend upon it.

The "-verbose" option may be used to trace the heap and boot file searching process and must appear before any boot or heap arguments for which search tracing is desired.

Ordinarily, the search for heap and boot files is limited to a set of default installation directories, but this may be overridden by setting the environment variable SCHEMEHEAPDIRS. SCHEMEHEAPDIRS should be a colon-separated list of directories, listed in the order in which they should be searched. Within each directory, the two-character escape sequence "%v" is replaced by the current version, and the two-character escape sequence "%m" is replaced by the machine type. A percent followed by any other character is replaced by the second character; in particular, "%%" is replaced by "%", and "%:" is replaced by ":". If SCHEMEHEAPDIRS ends in a non-escaped colon, the default directories are searched after those in SCHEMEHEAPDIRS; otherwise, only those listed in SCHEMEHEAPDIRS are searched. Under Windows, semi-colons are used in place of colons.

Boot files consist of ordinary compiled code and are created by concatenating a boot header onto the compiled code for one or more source files. See Section 2.8 for instructions on how to create boot files.

A heap file is a snapshot image of a Scheme heap, where all Scheme code and data is stored. They have both advantages and disadvantages relative to boot files. A heap file generally loads faster than the corresponding boot file, since all of the forms have already been evaluated. On systems supporting memory-mapped files with copy-on-write semantics, such as most Unix systems, most of the the contents of a heap may be shared by multiple Scheme processes, whereas each Scheme process has its own copy of code and data loaded from a boot file. On the other hand, heap files are not relocatable, and some operating systems do not guarantee that the heap can be loaded where it needs to be loaded. If it cannot be loaded into the proper range of addresses, an error is signaled at system startup time and the system exits. This occurs particularly often under both Windows and MacOS X. Even with operating systems that are more consistent in the range of addresses provided to the Scheme process, heap files built on one machine often cannot be used on another machine, so they cannot generally be distributed in lieu of boot files and must be rebuilt on the target machine.

For these reasons, we recommend that boot files be used for distributed applications unless startup time and/or sharing among multiple Scheme processes is critical.

Heap files are created via the "-s" option. If the "-s" option is present and Chez Scheme exits normally as described in Section 2.1, it will save the heap in the specified file. The heap contains the entire state of the system, so that any procedures or other objects created during a session are retained.

% scheme -s heap
> (define square (lambda (x) (* x x)))
> (exit)

% scheme -h heap
> (square 3)
9

The heap level is determined by the heap-level argument n. A level zero heap contains the entire heap, a level one heap contains only those portions that differ from the corresponding level zero heap, and so on. If no level follows the "-s" option, the level defaults to the level of the highest level heap loaded or zero if no heaps have been loaded.

A level one heap may be created in a session in which a level zero heap has been loaded, and the level one heap can be loaded with an additional "-h" option:

% scheme -h heap -s heap1
> (define abs (lambda (x) (sqrt (square x))))
> (exit)

% scheme -h heap -h heap1
> (square 3)
9
> (abs -3)
3

If the special level "+" follows the "-s" option, the level is one level higher than the highest loaded heap or zero if no heaps have been loaded. Thus, the following three lines create level zero, one, one, and two heaps.

% echo | {InstallSchemeName} -b petite.boot -s heap.0
% echo | {InstallSchemeName} -h heap.0 -s+ heap.1
% echo | {InstallSchemeName} -h heap.1 -s heap.1
% echo | {InstallSchemeName} -h heap.1 -s+ heap.2

After any boot files are loaded, the heap is compacted and the remaining heap storage is made static and is no longer subject to collection. The heap is also compacted by default before a heap file is saved. This can consume significant time and additional memory for very large heaps. The "-c" option can be used to disable this latter compaction. Multiple "-c" options toggle heap compaction; it is thus possible to use a shell script that disables compaction by default while still allowing compaction if desired. The "-c" option has no effect if the "-s" option has not been specified.

Section 2.6. Scheme Shell Scripts

When the "-script" command-line option is present, the named file is treated as a Scheme shell script, and the command-line is made available via the parameter command-line. This is primarily useful on Unix-based systems, where the script file itself may be made executable. To support executable shell scripts, the system ignores the first line of a loaded script if it begins with #! followed by a space or forward slash. For example, assuming that the Chez Scheme executable has been installed as /usr/bin/scheme, the following script prints its command-line arguments.

#! /usr/bin/scheme --script
(for-each
  (lambda (x) (display x) (newline))
  (cdr (command-line)))

The following script implementation of the traditional Unix echo command.

#! /usr/bin/scheme --script
(let ([args (cdr (command-line))])
  (unless (null? args)
    (let-values ([(newline? args)
                  (if (equal? (car args) "-n")
                      (values #f (cdr args))
                      (values #t args))])
      (do ([args args (cdr args)] [sep "" " "])
          ((null? args))
        (printf "~a~a" sep (car args)))
      (when newline? (newline)))))

Scripts may be compiled using compile-script, which is like compile-file but differs in two ways: (1) it copies the leading #! line from the source-file script into the object file, and (2) it disables the default compression of the resulting file, which would otherwise prevent it from being recognized as a script file.

Section 2.7. Customization

Chez Scheme and Petite Chez Scheme are built from several subsystems, a "kernel" encapsulated in a shared library (dynamic link library) that contains operating-system interface and low-level storage management code, an executable that parses command-line arguments and calls into the kernel to initialize and run the system, a base boot file (petite.boot) that contains the bulk of the run-time library code, and an additional boot file (scheme.boot), for Chez Scheme only, that contains the compiler.

While the kernel and base boot file are essential to the operation of all programs, the executable may be replaced or even eliminated, and the compiler boot file need be loaded only if the compiler is actually used. In fact, the compiler is usually never loaded for distributed applications, since doing so would require each user to have a license to run Chez Scheme.

The kernel exports a set of entry points that are used to initialize the Scheme system, load boot or heap files, run an interactive Scheme session, run script files, and deinitialize the system. In the threaded versions of the system, the kernel also exports entry points for activating, deactivating, and destroying threads. These entry points may be used to create your own executable image that has different (or no) command-line options or to run Scheme as a subordinate program within another program, i.e., for use as an extension language.

These entry points are described in Section 4.6, along with other entry points for accessing and modifying Scheme data structures and calling Scheme procedures.

The tarball (tar.gz) distributions of Chez Scheme and Petite Chez Scheme include within the distribution directory a subdirectory called "custom." This file custom.c in this subdirectory contains the "main" routine for the distributed executable image; look at this file to gain an understanding of how the system startup entry points are used. The custom subdirectory also contains configuration code and make files that can be used to rebuild the executable and install the resulting system.

Section 2.8. Building and Distributing Applications

While Chez Scheme cannot be redistributed without fee, code compiled using Chez Scheme is freely redistributable. Compiled object files do not, however, contain the run-time library code that is invariably required to run an application, including the code for primitive procedures, code required to interact with the operating system, and the storage manager. Fortunately, this library code is also freely redistributable in the form of Petite Chez Scheme, which may be used and redistributed without license fee or royalty for any purpose, including for resale as part of a commercial product. For details, see the Petite Chez Scheme Software License Agreement, which is available in the distribution directory for Petite Chez Scheme at http://www.scheme.com.

Although useful as a stand-alone Scheme system, Petite Chez Scheme was conceived as a run-time system for compiled Chez Scheme applications. The remainder of this section describes how to create and distribute such applications. using Petite Chez Scheme. It begins with a discussion of the characteristics of Petite Chez Scheme and how it compares with Chez Scheme, then describes how to prepare application source code, how to build and run applications, and how to distribute them.

Petite Chez Scheme Characteristics.  Although interpreter-based, Petite Chez Scheme evaluates Scheme source code faster than might be expected. Some of the reasons for this are listed below.

Nevertheless, compiled code is still far more efficient for most applications. The difference between the speed of interpreted and compiled code varies significantly from one application to another, but can amount to a factor of ten or more.

Two additional limitations result from the fact that Petite Chez Scheme does not include the compiler. First, the interpreter invokes the compiler to process foreign-procedure and foreign-callable forms. These forms cannot be processed by the interpreter alone, so they cannot appear in source code to be processed by Petite Chez Scheme. Compiled versions of foreign-procedure and foreign-callable forms may, however, be included in compiled code loaded into Petite Chez Scheme. Second, since inspector information is attached to code objects generated only by the compiler, source information and variable names are not available for interpreted procedures or continuations into interpreted procedures. This makes the inspector less effective for debugging interpreted code than it is for debugging compiled code.

Except as noted above, Petite Chez Scheme does not restrict what programs can do, and like Chez Scheme, it places essentially no limits on the size of programs or the memory images they create.

Preparing Application Code.  While it is possible to distribute applications in source-code form, i.e., as a set of Scheme source files to be loaded into Petite Chez Scheme by the end user, distributing compiled code has two major advantages over distributing source code. First, compiled code is usually much more efficient, as discussed in the preceding section, and second, compiled code is in binary form and thus provides more protection for proprietary application code. For these reasons, we suggest that applications be compiled.

Application source code generally consists of a set of Scheme source files possibly augmented by foreign code developed specifically for the application and packaged in shared libraries (also known as shared objects or, on Windows, dynamic link libraries). The following assumes that any shared library source code has been converted into object form; how to do this varies by platform. (Some hints are given in Section 4.3.) The result is a set of one or more shared libraries that are loaded explicitly by the Scheme source code during program initialization.

Once the shared libraries have been created, the next step is to compile the Scheme source files into a set of Scheme object files. Doing so typically involves simply invoking compile-file on each source (".ss") file to produce the corresponding object (".so") file. This may be done within a build script or "make" file via a command line such as the following:

echo '(compile-file "filename")' | scheme

which produces the object file filename.so from the source file filename.ss.

If the application code has been developed interactively or is usually loaded directly from source, it may be necessary to make some adjustments to a file to be compiled if the file contains expressions or definitions that affect the compilation of subsequent forms in the file, as described in Section 2.3. You may also wish to disable generation of inspector information both to reduce the size of the compiled application code and to prevent others from having access to the expanded source code that is retained as part of the inspector information. To do so, set the parameter generate-inspector-information to false either prior to calling compile-file or at the top of each application source file, wrapped in an eval-when with situation compile as shown in Section 2.3. The downside of disabling inspector information is that the information will not be present if you need to debug your application, so it is usually desirable to disable inspector information only for production builds of your application.

Although it is possible to intersperse initialization expressions and definitions at the top level of a Scheme source file, we suggest that initialization expressions be encapsulated in one or more initialization procedures that are explicitly invoked when the application is created or run. Initialization procedures to be invoked when the application is run may be invoked by the Scheme startup procedure, which is described below.

The Scheme startup procedure determines what the system does when it is started. The default startup procedure loads the files listed on the command line (via load) and starts up a new café, like this.

(lambda fns (for-each load fns) (new-cafe))

The startup procedure may be changed via the parameter scheme-start. The following example demonstrates the installation of a variant of the default startup procedure that prints the name of each file before loading it.

(scheme-start
  (lambda fns
    (for-each
      (lambda (fn)
        (printf "loading ~a ..." fn)
        (load fn)
        (printf "~%"))
      fns)
    (new-cafe)))

Scripts (see Section 2.6) use a different startup procedure that is the value of the scheme-script parameter. The default value of this parameter is a procedure that sets the command-line and command-line-arguments parameters, then loads the script. command-line holds the script name and arguments, while command-line-arguments holds just the arguments.

(lambda (fn . fns)
  (command-line (cons fn fns))
  (command-line-arguments fns)
  (load fn))

A typical application startup procedure would first invoke the application's initialization procedure(s) and then start the application itself:

(scheme-start
  (lambda fns
    (initialize-application)
    (start-application fns)))

Any shared libraries that must be present during the running of an application must be loaded during initialization. In addition, all foreign procedure expressions must be executed after the shared libraries are loaded so that the addresses of foreign routines are available to be recorded with the resulting foreign procedures. The following demonstrates one way in which initialization might be accomplished for an application that links to a foreign procedure show_state in the Windows shared library state.dll:

(define show-state)

(define app-init
  (lambda ()
    (load-shared-object "state.dll")
    (set! show-state
      (foreign-procedure "show_state" (integer-32)
        integer-32))))
 
(scheme-start
  (lambda fns
    (app-init)
    (app-run fns)))

Building and Running the Application.  Building and running an application is straightforward once all shared libraries have been built and Scheme source files have been compiled to object code.

Although not strictly necessary, we suggest that you concatenate your Scheme object files, if you have more than one, into a single object file. This may be done on Unix systems simply via the "cat" program or on Windows via copy. Placing all of the object code into a single file simplifies both building and distribution of applications.

With the Scheme object code contained within a single composite object file, it is possible to run the application simply by loading the composite object file into Petite Chez Scheme, e.g.:

petite app.so

where app.so is the name of the composite object file, and invoking the startup procedure to restart the system:

> ((scheme-start))

It is usually preferable, however, to convert the composite object file into a boot file. Boot files are loaded during the process of building the initial heap. Because of this, boot files have the following advantages over ordinary object files.

A boot file is simply an object file, possibly a composite object file created as described above, prefixed by a boot header. The boot header identifies a base boot file upon which the application directly depends, or possibly two or more alternatives upon which the application can be run. In most cases, "petite.boot" will be identified as the base boot file, but in a layered application it may be another boot file of your creation that in turn depends upon "petite.boot." The base boot file, and its base boot file, if any, are loaded automatically when your application boot file is loaded.

Boot headers are created with make-boot-header. This procedure accepts two or more arguments. The first is the name of a file into which the header should be placed, and the remainder name one or more boot files on top of which the application can be run. For example, the call

(make-boot-header "app.hdr" "petite.boot")

creates a header file that identifies a dependency upon "petite.boot," while the call

(make-boot-header "app.hdr" "scheme.boot" "petite.boot")

creates a header file that identifies a dependency upon either "scheme.boot" or "petite.boot." In the former case, the system will automatically load petite.boot when the application boot file is loaded, and in the latter it will load scheme.boot if it can find it, otherwise petite.boot. This would allow your application to run on top of the full Chez Scheme if present, otherwise Petite Chez Scheme.

While Petite Chez Scheme is freely redistributable, Chez Scheme may be used only under direct license from Cadence Research Systems and may not be redistributed. In most cases, this means that you should construct your application so that it does not depend upon features of Chez Scheme and you should specify only "petite.boot" in the call to make-boot-header. If your application calls eval, however, and you wish to allow users who have licensed Chez Scheme to be able to take advantage of the faster execution speed of compiled code, then specifying both "scheme.boot" and "petite.boot" is appropriate.

The resulting header can be concatenated onto the front of the object file to form the application header, again using cat under Unix and copy under Windows.

Distributing the Application.  Distributing an application involves creating a distribution package that includes, at a minimum, the following items:

The application installation script should install Petite Chez Scheme if not already installed on the target system. It should install the application boot file in the same directory as the Petite Chez Scheme boot file "petite.boot" is installed, and it should should install the application shared libraries, if any, either in the same location or in a standard location for shared libraries on the target system. It should also create a link to or copy of the Petite Chez Scheme executable under the name of your application, i.e., the name given to your application boot file. Where appropriate, it should also install desktop and start-menu shortcuts to run the executable. A sample installation script for Unix platforms is given below. For Windows, we suggest the use of an installation building program, such as the open-source NullSoft Scriptable Install System (NSIS).

Sample Unix Installation Script.  The script below demonstrates how to perform a straightforward installation of a Scheme application on a Unix-based platform. The script makes the following assumptions, any of which may be changed by altering the script's application configuration parameters:

The script also sets the default location for executables to /usr/bin and shared libraries to /usr/lib. These settings would typically be open to change by the end user; a friendlier script would query the user to verify that these settings are appropriate.

The script first installs Petite Chez Scheme, then installs the boot file and shared libraries, then sets up the executable.

# installation directories
prefix=/usr
bin=${prefix}/bin
lib=${prefix}/lib

# Petite Chez Scheme version information
machine=i3le
release=7.0

# application configuration
app=app
libs=lib${app}.so
boot=${app}.boot

# install Petite Chez Scheme
tar -xzf csv${release}-${machine}.tar.gz
(cd csv${release}/custom; ./configure --installprefix=${prefix})
(cd csv${release}/custom; make install)

# install the boot file
cp ${boot} ${lib}/csv${release}/${machine}
chmod 444 

# install the shared libraries
cp ${libs} ${lib}
chmod 444 ${libs}

# create a link for the executable
ln -s ${bin}/petite ${bin}/${app}

R. Kent Dybvig / Chez Scheme Version 7 User's Guide
Copyright © 2005 R. Kent Dybvig
Revised July 2007 for Chez Scheme Version 7.4
Cadence Research Systems / www.scheme.com
Cover illustration © 1998 Jean-Pierre Hébert
ISBN: 0-9667139-1-5
to order this book / about this book