Pigeon Core - Control Constructs

block

The special form:

(block code ...)

evaluates each code expression from left to right, and returns the result of the final expression. Kind of like a lambda that immediately evaluates itself. This is not terribly useful in isolation, but most often seen nested inside one of the other control constructs.

if

The special form:

(if test code [test2 code2] ... [else])

first evaluates the test condition. If the result is true, it evaluates and returns the first code expression. If the result is false, as long as there are two or more expressions remaining, it evaluates the next test, and should that be true, evaluates and returns the associated code expression. If there is only a single expression remaining (the else part) it evaluates and returns that. If there are no expressions remaining, it returns nil. Examples:

(def (min
  (lambda (a b)
    (if (less: a b) a b))))

(if verbose
  (block
    (print 'hello')
    (print 'world')))

(def (toupper
  (lambda (c)
    (if (equal: c 'a') 'A'
        (equal: c 'b') 'B'
        (equal: c 'c') 'C'
        'unknown'))))

and, or, not

There are three boolean special forms:

(and a b)
(or a b)
(not a)

These could be defined in terms of if:

(def (and (lambda (a b) (if a b a))))
(def (or  (lambda (a b) (if a a b))))
(def (not (lambda (a)   (if a false true))))

except that and and or only evaluate each argument once.

There is actually no reason why not couldn't just be a regular function, but having it built in makes life easier for the bytecode optimiser.

loop, break, next, redo

There is only one looping construct, but it is a fairly generic one:

(loop test code [tail])

This first evaluates the test condition, and if the result is false, terminates the loop with a result of nil. Otherwise it evaluates the code expression, then evaluates the tail expression if there is one, and jumps back to the top of the loop.

There are three special functions that can be called anywhere within a loop body:

These three functions cannot be used outside a loop body, or within nested lambda expressions. In such situations, use execution frames instead.

The default nil result, coupled with true from break, means you can wrap the entire construct in an or expression to simulate a Python style trailing else clause.

Some examples of how higher level constructs translate into loop format...

(while test code ...) becomes:

(loop test
  (block code ...))

(repeat code ... test) becomes:

(loop true
  (block code ...)
  (if test (break)))

A C style for loop, (for init test inc code ...) becomes:

(block
  init
  (loop test
    (block code ...)
    inc))

A Python style for loop, (for var container code ...) becomes:

(block
  (def (_it_ (iterator: container)))
  (loop (not (equal: undef (def (var (_it_)))))
    (block code ...)))

This is the kind of situation where macros really come in handy!

throw

The core exception handling mechanism is extremely simple. There is a single function:

(throw arg)

which takes an arbitrary object as the exception data. This scans back up the call chain, looking for a function with a CATCH label. When it finds one, it jumps back to that function, immediately returning the exception object from it. For instance:

// generic try block implementation
(def (try
  (lambda ((. CATCH) block)
    (block)
    nil)))

// try running some code
(def (e (try
  (lambda ()
    (print 'hello')
    (throw 'oops!')
    (print 'unreachable')))))

// check if we caught anything
(if e
  (print 'caught: ' e)
  (print 'no exception))

It would actually be quite easy to implement throw entirely in Pigeon. The only reason this is built in is that the exception system needs to be used by C++ as well as Pigeon code.

Obviously the high level syntax can make things nicer by cutting down the amount of boilerplate code, and automatically rethrowing unhandled exceptions. That is all just macros sitting on top of this basic mechanism, though.