SICP Study

Note on the Language

The code on this website is written in a language based on R6RS Scheme. The language provides custom syntax for modules and assertions, and defines a few built-in procedures and special forms. Source files use the language by wrapping their content in (SICP ...), a macro implemented in plain R6RS Scheme.

Modules

While R6RS provides a module system, using them for hundreds of sections and exercises is impractical. Their verbose syntax and indentation would distract from the flow of the chapter. We also need the flexibility to control module evaluation, for example to only run a subset of tests. This leads us to a custom solution:

(Chapter :1 "Chapter Title")

(define x 1)

(Section :1.1 "Section Title")

(define w (+ x y z)) ; ERROR: x, y, and z are undefined

(Exercise ?1.1)

(define y 2)
(define z 3)

Although every expression is at the top level, this actually defines three separate modules. Each module has a unique identifier, beginning with the : sigil for chapters and sections and the ? sigil for exercises. Modules do not nest, so :1 and :1.1 are equal siblings rather than parent and child.

Imports

We can import from other modules like so:

(Chapter :1 "Chapter Title")

(define x 1)

(Section :1.1 "Section Title"
  (use (:1 x) (?1.1 y z)))

(define w (+ x y z)) ; ok
(set! x 10) ; only affects :1.1

(Exercise ?1.1)

(define y 2)
(define z 3)

Here, :1.1 imports x from :1, and y and z from ?1.1. This implies that :1.1 must be evaluated after the other two modules, not in source order. The test runner topologically sorts modules to achieve this, and fails if there are any cycles. Importing from later modules is occasionally very useful. For example, Chapter 2 depends heavily on two-dimensional tables, which are not implemented until § 3.3.3.3.

You will not see expressions like (Chapter ...) and (Section ...) elsewhere on this website because they are converted to HTML headings. Likewise, the (use ...) blocks are converted to compact HTML lists set in a smaller font.

To avoid confusion, shadowing imports is not allowed:

(Exercise ?1.1
  (use (:1.1 x)))

(define x 2) ; ERROR: imported x is shadowed by local definition

Modules get turned into procedures, which do not allow mixing definitions with other expressions. Therefore, all top-level variables get hoisted to the top. To illustrate:

(Exercise ?1.1)

(display x) ; prints #<void> in chez
(display y) ; ERROR: y is undefined
(define x 1)

Pasting

The language provides one more feature for code reuse: pasting. Modules can unhygienically paste code from an earlier module in the same file:

(Section :1 "Original")

(define a 1)
(define (inc x) (+ x a))
(inc 42) ; 43

(Section :2 "Paste")

(define a -1)
(paste (:1 inc))
(inc 42) ; 41

If :2 had imported inc instead, the result would be 43 in both cases since a is resolved lexically. Using paste is inelegant, but preferable to duplicating code. It is often useful when an exercise asks to make an adjustment to earlier code and there is no dynamic dispatch to hook into.

Packages

Starting in Chapter 2, many modules use data-directed programming to dispatch based on operation names and type tags. Modules define packages (procedures ending in -pkg) that install operations in the global table. For example:

(define (real-part z) (apply-generic 'real-part z))

(define (rectangular-pkg)
  (put 'real-part '(rectangular) ...)
  ...)
(define (polar-pkg)
  (put 'real-part '(polar) ...)
  ...)

(using rectangular-pkg polar-pkg)

;; Tests go here.

The using procedure defined in § 2.4.3 resets the global table and installs the given packages. There is no automatic tracking of dependencies, so using calls must list everything explicitly. This package system is not part of the language, but it’s worth explaining here because so many modules use it.

Assertions

As with modules, standard techniques for assertions are too distracting. The mere word “assert” is too verbose here. Instead, the language provides six assertion operators that work at the top level: =>, ~>, =?>, =$>, =!>, and =>.... They report detailed information when they fail, including the actual result, expected result, and line number.

Exact

The => operator is by far the most common. It asserts that the left-hand side is equal to the right-hand side, using equal?. For example:

(+ 1 1) => 2

It can also be chained to assert that several expressions are equal, while ensuring that each expression is evaluated only once:

(fact 3)
=> (* 3 (fact 2))
=> (* 3 (* 2 (fact 1)))
=> (* 3 (* 2 1))
=> (* 3 2)
=> 6

When (fact 3) => (fact 5) fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (fact 3)
=> 6

right: (fact 5)
=> 120

test result: FAIL. 0 passed; 1 failed; 0 filtered out

Inexact

The ~> operator asserts that floating-point numbers are approximately equal using an absolute tolerance of 1010.10^{-10}\htmlClass{math-punctuation}{\text{.}} For example:

(* 4 (atan 1)) ~> 3.141592653589793

Like =>, it can be chained. Each item is compared to the previous one, not to the first.

When (* 4 (atan 1)) ~> 3.14 fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (* 4 (atan 1))
=> 3.141592653589793

right: 3.14
=> 3.14

delta: 0.0015926535897929917 > 1e-10

test result: FAIL. 0 passed; 1 failed; 0 filtered out

Nondeterministic

The =?> operator asserts that the left-hand side is equal to one of the elements from the right-hand side, using equal?. For example:

(random 5) =?> [0 1 2 3 4]

The right-hand side must be a list. Although square brackets are interchangeable with parentheses in R6RS, in this project we restrict their use to =?> and =$> assertions. They serve to remind us that the list is treated like (list ...), not (...) or '(...).

When (+ 1 1) =?> [(* 1 1) "two"] fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (+ 1 1)
=> 2

right: [(* 1 1) "two"]
=?> 1 | "two"

test result: FAIL. 0 passed; 1 failed; 0 filtered out

Output

The =$> operator captures standard output. For example:

(display "hello") =$> "hello"

If the right-hand side is a list, it is not evaluated but instead treated as a list of lines. Like with =?>, these lists are conventionally written using square brackets.

(define (foo)
  (display "hello")
  (newline)
  (display "world"))

(foo) =$> ["hello" "world"]

Newlines occurring at the beginning, end, or after another newline are ignored:

(display "\nOne\n\nTwo\n\n\nThree\n") =$> ["One" "Two" "Three"]

When (display "pong\nping") =$> ["ping" "pong"] fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (display "pong\nping")
=$> ["pong"
     "ping"]

right:
=$> ["ping"
     "pong"]

test result: FAIL. 0 passed; 1 failed; 0 filtered out

Error

The =!> operator asserts that an error will occur. For example:

(error 'foo "oops" 1 2) =!> "foo: oops: 1 2"

The assertion passes if the right-hand side occurs as a substring in the representation who: message: irritants. Thus, (f) =!> "" passes as long as (f) raises any error. This operator works best for errors raised by user code, as opposed to system errors. For example, (/ 1 0) fails in all Schemes, but they all use different error messages.

When (+ 1 2) =!> "disaster" fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (+ 1 2)
=> 3

right:
=!> ... disaster ...

test result: FAIL. 0 passed; 1 failed; 0 filtered out

When (error 'foo "catastrophe" 1) =!> "disaster" fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (error 'foo "catastrophe" 1)
=!> foo: catastrophe: 1

right:
=!> ... disaster ...

test result: FAIL. 0 passed; 1 failed; 0 filtered out

Nontermination

The =>... operator asserts that evaluation will never terminate. Unlike the other operators, it has no right-hand side. For example:

(let loop () (loop)) =>...

Of course, this doesn’t solve the halting problem. It just tries evaluating the expression and gives up after a short time.

When (+ 1 2) =>... fails, the output looks like this:

path/to/file.ss:123:1: assertion failed
left: (+ 1 2)
=> 3

right:
=>... (expected to never terminate)

test result: FAIL. 0 passed; 1 failed; 0 filtered out

Built-ins

The language also defines a few procedures and special forms. They are usable anywhere, not just at the top level like the assertion operators.

Procedures

(runtime) returns the time elapsed since some arbitrary point in the past, in seconds. Unlike the textbook version, which returns an integer, ours returns an inexact number with as much precision as possible. It is used for prime-test benchmarking in § 1.2.6.

(parallel-execute proc* ...) executes the given procedures in parallel. Unlike the textbook version, which returns immediately with a control object, ours blocks until all threads have completed. In addition, each thread sleeps for a random amount of time up to one millisecond before executing its procedure to help reveal bugs. It is used in § 3.4.

(make-mutex) returns an object mutex that supports messages (mutex 'acquire) and (mutex 'release). Unlike the textbook version, which calls test-and-set! in a busy loop (essentially a spinlock), ours use concurrency primitives provided by the operating system. Like parallel-execute, it is used in § 3.4.

Special forms

(capture-output exp* ...) evaluates the given expressions and returns standard output captured in a string. The =$> operator uses this internally, and it’s useful to invoke directly when you need to manipulate the string in some way.

(hide-output exp* ...) evaluates the given expressions while suppressing standard output. It returns the value of the last expression.

(cons-stream a b) is equivalent to (cons a (delay b)). It is used in § 3.5.

(with-eval eval env exp* ...) is equivalent to (begin (eval exp* env) ...), except it first creates bindings for eval and env to avoid re-evaluating them. It is used in Chapter 4 to make tests more readable.