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


Control Statements

The standard control statements in PLOT, with their syntax, are as follows:

defmacro do ?:body => ...

Evaluate a sequence of expressions in order. The last expression supplies the result(s) returned by the do statement. The preceding expressions are executed for effect.

If the keyword result: appears before one of the expressions, that expression supplies the result(s) and the other expressions are executed for effect. This allows the result(s) to come from other than the last expression in the body.

The body expressions can be followed by the keyword cleanup: and another body. The cleanup body is executed whenever control leaves the do statement, whether normally or abnormally. An abnormal exit can be due to a non-local transfer of control performed by calling an exit function established at an outer level. The result(s) of the cleanup body are discarded. The cleanup body is its own block so any definitions in it are not visible outside.

Note that the cleanup: keyword must be exdented at least one space relative to the body. Otherwise the keyword will be interpreted as an (invalid) expression in the body rather than as introducing a cleanup body. Fundamentally, this is because of the LL(1) parsing.

All statements that have bodies evaluate their bodies in this same way and accept these same keywords.

defmacro block ?:block => ...

Similar to do but defines a local scope.

In addition, the body expressions can be preceded by the keyword exit: and a name. This defines the name to be a function which when called returns immediately from the block statement without executing the rest of the body. Any intervening function calls are terminated. The arguments to the exit function become the results of the block statement. The exit function cannot be called once control has left the block statement.

All statements that contain embedded blocks evaluate those blocks in this same way and accept these same keywords.

defmacro if { ?test [then] ?then is block & elseif }+
            [ else ?else is block ] => ...

Conditional. Evaluate the tests one by one. The first test whose value is not false executes the corresponding then block and returns its values as the results of the if statement. The remaining tests are not evaluated. If every test is false, the else block is executed and its values are returned, or if there is no else block the result is false.

Example:

        if x < 10
          f(x)
          g(x - 1)
        elseif x < 100
          f(x, x * 2)
        else
          f(x / 1000)

Any definition in a test expression is defined in the enclosing block, so its scope includes the entire conditional statement including all the tests and all the bodies, and also extends outside the if statement. It is considered poor style to access such a definition outside the if statement. Each then or else clause is itself a block.

Example:

        if def temp = f(x)
          g(temp)
        else
          g(x)

Note that by PLOT newline rules you can write

if f(x) then g(x) else e(x)
and you can write
if f(x) then g(x)
else e(x)
and you can write
if f(x)
  g(x)
  h(x)
else e(x)
but you cannot write
if f(x) then g(x)
             h(x)
else e(x)
A multi-line body must start and end on a new line. This applies to all statements containing bodies or blocks, not just if.

defmacro case ?object { ?value \=> ?:block }*
                      [ default: ?default is block ] => ...

Case dispatch. Evaluate the object expression once and save the value. Then evaluate the value expressions one by one. The first value that is = to the object executes the corresponding block and returns its value(s) as the result(s) of the case statement. The remaining values are not evaluated. If no value matches, the default block is executed and its values are returned, or if there is no default block the result is false.

Example:

        case color
          #red    => f("magenta")
          #blue   => f("cyan")
          #green  => f("jade")
          selected-color => f(0)
          default: f(1)

defmacro with-slots ?object ?:block => ...

Slot access abbreviation. Evaluate the object expression once and save the value. For each slot in the compile-time class of the object, locally define a macro whose name is the slot and whose expansion is object.slotname. Execute the block in the scope of those macros and return its values as the result(s) of the with-slots statement. The slot macros are only visible in the body block.

Example:

        with-slots point
          (x - x1) ^ 2 + (y - y1) ^2
is the same as
        (point.x - x1) ^ 2 + (point.y - y1) ^2
assuming that the compile-time type of point is a class that has slots named x and y.

Example of early exit:

        block exit: yield
          for x in sequence
            if f(x) yield(x + 1)

Example of cleanup:

        do
          open-gate()
          go-through()
         cleanup:
          close-gate()

All of these statements use the syntactic types body and block. The syntax of body and implementation of its parser are:

defparser body { ^ [ result: ??result ] ?expr }+
               [ cleanup: ?cleanup is block ] =>
  ;; Construct the main body
  def body := if length(expr) > 1
                collation@compiler(expr...)
              else expr[0]

  ;; Adjust it if the result: keyword is used
  if def pos = position(true, result)
    if (for x in result count x) > 1
      error("The result: keyword can only be used once in a body")
    body := collation-first@compiler(expr[pos : expr.length - 1]...)
    if pos > 0
      body := collation@compiler(expr[0 : pos - 1]..., body)

  ;; Wrap it in a cleanup if necessary
  if cleanup
    body := sanitation@compiler(body, cleanup)

  ;; Return the P-expression
  body

Note that the implementation of the syntactic type body only allows the result: keyword to appear at most once even though the syntax pattern allows multiple occurrences.

The syntactic type block encloses a body in a block thus locally scoping any definitions in the body. It also allows an exit function to be specified before the body. The implementation looks like:

def parse-block(token-stream, error?)
  with-source-location source-location(token-stream)
    with-new-compiler-scope
      def scope = get-local-compiler-scope()

      ;; Handle the optional exit: keyword
      def exit = match?(#exit:, token-stream) and
                 parse-name(token-stream, true)
      if exit
        constant-definition(scope, exit, exit-function(name))

      ;; Parse the body
      def body := parse-body(token-stream, error? or exit)

      ;; Wrap the body in an exit if necessary
      if exit
        body := exitation@compiler(scope, exit, body)

      ;; If nothing was parsed, return false (error? must be false)
      ;; Otherwise return the body, wrapped in a block if necessary
      if not body false
      else if empty?(scope) body
      else scopation@compiler(scope, body)
Note that it not sufficient just to enclose the body in a scopation, the scope must be set up before parsing any definitions that may be in the body.


Previous page   Table of Contents   Next page