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, 2.2, and 2.3 describe how one uses Chez Scheme interactively. Section 2.4 describes how to create and use compiled files. Section 2.6 covers command-line options used when invoking Chez Scheme. Section 2.7 covers support for writing and running Scheme scripts, including compiled scripts. Section 2.9 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.10 describes how to build applications using Chez Scheme with Petite Chez Scheme for run-time support. Finally, Section 2.5 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 definition or expression may be entered. The system evaluates the definition or expression and prints its values, if any. After printing the values, 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 "path"). 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. Passing 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 break> prompt. One of several commands may be issued to the break handler (followed by a newline), 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 exception other than a warning occurs, the default exception handler prints an error message and resets. Typing (debug) after an exception occurs places you into a debug handler. Alternatively, you can supply the --debug-on-exception command-line option to force the default exception handler to invoke debug directly. This is particularly useful when debugging scripts or top-level programs run via the --script or --program options. The debug handler is similar to the break handler and allows you to inspect the continuation (control stack) of the exception 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, or when new-cafe is invoked explicitly from a top-level program or script run via --program or --script, 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.6) 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.6) 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. Furthermore, when a close parenthesis or close bracket is typed, it is automatically corrected to match the corresponding open delimiter, if any.

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 14, 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
backspace, ^H delete character before cursor
delete delete character under 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, esc-delete delete next expression
esc-backspace, 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. The Interaction Environment

In the language of the Revised6 Report, code is structured into libraries and "RNRS top-level programs." The Revised6 Report does not require an implementation to support interactive use, and it does not specify how an interactive top level should operate, leaving such details up to the implementation.

In Chez Scheme, when one enters definitions or expressions at the prompt or loads them from a file, they operate on an interaction environment, which is a mutable environment that initially holds bindings for built-in keywords and primitives. It may be augmented by user-defined identifier bindings via top-level definitions. The interaction environment is sometimes referred to as a top-level environment, because it is at the top level for purposes of scoping. Programs entered at the prompt or loaded from a file via load should not be confused with RNRS top-level programs, which are actually more similar to libraries in their behavior. In particular, while the same identifier can be defined multiple times in the interaction environment, to support incremental program development, an identifier can be defined at most once in an RNRS top-level program.

The default interaction environment used for any code that occurs outside of an RNRS top-level program or library (including such code typed at a prompt or loaded from a file) contains all of the bindings of the (chezscheme) library (or scheme module, which exports the same set of bindings). This set contains a number of bindings that are not in the RNRS libraries. It also contains a number of bindings that extend the RNRS counterparts in some way and are thus not strictly compatible with the RNRS bindings for the same identifiers. To replace these with bindings strictly compatible with RNRS, simply import the rnrs libraries into the interaction environment by typing the following into the REPL or loading it from a file:

(import
  (rnrs)
  (rnrs eval)
  (rnrs mutable-pairs)
  (rnrs mutable-strings)
  (rnrs r5rs))

To obtain an interaction environment that contains all and only RNRS bindings, use the following.

(interaction-environment
  (copy-environment
    (environment
      '(rnrs)
      '(rnrs eval) 
      '(rnrs mutable-pairs) 
      '(rnrs mutable-strings) 
      '(rnrs r5rs))
    #t))

To be useful for most purposes, library and import should probably also be included, from the (chezscheme) library.

(interaction-environment
  (copy-environment
    (environment
      '(rnrs)
      '(rnrs eval) 
      '(rnrs mutable-pairs) 
      '(rnrs mutable-strings) 
      '(rnrs r5rs)
      '(only (chezscheme) library import))
    #t))

It might also be useful to include debug in the set of identifiers imported from (chezscheme) to allow the debugger to be entered after an exception is raised.

Most of the identifiers bound in the default interaction environment that are not strictly compatible with the Revised6 Report are variables bound to procedures with extended interfaces, i.e., optional arguments or extended argument domains. The others are keywords bound to transformers that extend the Revised6 Report syntax in some way. This should not be a problem except for programs that count on exceptions being raised in cases that coincide with the extensions. For example, if a program passes the = procedure a single numeric argument and expects an exception to be raised, it will fail in the initial interaction environment because = returns #t when passed a single numeric argument.

Within the default interaction environment and those created as described above, variables that name built-in procedures are read-only, i.e., cannot be assigned, since they resolve to the read-only bindings exported from the (chezscheme) library or some other library:

(set! cons +) <graphic> exception: cons is immutable

Before assigning a variable bound to the name of a built-in procedure, the programmer must first define the variable. For example,

(define cons-count 0)
(define original-cons cons)
(define cons
  (lambda (x y)
    (set! cons-count (+ cons-count 1))
    (original-cons x y)))

redefines cons to count the number of times it is called, and

(set! cons original-cons)

assigns cons to its original value. Once a variable has been defined in the interaction environment using define, a subsequent definition of the same variable is equivalent to a set!, so

(define cons original-cons)

has the same effect as the set! above. The expression

(import (only (chezscheme) cons))

also binds cons to its original value. It also returns it to its original read-only state.

The simpler redefinition

(define cons (let () (import scheme) cons))

turns cons into a mutable variable with the same value as it originally had. This same effect can be had in a wholesale manner for all variables in the interaction environment by calling the revert-interaction-semantics without arguments or passing the --revert-interaction-semantics option on the command line. Doing so, however, prevents the compiler from generating efficient code for primitive calls or producing warning messages for improper use of primitives.

All identifiers not bound in the initial interaction environment and not defined by the programmer are treated as "potentially bound" as variables to facilitate the definition of mutually recursive procedures. For example, assuming that yin and yang have not been defined,

(define yin (lambda () (- (yang) 1)))

defines yin at top level as a variable to a procedure that calls the value of the top-level variable yang, even though yang has not yet been defined. If this is followed by

(define yang (lambda () (+ (yin) 1)))

the result is a mutually recursive pair of procedures that, when called, will loop indefinitely or until the system runs out of space to hold the recursion stack. If yang must be defined as anything other than a variable, its definition should precede the definition of yin, since the compiler assumes yang is a variable in the absense of any indication to the contrary when yang has not yet been defined.

A subtle consequence of this useful quirk of the interaction environment is that the procedure free-identifier=? (Section 8.3 of The Scheme Programming Language, 4th Edition) does not consider unbound library identifiers to be equivalent to unbound (potentially bound) top-level identifiers, even if they have the same name.

(library (A) (export a)
  (import (rnrs))
  (define-syntax a
    (lambda (x)
      (syntax-case x ()
        [(_ id) (free-identifier=? #'id #'undefined)]))))
(let () (import (A)) (a undefined)) <graphic> #f

If it is necessary that they have the same binding, as in the case where an identifier is used as an auxiliary keyword in a syntactic abstraction exported from a library and used at top level, the library should define and export a binding for the auxiliary keyword.

(library (A) (export a aux-a)
  (import (rnrs) (only (chezscheme) syntax-error))
  (define-syntax aux-a
    (lambda (x)
      (syntax-error x "invalid context")))
  (define-syntax a
    (lambda (x)
      (syntax-case x (aux-a)
        [(_ aux-a) #''okay]
        [(_ _) #''oops]))))
(let () (import (A)) (a aux-a)) <graphic> okay
(let () (import (only (A) a)) (a aux-a)) <graphic> oops

Section 2.4. 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 "path") compiles the forms in the file path.ss and places the resulting object code in the file path.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 12.4. 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 12.4.

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 effect 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 default exception 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 base-exception-handler (Section 12.1) to achieve a similar affect while exercising more control over the format of the messages that are produced.

Section 2.5. 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.

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-syntax a ---)
  (define-record-type b ---)
  (define c ---)
  (define d ---)
  expr
  ...)

where expr ... runs the application using the bindings for a, b, c, and d.

A top-level "let nil" can be replaced with an RNRS top-level program and run via the --program option, the scheme-script command, or the load-program procedure.

#! /usr/bin/env scheme-script
(import (chezscheme))
(define-syntax a ---)
(define-record-type b ---)
(define c ---)
(define d ---)
expr
...

If you wish to compile a top-level program, be sure to use compile-program, since it preserves the top-level program semantics and produces more efficient code than either compile-file or compile-script.

One can also break programs up into separate libraries and load each from an RNRS top-level program via import. The compiler separately compiles all libraries and does not optimize across library boundaries, however, so the result is not as efficient as including all of an application's code within a single top-level module, let expression, or RNRS top-level program. For the best run-time performance, therefore, it is better to avoid the use of libraries and instead structure an application's code so that it can be included via the include syntax.

If one or more of the bindings must remain visible at top-level, the "let nil" can be replaced with a module form.

(module (d)
  (define-syntax a ---)
  (define-record-type b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d)
  expr
  ...)

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

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

(module abcd (d)
  (define-syntax a ---)
  (define-record-type b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d)
  expr
  ...)

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

By default, references to the names of primitives like car, cons, and + always resolve not to the top level environment but to the scheme module, where they are read-only and can be handled efficiently by the compiler. For example, the compiler can open-code (code inline in machine code) primitive operations or even fold them to constant values at compile time.

If the --revert-interaction-semantics option has been passed on the command line, or the revert-interaction-semantics procedure has been called, however, references to primitive names resolve to mutable top-level bindings, with which the compiler can do little. Addressing this is a straightforward matter of importing the bindings from the scheme module or the equivalent (chezscheme) library. It is a good idea to do this in any case to make the origin of the primitive bindings clear.

(module abcd (d)
  (import scheme)
  (define-syntax a ---)
  (define-record-type b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d)
  expr
  ...)

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

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)
  (define-syntax a ---)
  (define-record-type b ---)
  (define c ---)
  (define local-d ---)
  (define d local-d)
  expr
  ...)

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. The same effect is had by using just import of a library or set of libraries in an RNRS top-level program, since top-level programs cannot reference any identifiers bound outside of the program unless they are explicitly imported.

One may also choose to import bindings from any of the standard Revised6 Report libraries.

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 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 a top-level let, top-level module, or RNRS top-level program.

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

or

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

or

#! /usr/bin/env scheme-script
(import (chezscheme))
(include "file1.ss")
(include "file2.ss")
(include "file3.ss")
(include "file4.ss")
(include "file5.ss")

For the RNRS top-level program shown last, include is visible within the library only because the (chezscheme) library has been imported. If the library does not otherwise need the (chezscheme) library, it should still import include from the library.

#! /usr/bin/env scheme-script
(import (rnrs) (only (chezscheme) include))
(include "file1.ss")
(include "file2.ss")
(include "file3.ss")
(include "file4.ss")
(include "file5.ss")

With an application structured as described above, we have done most of what can be done to help the compiler, but there are still a few more 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 ()
  (import scheme)
  (include "file1.ss")
  (include "file2.ss")
  (include "file3.ss")
  (include "file4.ss")
  (include "file5.ss"))

The optimize level can also be set by the --optimize-level command-line option (Section 2.8.

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 12.6, and the storage manager control parameters are described in Section 13.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 12.7 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 adds significant run-time overhead.

Section 2.6. Command-Line Options

Chez Scheme recognizes the following command-line options.

-q, --quiet   suppress greeting and prompt
--script path   run as shell script
--program path   run rnrs top-level program as shell script
--libdirs dir:...   set library directories
--libexts ext:...   set library extensions
--compile-imported-libraries   compile libraries before loading
--optimize-level 0 | 1 | 2 | 3   set initial optimize level
--debug-on-exception   on uncaught exception, call debug
--eedisable   disable expression editor
--eehistory off | path   expression-editor history file
--revert-interaction-semantics   use Version 7 top-level environment semantics
-b path, --boot path   load boot file
--verbose   trace boot-file search process
--version   print version and exit
--help   print help and exit
--   pass through remaining args

The following options are recognized but cause the system to print an error message and exit because saved heaps are not presently supported.

-h path, --heap path   load heap file
-s[npath, --saveheap[npath   save heap file
-c, --compact   toggle compaction flag

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

The --script and --program options are described in Section 2.7. 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 to load. Boot files contain the compiled Scheme code that implements most of the Scheme system, including the interpreter, compiler, and most libraries. Boot files may be specified explicitly on the command line via -b options or implicitly. In the simplest case, no -b options are given and the necessary 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.boot" in a set of standard directories. It also looks for and loads any subordinate boot files required by "frob.boot".

Subordinate boot files are also loaded automatically for the first boot file explicitly specified via the command line. Each boot file must be listed before those that depend upon it.

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

Ordinarily, the search for 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, and one additional escape is recognized: "%x," which is replaced by the directory in which the executable file resides. The default search path under Windows consists only of "%x." The registry key HeapSearchPath in HKLM\SOFTWARE\Chez Scheme\csvversion, where version is the Chez Scheme version number, e.g., 7.9.4, can be set to override the default search path, and the SCHEMEHEAPDIRS environment variable overrides both the default and the registry setting, if any.

Boot files consist of ordinary compiled code and consist of a boot header and the compiled code for one or more source files. See Section 2.10 for instructions on how to create boot files.

Section 2.7. 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 implements 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) when the #! line is present, it disables the default compression of the resulting file, which would otherwise prevent it from being recognized as a script file.

If Petite Chez Scheme is installed, but not Chez Scheme, /usr/bin/scheme may be replaced with /usr/bin/petite.

The --program command-line option is like --script except that the script file is treated as an RNRS top-level program (Chapter 10). The following RNRS top-level program implements the traditional Unix echo command, as with the script above.

#! /usr/bin/scheme --program
(import (rnrs))
(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))
        (display sep)
        (display (car args)))
      (when newline? (newline)))))

Again, if only Petite Chez Scheme is installed, /usr/bin/scheme may be replaced with /usr/bin/petite.

scheme-script may be used in place of scheme --program or petite --program, i.e.,

#! /usr/bin/scheme-script

scheme-script runs Chez Scheme, if available, otherwise Petite Chez Scheme.

It is also possible to use /usr/bin/env, as recommended in the Revised6 Report nonnormative appendices, which allows scheme-script to appear anywhere in the user's path.

#! /usr/bin/env scheme-script

If a top-level program depends on libraries other than those built into Chez Scheme, the --libdirs option can be used to specify which source and object directories to search. Similarly, if a library upon which a top-level program depends has an extension other than one of the standard extensions, the --libexts option can be used to specify additional extensions to search.

These options set the corresponding Chez Scheme parameters library-directories and library-extensions. The values of both parameters are lists of pairs of strings. The first string in each library-directories pair identifies a source-file root directory, and the second identifies the corresponding object-file root directory. Similarly, the first string in each library-extensions pair identifies a source-file extension, and the second identifies the corresponding object-file extension. The full path of a library source or object file consists of the source or object root followed by the components of the library name prefixed by slashes, with the library extension added on the end. For example, for root /usr/lib/scheme, library name (app lib1), and extension .sls, the full path is /usr/lib/scheme/app/lib1.sls.

The format of the arguments to --libdirs and --libexts is the same: a sequence of substrings separated by a single separator character. The separator character is a colon (:), except under Windows where it is a semi-colon (;). Between single separators, the source and object strings, if both are specified, are separated by two separator characters. If a single separator character appears at the end of the string, the specified pairs are added to the existing list; otherwise, the specified pairs replace the existing list. The parameters are set after all boot files have been loaded.

If no --libdirs option appears and the CHEZSCHEMELIBDIRS environment variable is set, the string value of CHEZSCHEMELIBDIRS is treated as if it were specified by a --libdirs option. Similarly, if no --libexts option appears and the CHEZSCHEMELIBEXTS environment variable is set, the string value of CHEZSCHEMELIBEXTS is treated as if it were specified by a --libexts option.

The library-directories and library-extensions parameters set by these options are consulted by the expander when it encounters an import for a library that has not previously been defined or loaded. The expander searches for the library source or object file in each pair of root directories, in order, trying each of the source extensions then each of the object extensions in turn before moving onto the next pair of root directories. If the expander finds a source file before it finds an object file, it loads the corresponding object file if the object file exists and is not older than the source file. If this is not the case, and the parameter compile-imported-libraries is set to #t, the expander compiles the library via compile-library, then loads the resulting object file. Otherwise, the expander loads the source file. An exception is raised during this process if a source or object file exists but is not readable or if an object file cannot be created.

Whenever the expander determines it must compile a library to a file or loads one from source, it adds the directory in which the file resides to the front of the source-directories list while compiling or loading the library. This allows a library to include files found in or relative to its own directory.

Top-level programs may be compiled using compile-program, which is like compile-script but implements the semantics of top-level programs. It is possible to use compile-script or compile-file (if the #! line is not present) to compile a top-level program, but the semantics will be that of the interactive top level, and the resulting program will not run as fast. Any libraries upon which the top-level program depends, other than built-in libraries, must be compiled first via compile-file or compile-library. This can be done manually or by setting the parameter compile-imported-libraries to #t before compiling the program. The program must be recompiled if any of the libraries upon which it depends are recompiled.

Section 2.8. Optimize Levels

The --optimize-level option sets the initial value of the optimize-level parameter to 0, 1, 2, or 3. The value is 0 by default.

At optimize-levels 0, 1, and 2, code generated by the compiler is safe, i.e., generates full type and bounds checks. At optimize-level 3, code generated by the compiler is unsafe, i.e., may omit these checks. Unsafe code is usually faster, but optimize-level 3 should be used only for well-tested code since the absense of type and bounds checks may result in invalid memory references, corruption of the Scheme heap (which may cause seemingly unrelated problems later), system crashes, or other undesirable behaviors.

At optimize levels 2 and 3, the system also assumes that the names of built-in procedures have their original values, even if assigned to different values. This aspect of the optimize level affects only code evaluated in an interaction environment whose semantics have been reverted by the revert-interaction-semantics procedure or the --revert-interaction-semantics command-line option. It does not affect bindings imported from a library or from the scheme module, so it is irrelevant for for code evaluated in an unreverted interaction environment and for code appearing within a library or RNRS top-level program.

The compiler currently treats optimize levels 0 and 1 the same, and the only difference between levels 0 and 1 and level 2 is the level 2 treatment of primitive names in reverted interaction environments. Thus, with the exception of code evaluated in a reverted interaction environment, the only current distinction between optimize levels is that optimize-level 3 code is unsafe while optimize-level 0, 1, and 2 code is safe.

Section 2.9. 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.7, 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." The 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.10. 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.

Compiled scripts and programs.  One simple mechanism for distributing an application is to structure it as a script or RNRS top-level program, use compile-script or compile-program, as appropriate to compile it as described in Section 2.7, and distribute the resulting object file along with a complete distribution of Petite Chez Scheme. When this mechanism is used on Unix-based systems, if the source file begins with #! and the path that follows is the path to the Chez Scheme executable, e.g., /usr/bin/scheme, the one at the front of the object file should be replaced with the path to the Petite Chez Scheme executable, e.g., /usr/bin/petite. The path may have to be adjusted by the application's installation program based on where Petite Chez Scheme is installed on the target system. When used under Windows, the application's installation program should set up an appropriate shortcut that starts Petite Chez Scheme with the --script or --program option, as appropriate, followed by the path to the object file.

The remainder of this section describes how to distribute applications that do not require Petite Chez Scheme to be installed as a stand-alone system on the target machine.

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.4.) 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.4. 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 #f 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.4. 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)))

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 set of object files 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 containing the code for more than one source file, 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 files are created with make-boot-file. This procedure accepts two or more arguments. The first is a string naming the file into which the boot header and object code should be placed, the second is a list of strings naming base boot files, and the remainder are strings naming input files. For example, the call:

(make-boot-file "app.boot" '("petite") "app1.so" "app2.ss" "app3.so")

creates the boot file app.boot that identifies a dependency upon petite.boot and contains the object code for app1.so, the object code resulting from compiling app2.ss, and the object code for app3.so. The call:

(make-boot-file "app.boot" '("scheme" "petite") "app.so")

creates a header file that identifies a dependency upon either scheme.boot or petite.boot, with the object code from app.so. 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 you should construct your application so it does not depend upon features of Chez Scheme and you should specify only "petite" in the call to make-boot-file. 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" and "petite" is appropriate.

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 8 User's Guide
Copyright © 2009 R. Kent Dybvig
Revised December 2009 for Chez Scheme Version 7.9.4
Cadence Research Systems / www.scheme.com
Cover illustration © 2009 Jean-Pierre Hébert
ISBN: 978-0-966-71392-3
to order this book / about this book