Lunar Programming Language

by David A. Moon
January 2017 - January 2018



Statements

The following sections explain the standard syntactic forms that could be considered to be statements. There is actually no distinction between a statement and an expression in Lunar; we just call an expression a statement if it tends to be used for effect or an expression if it tends to be used for value.

All of these statements are defined within the language by using macros.

If Statement

if is a macro that provides the conditional statement. It could have been defined by:

defmacro if test_expression [ "then" ] [^] then_body
            [ [ ^= ] "else" else_body ] =>
  if_expression(test_expression, then_body, else_body or quotation(false))

The result is false if the test is not satisfied and no else body is present. Otherwise the result is the result of whichever body is chosen for execution by the test.

Case Statement

case is a macro that provides a "dispatching" conditional statement. It evaluates a subject expression exactly once, then compares the result to a series of value expressions one by one. The first clause whose value result is = to the subject result is chosen for execution and the result of the case statement is the result of that clause's body. Other clauses' bodies and the remaining value expressions are not executed. If no value matches, the default block is executed and its result is the statement's result, or if there is no default block the statement's result is false.

Example:

        case color
          #red    => f("magenta")
          #blue   => f("cyan")
          #green  => f("jade")
          selected_color => f(0)
          default: f(1)
which could also be written this way, since every statement is an expression:
        f(case color
            #red   => "magenta"
            #blue  => "cyan"
            #green => "jade"
            selected_color => 0
            default: 1)

The case statement requires each clause to be on a separate line.

The case statement could have been defined by:

defmacro case subject_expression
              { ^ value_expression "=>" body }+
              [ ^ "default:" default_body ] =>
  `block
    def subject = $subject_expression
    ${if $value_expression = subject then $body & else}
    else $(default_body or `false`)`

See Exception Handling for more complex control statements.

Block Statement

The block statement contains a series of one or more expressions that are executed sequentially. If there is more than one expression, the expressions must be separated by newlines and a newline must intervene between the word block and the first expression. An expression can be preceded by any number of modifier keywords. All expressions in the statement must be indented equally. The result of the statement is the result of the last expression. In other words, block is followed by a body.

block delimits the scope of definitions in its body; these definitions are only visible in the rest of the body and are not visible outside of its body. In all statements that include a body, the body is actually a block.

Early Exit

Sometimes it is useful to bypass the normal flow of control and exit early from a block. Informally this is called "unwinding the stack."

The block statement provides this feature by writing exit: name before the body. This defines name as an exit function that is in scope in the body. Calling the exit function with one actual parameter immediately returns that parameter as the result of the block statement. If the exit function is never invoked, the result of the block statement is the result of its body.

It is an error to call an exit function after its block has already been exited; exit functions are not full continuations.

Cleanup

Sometimes it is useful to guarantee that cleanup code is executed regardless of whether execution takes the normal exit or an early exit. The block statement provides this feature by writing finally: cleanup_body after the main body. When the stack is unwound, by normal or early exit, the cleanup_body will be executed. Its result is discarded.

Parsing the Block Statement

block could have been defined by:

defmacro block [ "exit:" exit_name ] body [ ^^ "finally:" cleanup_body ] =>
  ;; Wrap the special features around the body
  def body1 = if exit_name then exit_expression(exit_name, body) else body
  def body2 = if cleanup_body then finally_expression(body1, cleanup_body) else body1
  ;; Result is the wrapped block
  body2
except that if exit_name is present it needs to be brought into scope before parsing the rest of the statement.

Most of the heavy lifting is built into parse_body.

parse_body could have been defined by

def parse_body(lexer, indentation, scope, required?)
  def block_scope = block_head_scope(scope)
  def body_indentation = match_newline?(lexer, indentation, true)
  if body_indentation
    ;; One or more expressions separated by newlines
    def loop(inner_scope)
      def expr := parse_modified_expression(lexer, body_indentation, inner_scope, true)
      if expr in prog_expression
        ;; Rip prog apart
        inner_scope.expressions := inner_scope.expressions + butlast(expr.expressions)
        expr := last(expr.expressions)
      ;; Append expr to body so far
      inner_scope.expressions := inner_scope.expressions + [expr]
      ;; Check for more lines
      if match_newline?(lexer, body_indentation, false)
        ;; There are more expressions, continue parsing with possible scope change
        loop(if expr in block_tail_scope then expr else inner_scope)
      else
        ;; expr is the last expression in the body
        if empty?(block_scope) and inner_scope.expressions.length = 1
          ;; The body is just one expression, strip unnecessary block_head_scope
          expr
        else
          ;; Multiple expressions and/or definitions in the body
          block_scope
    loop(block_scope)
  else
    ;; A single expression, or maybe false if not required?
    def expr = parse_modified_expression(lexer, indentation, block_scope, required?)
    if empty?(block_scope)
      ;; Strip unnecessary block_head_scope
      expr
    else
      ;; Wrap a definition in the block_head_scope
      block_scope.expressions := block_scope.expressions + [expr]
      block_scope

;; Parse an expression preceded by optional modifier keywords
;; but not finally:
def parse_modified_expression(lexer, indentation, scope, required?)
  if next(lexer) in keyword and next(lexer).name ~= #finally
    def modifiers = set![name]()
    while next(lexer) in keyword
      push!(modifiers, next!(lexer).name)
      match_newline?(lexer, indentation, false)
    def result = parse_expression(lexer, indentation, scope, true, -1, modifiers)
    if not empty?(modifiers)
      def plural = if modifiers.length > 1 then "s" else ""
      parse_error(lexer, "Unrecognized modifier$plural ${$modifiers&,} preceding $result")
    result
  else parse_expression(lexer, indentation, scope, required?)

Iteration Statements

The language semantics require tail recursion removal, so iteration can be accomplished simply by writing a recursive function. However, for readability several iteration statements are defined by macros exported by the lunar module.

while and until statements repeatedly execute the body zero or more times until the test is false or true, respectively. The scope of any definition in the test includes the body. The entire statement is enclosed in a block. The result is always false.

They could have been defined by:

defmacro while expression body =>
  `block
     def loop()
       if $expression
         $body
         loop()
     loop()`

defmacro until expression body =>
  `block
     def loop()
       if not $expression
         $body
         loop()
     loop()`

Note that macro hygiene prevents the definition of loop from being visible to expression or body.

See For Statement for a more complex and powerful iteration statement.

Function Statement

The fun statement creates a method object from a formal parameter list and body. The method can be invoked directly with actual parameters or can be added to a function bundle.

The fun statement creates a generic method if generic formal parameters are supplied in square brackets.

A fun statement allows a name (purely for debugging) and modifier keywords before the formal parameter list and a result type after the formal parameter list.

The syntax of the fun statement is

$fun [ "[" generic_formalparameters "]" ]
     [ name ]
     "(" methodmodifiers formalparameters ")"
     [ "=>" result_type_expression ]
     body

The syntax of methodmodifiers is

{ "sealed:" | "dominant:" | "intrinsic:" | "description:" description_expression [ "," ] }*
which specify the corresponding attributes of the method. The result of description_expression must be a string. The comma after description_expression is only necessary when the formalparameters start with a [ destructuring, but it can always be inserted to improve readability.

TODO: Why are the modifiers inside the parentheses rather than preceding the fun statement? Because of description: but maybe that indicates a general defect in the syntax?

Formal Parameter List

A formal parameter list comprises a required section, an optional section, a named section, and a rest section, in that order. A section must be omitted when there are no formal parameters of that variety. Sections that are not omitted are separated by commas. The optional section is introduced by optional:. The named section is introduced by named:. The rest section is followed by ... which must be the last token in the formal parameter list.

If a section contains more than one formal parameter, they are separated by commas. The rest section can only contain one formal parameter.

Each required or rest formal parameter is a name followed by an optional expression known as a type suffix, which specifies that the corresponding actual parameter must be a member of the specified type.

Each optional formal parameter is a name, optionally followed by = and a default expression, followed by an optional type suffix.

Each named formal parameter is an optional keyword, then a name, optionally followed by = and a default expression, followed by an optional type suffix. The selector is the keyword's name, or the parameter name if no keyword is present.

The "name" of a formal parameter can be a destructuring expression rather than just a name. See Destructuring Formal Parameters If the name of a named formal parameter is a destructuring, it must have a keyword to specify its selector.

If a formal parameter name, type suffix, and default are replaced by # constant it is an abbreviation for an anonymous formal parameter whose type is set(#constant), i.e. it only accepts that constant as an actual parameter. The constant should be a name, integer, or character.

An optional newline can be inserted before the first formal parameter or after a comma. This allows a formal parameter list to extend onto a continuation line. The indentation of the newline must be greater than the indentation of the line where the parameter list began.

TODO: examples of formal parameter lists

Curried Functions

A curried function is an abbreviated form of the function statement that creates an anonymous method with one or more required parameters, no optional, named, or rest parameters, no modifiers, and no result type declaration.

The name _ (pronounced "blank") has a special meaning understood by parse_expression, indicating partial application. Thus a function call containing one or more blanks creates a curried function which has one required parameter for each occurrence of _. The parameter replaces the blank.

For example, _ + 5 is a function of one formal parameter that adds 5 to its actual parameter. It is a shorthand abbreviation for fun(temp) temp + 5 where temp is a hygienically invisible name.

This works for both function call syntax and operator syntax. Thus f(_, 1, _) is a function of two parameters that calls the function f with three actual parameters. It is short for fun(temp1, temp2) f(temp1, 1, temp2).

This works for nested operator calls as well. For example, _ + 3 * _ is a function of two parameters that adds the first to three times the second, not a function of one parameter that adds its parameter to another function of one argument that multiplies its argument by 3. It is short for fun(temp1, temp2) temp1 + 3 * temp2. Presumably the + function has no method applicable to functions.

Similarly, (_ + 3) * _ is a function of two parameters that multiplies the first plus three times the second. It is short for fun(temp1, temp2) (temp1 + 3) * temp2.

A useful example is

sort(collection, _.key < _.key)
which passes to sort a comparison function that compares the keys of its two parameters. This is short for sort(collection, fun(x, y) x.key < y.key). Currying does not work for nested function calls using parentheses or brackets, because that would prevent examples such as this one.

Unfortunately currying cannot be defined by a macro but must be built into parse_expression.

Assignment Statement

The assignment operator := is a right-associative, infix operator macro whose right-hand side is an expression. The result of the operator invocation is the result of the right-hand side.

If the left-hand side is a name, the name's definition in the current scope must be variable. The definition's value is changed to the result of the right-hand side.

If the left-hand side is a function call, the function being invoked must be a name. The macro's expansion is an invocation of the corresponding assignment function, whose name is the original function's name suffixed with ":=" and whose hygienic context is the original function name's context. The assignment function takes one additional parameter, which is the right-hand side, and must return that parameter as its result. An invocation on the left-hand side can be used to assign to a slot, a list! member, or other functionally-accessed location.

Examples:

f(x, y) := z
is equivalent to
\"f:="(x, y, z)

x.y := z
is equivalent to
\".:="(x, #y, z)
which typically is defined to assign to the y slot of the datum that is the value of x.

array[index] := value
is equivalent to
\"[:="(array, index, value)
because
array[index]
is a macro invocation that expands into
\"["(array, index)
This typically is defined to assign to the member of array selected by index.

The assignment operator could have been defined by:

;; left_precedence higher than right_precedence makes it right-associative
defoperator := precedence: 80,0
  macro: lhs ":=" rhs_expression =>
    if lhs in name then assignment_expression(lhs, rhs_expression)
    else if lhs in call_expression and lhs.function in name
      ;; assignment function name has := suffix, in same context
      call_expression(name(lhs.function.spelling + ":=", lhs.function),
                      lhs.parameters + [rhs_expression] ...)
    else error("invalid left-hand side for assignment: $lhs")

TODO cross-reference additional statements defined elsewhere


Previous page   Table of Contents   Next page



Creative Commons License
Lunar by David A. Moon is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Please inform me if you find this useful, or use any of the ideas embedded in it.
Comments and criticisms to dave underscore moon atsign alum dot mit dot edu.