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


An operator is a subclass of function. A name that has a known definition as an operator and is not denatured by a \ prefix has special syntactic meaning in expressions. An operator can potentially be used in prefix form and/or in infix form. Either form of an operator can be a function call, a macro call, or disallowed, depending on the definition of the operator. An operator function call has the semantics of an ordinary function call, just in a special syntax. An operator has precedence and associativity properties that control its parsing. The right-hand operand of an infix operator, and the only operand of a prefix-operator, will not include infix operators of lower precedence, and will only include infix operators of equal precedence if the operator is right-associative.

If an operator is a function call in both prefix and infix form, both forms invoke the same function. Methods that accept one argument are applicable to prefix calls. Methods that accept two arguments are applicable to infix calls.

defoperator defines an operator. You specify its name, precedence, associativity (default left), prefix usage, and infix usage. Each usage is either a macro definition or a function parameter list (which is only for documentation). If no usage is specified, that usage is disallowed.

Operator macros are a powerful language extension facility that can be used to define the () and [] syntaxes that would otherwise have to be built in. Other examples of operator macros include and, or, is, and :=.

The specification for an infix or prefix operator macro consists of an optional pattern, an =>, and a block whose value is the macro expansion. The pattern is specially modified so that if it ends with a pattern variable whose syntactic type is expression (even inside of a repeat), the parsing of that expression will respect operator precedence and associativity; parsing will stop short of the maximal length expression when an operator of lower precedence is encountered. Thus "a and b or c" parses as "(a and b) or c" rather than as "a and (b or c)", and "a or b and c" parses as "a or (b and c)". Similarly "not a or b" parses as "(not a) or b" rather than as "not (a or b)", while "not f(x)" parses as "not(f(x))" rather than "(not f)(x)".

In an infix operator macro's parser function, the name lhs is automatically defined to be the expression on the left hand side. The explicit pattern only applies to the right hand side that follows the operator.

Note that when an operator is a macro, the definition of its name is not a macro. The definition of its name is an operator. You can have an operator macro that expands into a call to the function with the same name. Infix [ is an example, which is a macro so it can match the ] and so it can allow multiple arguments separated by commas.

To define a postfix operator, just define an infix operator macro with an empty pattern for the right-hand side.

Operator overloading: If an operator is a function call, simply define methods for it that are applicable to the argument(s) of interest, the same as for a non-operator function.

The syntax of defoperator is

defmacro defoperator ?:name [ ^ precedence: ?precedence is literal |
                              ^ associative: [ left | ??right-associative right ] |
                              ^ [ prefix: ?prefix-parms is parameter-list |
                                  prefix-macro: ?prefix-pattern is pattern \=>
                                                ?prefix-body is block ] |
                              ^ [ infix: ?infix-parms is parameter-list |
                                  infix-macro: ?infix-pattern is pattern \=>
                                               ?infix-body is block ] ]* => ...

Some examples of built-in operators:

defoperator +
  precedence: 120
  prefix: (arg)
  infix: (lhs, rhs)

defoperator -
  precedence: 120
  prefix: (negatend)
  infix: (minuend, subtrahend)

defoperator /
  precedence: 130
  infix: (dividend, divisor)

defoperator and
  precedence: 40
  infix-macro: ?rhs => `if ?lhs then ?rhs else false`

;; This works because macros are hygienic, so the local variable
;; named temp does not conflict with anything else named temp
;; It wraps the whole thing in a block so the scope of temp cannot
;; become global
defoperator or
  precedence: 30
  infix-operator: ?rhs =>
           if def temp = ?lhs
           else ?rhs`

defoperator \(
  precedence: 200

  ;; Prefix () is grouping
  prefix-macro: ?x \) => x

  ;; Infix () is function call
  ;;--- The real definition has additional support for spread-invocation and currying
  infix-macro: ?:argument-list \) => invocation@compiler(lhs, argument-list...)

defoperator \[
  precedence: 200

  ;; Infix [] is subscripting - call the \[ function
  infix-macro: ?:argument-list \] => `\[( { ?argument-list & , }* )`

  ;; Prefix [] is list or dictionary constructor
  prefix-macro: { ?element [ \=> ?value ] & , ^^ }* \] =>
    ;; If no arrows are present, it is a list
    if every?(\not, value) `list( { ?element & , }* )`
    ;; Otherwise arrows must be used consistently
    else if member?(false, value)
      error("Inconsistent use of arrows in [...] syntax")
    ;; With arrows it is a dictionary
    else `dictionary( { ?element , ?value & , }* )`

;; Construct a function whose name is specified by the optional variable
;; and whose parameters and result type are specified by the parameter-list
;; The name is only for debugging and has no other significance
defmacro fun [ ?:variable ] ?:parameter-list [ ?:annotations ] ?:block =>
                    functation@compiler(variable or #anonymous,

Previous page   Table of Contents   Next page