Version: 4.0.2.3

2 Concatenative Macros

  > (require (planet zwizwa/staapl/coma))

  ;; \__ target

  ;; \__ coma

The Coma language is a Scat-like language residing in the (macro) namespace. It can be accessed using the macro: and macro> forms analogous to the scat: and scat> forms in the previous section.

(macro: word ...)

(macro> word ...)

Coma’s values are lists of stack machine instructions, transformed by its operations. The transformations are always local to the end of the list, so the list of instructions behaves as a stack. Coma is thus a stack language serving as meta language for another stack language.

In its most basic form, the stack machine has two instructions. The qw (Quote Word) instruction loads a literal (number) from the instruction stream onto its parameter stack, while the cw (Call Word) instruction performs a procedure call to a named function. The Coma language performs partial evaluation on the qw instructions, but leaves the abstract cw instructions alone.

  > (macro> 1 2)

  [qw 1]

  [qw 2]

  > (macro> 1 2 +)

  [qw (1 2 +)]

  > (macro> 123 345 10 3 -)

  [qw 123]

  [qw 345]

  [qw (10 3 -)]

The Coma composition mechanism is the same as Scat’s. The difference of the two languages lies in their primitives. When writing a compiler for a particular architecture, the task is to define the semantics of the primitives in terms of target code transformers. This can done using the form patterns, a pattern matching mechanism for stack machine code.

(patterns namespace (lhs rhs) ...)

 

lhs

 

=

 

([opcode operand ...] ... name)

 

 

 

 

 

rhs

 

=

 

([opcode operand ...] ...)

 

 

|

 

expr

Each clause (lhs rhs) defines how the Coma macro name transforms a particular pattern of instructions ([opcode operand ...] ...) on the top of the code stack. For each unique name, the corresponding patterns are grouped and tried from top to bottom until there’s a match. The opcodes in lhs are matched literally as symbols, while the operands bind variables in the tagged lists representing instructions. The namespace is most likely (macro).

If rhs is a list of lists, the macro will replace the matched instructions with a list of template instructions with the opcodes quoted literally and the operands evaluated. If rhs is not a list of lists, it is a Scheme expression that evaluates to a macro which is subsequentially evaluated on the code stack with the matched instructions removed.

For example, Let’s redefine the Coma macro + to use the PIC18 instructions addlw.

  > (patterns (macro)

      (([qw a] [qw b] +) ([qw (+ a b)]))

      (([qw a] +)        ([addlw a])))

  ;; macro/+

  > (macro> 1 2 +)

  [qw 3]

  > (macro> 123 +)

  [addlw 123]

On top of the + just defined, we define a macro - using macro delegation. The second pattern doesn’t produce a macro but throws an exception.

  > (patterns (macro)

      (([qw a] -)        (macro: ',(* -1 a) +))

      ((-)               (error 'not-implemented)))

  ;; macro/-

  > (macro> 10 3 -)

  [qw 7]

  > (macro> -)

  error: not-implemented

Observe that (macro> 1 2 +) is now different than before. We used direct evaluation in the expression (+ a b). However, it is best to use the tscat: form for this.

(tscat: word ...)

This is like scat: in that the words come from the scat namespace, but it is different in two ways. If word is present in the lexical context, it is interpolated as a quoted literal value. In the case above, the expression (tscat: a b +) is equivalent to (scat: ',a ',b +). Additionally, the computation is wrapped to be evaluated at assembly time. This allows code addresses that are only available at assembly time to be used in partial evaluation. The previous example can now be adapted to:

  > (patterns (macro)

      (([qw a] [qw b] +) ([qw (tscat: a b +)]))

      (([qw a] +)        ([addlw a])))

  ;; macro/+

  > (macro> 1 2 +)

  [qw (1 2 +)]

  > (macro> 123 +)

  [addlw 123]

Beyond the first level of deconstruction that patterns performs, enforcing primitives to process lists of opcode . operands pairs locally as a stack, the pattern matching syntax is PLT Scheme’s match from scheme/match and can be used to further deconstructs operands. This allows the processing of arbitrary data structures in intermediate code and is the substrate of compile time computation in Staapl. One creates primitive code generators and processors that can be used as components in low-level programs written in a Forth-like compositional language.

  > (define-struct vec (x y))

  > (patterns (macro)

      (([qw x] [qw y] vpack)               ([qw (make-vec x y)]))

      (([qw (struct vec (x y))] vunpack)   ([qw x] [qw y]))

      (([qw (struct vec (x y))] vswap)     ([qw (make-vec y x)])))

  > (macro> 1 2 vpack)

  [qw #<vec>]

  > (macro> 1 2 vpack vswap vunpack)

  [qw 2]

  [qw 1]

Opcodes are just symbolic tags and can be used inside a patterns form without the need for declaration. The only restriction is that stack machine code produced by a composition of macros can be processed by the stack machine assembler. This freedom allows the introduction of macros that serve as primitives for arbitrary code generation idioms, by allowing arbitrary data to be passed on the code stack in the form of compile time data, represented by the 'qw tag, or arbitrarily tagged pseudo instructions.

Instead of [qw (struct vec (x y))] we could have introduced the opcode 'vec using something like [vec v] or [vec (struct vec (x y))]. Doing so prevents manipulation of such objects by generic stack manipulation words like dup and drop. The general rule is: use 'qw for anything that rougly behaves as a target machine value, and use anything else to prevent the partial evaluator for 'qw tags to touch the instruction.

For certain classes of operations one can abstract the possible patterns over the instances of the class. To this end the macro patterns-class can be used.

(patterns namespace formals (actuals ...) clause ...)

The formals parameter is a list containing identifiers that parameterize the clauses, which are pattern clauses passed to the patterns form. The actuals parameters are values to be filled in for the formal parameters.

The following example defines pattern matching rules for unary operators on the Microchip PIC18 architecture. Note that the first clause implements peephole optimization: it combines a real machine instruction with the effect of a macro.

  > (patterns-class (macro)

        [word         opcode]

        [(1+          incf)

         (1-          decf)

         (rot<<c      rlcf)

         (rot>>c      rrcf)

         (rot<<       rlncf)

         (rot>>       rrncf)

         (swap-nibble swapf)]

     (([movf f 0 0] word) ([opcode f 0 0]))

     ((word)              ([opcode WREG 0 0])))