2.4 Macros Introduce New Object Types

The previous sections described our document model and the techniques used to construct high-level representations given documents that conform to this model. The TEX macro facility [Knu84Knu86] allows the definition of new markup commands, making (LA)TEX extensible. Macros permit the author to abstract away layout details when writing the document. To give an example, the command \kronecker is not present in (LA)TEX. An author can extend (LA)TEX by defining

\newcommand{\kronecker}{\raisebox{1pt}{\:\otimes\:}}

and then write

$ A \kronecker B$

The new definition for \kronecker has extended the markup language. LATEX [Lam86] itself is a good example of how TEX macros can be used to implement a language for encoding document structure.

The presence of user-defined macros in documents presents an interesting challenge for a system like AS TE R. Our goal is to handle books and technical documents written in (LA)TEX, so recognizing the extended logical structure introduced by the definition of new macros is therefore essential. In general, macro expansion can perform any arbitrary computation permitted by the TEX language. Hence, it is impossible to directly translate the macro expansion into an audio rendering. The TEX primitives are visual layout operators, and translating a TEX macro directly into an audio rendering rule would imply a one-to-one mapping between the visual and audio rendering.

As explained in Section 1.1, visual renderings are attuned to a two-dimensional display, and audio renderings need to be attuned to the features of an auditory display. Further, expanding a TEX macro loses structural information; when all macros in a document have been expanded, only the visual layout remains.

The first step in solving this problem is to represent user-defined macros in our high-level document model. Producing audio renderings of such instances will then be equivalent to rendering any other object present in the model.

Macro definitions introduce new object types. Thus, defining \kronecker is equivalent to adding object kronecker to the set of objects present in the document model. A macro definition in (LA)TEX has two parts; the first part declares the macro and its number of arguments; the second part specifies how instances of this macro call are to be displayed. Translating this to the object-oriented model, the first part of the macro introduces a new object type; the second part is a rendering rule for instances of this object.

We first describe how the recognizer is extended to handle instances of the new object introduced by a macro definition. We specify the following information about the new macro and the associated object type:

Macro name:
The name of the macro.
Number of arguments:
The number of arguments taken by this macro.
Processing function:
Name of a processing function that parses instances of this macro call. This function is synthesized automatically.
Object name:
Name of the new object type introduced by this macro. Calls to this macro appearing in the document will be converted to instances of this object type.
Precedence:
If the new object is an operator, its precedence is declared in terms of one of the existing operators. See Table 2.1 on page 43 for the precedence table.
Super classes:
Super classes of this new object. The new object will inherit the behavior of its super classes. Thus, since \kronecker will be used as a binary operator, we can declare it as such.
Arguments are called:
Contextual names for the arguments of this macro. For example, the left-hand side of an inference is called its premise, the right-hand side its conclusion. If \inference is defined as a macro with two arguments, then we can supply these contextual names to define-text-object. Such information is used to generate sophisticated audio renderings.

This information is supplied by calling Lisp macro define-text-object. We illustrate this in the case of \kronecker in Figure 2.2 on page 49. The Lisp macro itself will be described in Section 2.4.1.


(define-text-object :macro-name "kronecker" :number-args 0

  :processing-function kronecker-expand

  :object-name kronecker

  :supers (binary-operator) :precedence  multiplication)

Figure 2.2: Extending the recognizer to handle user-defined macros.

Note that our recognizer has more information about the new macro than TEX. This is consistent with the fact that our internal representation is richer than the TEX representation, as described in Section 2.2.

To summarize, we model a macro as:

A call to the macro in the document creates an object of the type introduced by that macro.

To continue with the example of kronecker, given

$A \kronecker B$

LISPIFYconverts it to

(inline-math  "A"  (cs   "kronecker" ) "B" )

LISPIFYmarks the (LA)TEX macro instance as a a control sequence (cs). The recursive descent recognizer performs the following steps on encountering calls to macro \kronecker:

Function kronecker-expand constructs an instance of object kronecker. At this point, this instance of object kronecker has its children set to null. The input list is thus converted to a list containing three math objects shown below.

(ordinarykroneckerordinary)

In the above list, only the types of the objects are shown. This list is now processed by function inf-to-pre to produce the quasi-prefix form. Since class kronecker is a subclass of binary-operator with the same precedence as multiplication, the result is a tree with kronecker as the root and with two children, one each corresponding to A and B.

In the above, A and B may be arbitrarily complex pieces of (LA)TEX markup; the recursive nature of the recognition algorithm will set the children of object kronecker to the operands in their processed form. For example, we can now build an internal representation for the following equation:

(A ⊗ B)T = AT ⊗ BT

which would be written in (LA)TEX as

\[ (A \kronecker B)ˆ{T} = Aˆ{T} \kronecker Bˆ{T}  \]

2.4.1 How define-text-object Works

Lispmacro define-text-object is quite involved. In this overview, we use the call shown in Figure 2.2 on page 49 to illustrate the various steps. A call to macro define-text-object performs the following steps:

Define class:
Generate the class definition for the new object type. In our example, it produces:

(defclass kronecker (binary-operator)

  ((contents :initform nil :initarg :contents

             :accessor contents))

  (:documentation "class kronecker

   corresponding to  document macro kronecker"))

If the corresponding macro takes n arguments, i.e., number-args = n in Figure 2.2 on page 49, then the new object type is defined to have a slot arguments. This slot will hold a list containing the result of processing the n arguments of the (LA)TEX macro call.

Define processing function:
Define a processing function that is called to process instances of the (LA)TEX macro call. The function applies the recursive-descent algorithm to the next number-args tokens from the input stream. In the current example, the function generated is:

(defun kronecker-expand (’rest arguments)

  "automatically generated processing function"

  (assert (= (length arguments) 0)

          nil "wrong number of arguments")

  (let⋆ ((self (make-instance 'kronecker))

         (processor (if (math-p self)

                        #'process-argument-as-math

                        #'process-argument-as-text)))

    (unless (= number-args  0)

      (setf (arguments self)

            (loop for arg in arguments collect

                  (funcall processor arg)))) self))

where variable number-args is bound to the value of the number-args keyword argument in the lexical scope in which the function definition is evaluated. Functions process-argument-as-math and process-argument-as-text apply the recursive descent parser to their argument; the former parses mathematical content, the latter processes plain text.

Define accessor methods:
If number-args0, accessor method argument is generated. Method argument takes two arguments, an instance o of the new object and an integer n, and retrieves the result of processing argument n of the corresponding call to the (LA)TEX macro. It is used in the rendering rules to retrieve different pieces of a (LA)TEX macro call. See Section 2.4.2 for an example of its use. Assuming that number-args = n, this method looks like:

(defmethod argument ((n integer) (kronecker kronecker))

  "automatically generated argument accessor"

  (assert (<= n (length (arguments kronecker ))) nil

   "not that many arguments.")

  (elt (arguments kronecker) (- n 1)))

We take advantage of the generic dispatch provided by CLOS and define an instance of the above method with its arguments reversed —this avoids having to remember the order of arguments to function argument when writing rendering rules.

Define precedence:
If the precedence keyword argument is supplied, an appropriate call to define-precedence is generated:

(define-precedence  "kronecker" :same-as 'multiplication)

Install macro definition:
Finally, the (LA)TEX macro is installed in a global table that records all known (LA)TEX macros.

(define-tex-macro "kronecker" 0 'kronecker-expand)

This specifies that the macro being defined takes 0 arguments and calls to it should be processed using function kronecker-expand.

The call to the Lisp macro define-text-object shown in this example produces 123 lines of Lisp code.

2.4.2 Rendering Instances of User-Defined Macros

Our system of rendering rules will be described in detail in Chapter 4. Such rules are written in AFL, our language for audio formatting, described in Chapter 3. Here, we show a small example of such a rendering rule for a user-defined macro. In the following, we use CLOS generic function read-aloud, described in Chapter 4. For the present, let us assume that function read-aloud executes the necessary actions to render its argument. After executing the appropriate call to define-text-object for the (LA)TEX macro \inference, which is defined as

\newcommand\inference[2]{\frac{#1}{#2}}

to render instances of calls to \inference, we can define

(defmethod read-aloud((inference inference))

  "Sample read-aloud method for object inference.

Demonstrates how macro arguments are accessed when rendering. "

  (read-aloud (argument 1 inference))

  (read-aloud "implies")

  (read-aloud (argument 2 inference)))

If we wished to produce a rendering that inverts the order in which the arguments to macro \inference are rendered, we would define:

(defmethod read-aloud((inference inference))

  "Renders inference with arguments reversed."

  (read-aloud "We know that ")

  (read-aloud (argument 2 inference))

  (read-aloud "because")

  (read-aloud (argument 1 inference)))

A flexible method for switching among different rendering rules to obtain different “audio views” of the same object is described in Section 4.1.

Defining New Environments in LATEX

As outlined in Section 2.1, the document model in LATEX can be extended by defining new environments. Typically, a new environment is defined to achieve specific kinds of layout, e.g., for typesetting conjectures and lemmas. We treat such environment definitions as adding new objects to the document model. The approach used is very similar to handling user-defined macros, though somewhat simpler. Object new-environment is used as a base class for all user-defined environments. The principal difference between objects introduced by user-defined macros and user-defined environments is that new-environment objects can be labeled and later cross referenced. The Lisp macro define-new-environmentdoes the necessary bookkeeping involved in tracking such cross-referenceable objects. These properties are provided by the class definition for object new-environment.