Programming Language for Old Timers

by David A. Moon
February 2006 .. September 2008

Comments and criticisms to dave underscore moon atsign alum dot mit dot edu.

Previous page   Table of Contents   Next page

Introductory Examples

To fully understand these examples you will need to read and understand the entirety of this document. However, perusing the examples can give you some sense of the "flavor" of the PLOT language.

;; A silly toy program
defun response(threat)
  if threat = #severe
  else if threat = #moderate
    print("Danger Will Robinson")

;; A simple class definition with two slots
defclass cons (a, d) is collection
  car := a
  cdr := d


;; Fibonacci, the classic recursive function
defun fib(x)
  if x > 2
    fib(x - 1) + fib(x - 2)
  else 1

;; Another way to write Fibonacci, letting function dispatch
;; take care of the conditional
defun fib(x) 1
defun fib(x is integer-above(2)) fib(x - 1) + fib(x - 2)

;; Three ways to increment the elements of a list and produce a new list

defun inclist(lst is list, optional: incr = 1)
  map(fun(x) x + incr, lst)

defun inclist(lst is list, optional: incr = 1)
  map(_ + incr, lst)

defun inclist(lst is list, optional: incr = 1)
  for x in lst collect x + incr

;; The simplest iteration macro
defmacro while ?test ?:body =>
     defun loop()
       if ?test

;; A simple iteration macro so we can do things like
;; for i from 0 to 10 print(f(i))
defmacro for ?var is name from ?from to ?to ?:body =>
     def start = ?from
     def limit = ?to
     defun loop(?var)
       if ?var <= limit
         loop(?var + 1)

;; The macro expansion of for i from 0 to 10 print(f(i)) is
;; as follows, ignoring hygienic scoping:
  def start = 0
  def limit = 10
  defun loop(i)
    if i <= limit
      loop(i + 1)

Obviously in order to work this depends on tail recursion removal so the recursive function loop does not blow out the stack, and depends on hygienic macros so the local definitions of start, limit, and loop do not pollute the namespace of the body block. If the body had been print(limit(i)), that limit would not be the same limit that is defined just before the loop.

Here is how to define the same macro without using patterns and templates, instead parsing and constructing the object-oriented representation of source code directly:

defmacro for =>
  ;; The for statement will be inside a block
  ;; I have cheated a little and used the same block for the whole statement
  ;; and for the loop method, since I know adding a nested block for the
  ;; method would not make any difference

    ;; Parse the simple for statement
    def var       = parse-name(tokens, indentation, true)
    match!(#from, tokens)
    def from      = parse-expression(tokens, indentation + 1, true)
    match!(#to, tokens)
    def to        = parse-expression(tokens, indentation + 1, true)
    def body      = parse-body(tokens, indentation + 1, true)

    ;; Create names that will not be visible to the caller
    def start-var = name("start", macro-context)
    def limit-var = name("limit", macro-context)
    def loop-var  = name("loop",  macro-context)

    ;; Construct the loop function
    def params = parameter-list@compiler(list(var), list(#$anything@PLOT),  // required
                                         false, false, false,               // optional
                                         false, false,                      // rest
                                         false, false, false,               // key
                                         false, false)                      // result
    def test = invocation@compiler(#<=@PLOT, var, limit-var)
    def recurse = invocation@compiler(loop-var, invocation@compiler(#+@PLOT, var, 1))
    def body2 = collation@compiler(body, recurse)
    def loop-body = conditional@compiler(test, body2, #false@PLOT)
    def loop = functation@compiler(loop-var, get-local-compiler-scope(),
                                   params, loop-body)

    ;; Construct the main part of the expansion
    def scope = get-local-compiler-scope()
    def expansion := scopation@compiler(
      variable-definition@compiler(scope, start-var, $anything, from),
      variable-definition@compiler(scope, limit-var, $anything, to),
      method-definition@compiler(scope, loop-var, loop),
      invocation@compiler(loop-var, start-var))

    ;; Return the macro expansion

Any language that can't handle macro-defining macros is babyish. Here is PLOT's predefined macro-defining macro:

;; Define a macro, using pattern matching to specify the parsing
defmacro defmacro ?:name { ^ ?:pattern \=> ?:block }+ =>
  def possibilities = collect-pattern-starts(pattern)
  def err = `wrong-token-error(?=tokens, ?possibilities)`
  def expander = reduce-right(translate-pattern(`?=tokens`, `?=indentation`, _, _, _),
                              err, pattern, block)
  `def ?name = macro@compiler(fun ?name(?=tokens is token-stream,
                                        ?=indentation is integer)
                               def ?=macro-context = unique-macro-context()

What does this mean? Just this: The syntax of defmacro is a name (possibly quoted if it's an operator) followed by one or more occurrences of "pattern => block". Each occurrence can start on a new line, but all occurrences must be indented the same, as indicated by the ^ in the pattern. If none of the patterns match, call wrong-token-error with the token-stream (for source location context) and an error message constructed from the patterns that describes what tokens were allowed. The body of the macro expander is constructed by translating each pattern into code that parses the tokens coming from the token-stream and if the pattern matches, defines the pattern variables as appropriate portions of the parse tree, executes the body block, and returns the result of the body as the expansion of the macro. If the pattern does not match, try the next "pattern => block" rule. If no more rules, call wrong-token-error. Having constructed the expander, define the name of the macro to be a macro object encapsulating a function that takes a token-stream and an indentation as its arguments, defines macro-context to be a new macro context, and runs the expander. The definitions of tokens, indentation, and macro-context are made visible to the expander rather than hygienic. Fairly simple as macro-defining macros go.

Suppose you were writing some kind of parser and wanted to use a bit vector indexed by character code to classify incoming characters. Further suppose that, quite reasonably, you wanted to construct the bit vectors at compile time rather than at run time or during program initialization. Here is one way you could do that with a macro:

defmacro def-character-class
         ?:name = { ^ ?expr }+ [ size: ?size] =>
  ;; Functions to convert input to character code ranges
  defun range(x is invocation) code(x.arguments[0]) .. code(x.arguments[1])
  defun range(x is anything) code(x) .. code(x)
  defun code(x is integer) x
  defun code(x is character) x.code
  ;; Build the bit vector at compile time
  def bits = bit-vector#(size or 256)
  for x in expr
    for code in range(x)
      bits[code] := 1
  ;; Define name to be that constant bit vector
  def constant = quotation@compiler(bits)
  `def ?name = ?constant`

;; Example uses of the macro

def-character-class whitespace = ' ' '\t' 10 13

def-character-class letters = 'A' .. 'Z'    ; Majuscules
                              'a' .. 'z'    ; miniscules

That code is a bit sloppy, for example it assumes that any invocation must be an invocation of the range construction operator .. however that is probably how much care a typical application programmer would take.

Previous page   Table of Contents   Next page