Lunar Programming Language

by David A. Moon
January 2017 - January 2018



Thrilling Examples

A collection of program examples that might be interesting:

Suffix Macros

If you wanted 32 bits and 47 unsigned bits to be types, you could do

defoperator bits
  precedence: 40
  macro: => `bits_type($lhs)`

defoperator unsigned
  precedence: 40
  macro: "bits" => `unsigned_bits_type($lhs)`

def bits_type(nbits)
  def shift = nbits - 1
  -1 << shift ..< 1 << shift

def unsigned_bits_type(nbits)
  0 ..< 1 << nbits

Percentages

If you wanted 120 * 5% = 6 and 120 + 5% = 126 but 2% + 5% = 7%, you could do something like this:

constant:
defclass percentage(percent number) number

defoperator %
  precedence: 90
  macro: => `percentage($lhs)`

def (x number) * (y percentage)         number * y.percent / 100.0
def (x percentage) * (y percentage)     percentage(x.percent * y.percent)
def (x number) + (y percentage)         number * (100.0 + y.percent) / 100.0
def (x percentage) + (y percentage)     percentage(x.percent + y.percent)

S-Expressions

Lunar does not include Lisp's S-expressions (lists made out of conses) but if you like them you can easily implement them yourself, as follows. There might be a 50% space penalty compared to a typical Lisp implementation that has a special representation for cons cells. Of course if the implementation does have a special representation for cons cells you would simply add intrinsic: in front of defclass list and the compiler would know to use that representation.

For clarity (?) I have omitted the export: annotation from in front of the following definitions.

module:
defmodule S_expressions
  local: list                   ; don't import Lunar's definition of list

;; The base class of all S-expression lists
abstract:
defclass list (car, cdr)
  _car := car
  _cdr := cdr

;; The class of all S-expression lists except the empty list
defclass cons (car, cdr) list(car,  cdr)

;; A special class just for the empty list
defclass null constructor: _make_null() list(0, 0)

;; A named constant whose value is the empty list
;; By convention this is the only instance of null
def nil = _make_null()

;; car and cdr of nil are nil
nil._car := nil._cdr := nil

;; The usual functions on lists
;; car and cdr are defined as slot readers
;; car:= and cdr:= are defined as slot writers
;; but need to be overridden for null - since the slot
;; writers are sealed, we have to use private slot names
;; to allow this override.

def car = _car
def cdr = _cdr
def car(x list) := (y everything)               x._car := y
def cdr(x list) := (y everything)               x._cdr := y

def car(x null) := (y everything)               error("Can't change car of nil")
def cdr(x null) := (y everything)               error("Can't change cdr of nil")

;; Allow car and cdr to be accessed via slot syntax as well as function syntax
sealed: def (x list).car                        car(x)
sealed: def (x list).cdr                        cdr(x)
sealed: def (x list).car := (y everything)      car(x) := y
sealed: def (x list).cdr := (y everything)      cdr(x) := y

def atom?(x) not (x in cons)
def null?(x) x eq nil

def rplaca(x cons, y)
  x._car := y
  x

def rplacd(x cons, y)
  x._cdr := y
  x

def list(x...)
  def result := nil
  for i = x.length - 1 then i - 1 while i >= 0
    result := cons(x[i], result)
  result

;; This uses eval-once which introduces temporary variables
;; as needed to prevent arguments in list from being evaluated twice
defmacro push(item_expression, list_expression) =>
  def [temps, values, expr] = eval-once(list_expression)
  `block
    ${ def $temps = $values & ^ }*
    $expr := cons($item_expression, $expr)`

defmacro pop($list_expression) =>
  def [temps, values, expr] = eval-once(list_expression)
  `block
     ${ def $temps = $values & ^ }*
     def result = car($expr)
     $expr := cdr($expr)
     result`

def print(x list, stream)
  def where := x
  write('(', stream)                    ; TODO add pretty-print hook
  until atom?(where)
    print(where.car, stream)
    write(' ', stream)                  ; TODO add pretty-print hook
    where := where.cdr
    if atom?(where) and not null?(where)
      write(". ", stream)
      print(where, stream)
  write(')', stream)                    ; TODO add pretty-print hook

;; Implement the sequence protocol
;; The iteration state is the current cons in the list

def iterate(x list)                             x
def more?(x list, position)                     not atom?(position)
def next(x list, position cons)                 position.car
def iterate(x list, position list)              position.cdr

def (x is list).length
  def length := 0
  def where := x
  while where in cons
    length := length + 1
    where := where.cdr
  length

Parsing Expressions

Parsing expressions is inherently complicated, but it is not beyond understanding. The main expression parser could have been defined by:

;;---TODO: support ternary operators
def parse_expression_without_currying(lexer, indentation, scope, required?,
                                      optional: precedence = -1, modifiers = empty_modifiers)
  ;; Three stacks to hold temporary state
  def lhs_stack    = stack()    ; expression...
  def unary_stack  = stack()    ; alternating name and bundle...
  def binary_stack = stack()    ; alternating name and bundle...

  ;; Auxiliary functions
  def do_lhs()
    def token = next(lexer)
    if token in number | character | string
      ;; A literal
      next!(lexer)
    else if same_spelling?(token, #\\)
      ;; \ operator denatures the next name
      parse_denatured_name(lexer)
    else if token in name
      def bundle = known_definition(scope, token)
      if bundle in operator
        if bundle.macro_expander
          ;; A prefix macro
          def expansion = bundle.macro_expander(lexer, indentation, scope, modifiers,
                                                next!(lexer).context)
          if expansion in expression then expansion
          else if expansion in sequence
            ;; The macro expansion is a sequence of tokens, push it back and try again
            insert!(lexer, expansion)
            do_lhs()
          else
            parse_error(lexer, "Macro $token returned unrecognized expansion")
        else
          ;; bundle is a non-macro operator
          push!(unary_stack, next!(lexer))
          push!(unary_stack, bundle)
          do_lhs()
      ;; Not an operator or a macro
      else if punctuation?(token)
        do_unrecognized_token()
      else
        ;; A plain ordinary name
        next!(lexer)
    else if token in expression
      ;; An already parsed expression
      next!(lexer)
    else
      do_unrecognized_token()

  def do_unrecognized_token()
    ;; End of expression
    if empty?(lhs_stack) and empty?(binary_stack) and unary_stack.length = 2
      ;; Special case of an operator by itself
      pop!(unary_stack)         ; discard bundle
      pop!(unary_stack)         ; return operator name
    else if required? or not empty?(unary_stack) or not empty?(lhs_stack)
      wrong_token_error(lexer, "a literal, a name, or a prefix or unary operator")
    else
      ;; Valid end of lhs
      false

  def do_unit()
    if def lhs := do_lhs()
      while not empty?(unary_stack)
        def bundle   = pop!(unary_stack)
        def operator = pop!(unary_stack)
        if #unary in bundle.arity
          lhs := call_expression(operator, lhs)
        else
          parse_error(lexer, "$operator is not a unary operator")
    else if not empty?(unary_stack)
      parse_error(lexer, "unary operator(s) ${$unary_stack&, } not followed by operand")

  def do_operator(lhs)
    def token = next(lexer)
    def bundle = token in name and known_definition(scope, token)
    if bundle in operator
      ;; Next token is an operator
      if #binary in bundle.arity or #infix in bundle.arity or #suffix in bundle.arity
        def operator_precedence = bundle.left_precedence
        if operator_precedence <= precedence
          ;; precedence parameter to parse_expression prevents seeing this operator
          ;; so this is the end of the expression
          drain(lhs, precedence)
        else if bundle.infix_macro_expander
          ;; Infix or suffix macro, drain higher precedence operators to left
          ;; then expand and continue looking for operators
          def lhs = drain(lhs, operator_precedence)
          def expansion = bundle.infix_macro_expander(lhs, lexer, indentation, scope,
                                                      next!(lexer).context, false)
          if expansion in expression
            do_operator(expansion)
          else if expansion in sequence
            ;; Push back the macro expansion and try again
            insert!(lexer, expansion)
            do_operator(do_lhs())
          else
            parse_error(lexer, "Macro $token returned unrecognized expansion")
        else
          ;; Non-macro operator
          ;; Drain higher precedence operators to left, stack this
          push!(lhs_stack, drain(lhs, operator_precedence))
          push!(binary_stack, next!(lexer))
          push!(binary_stack, bundle)
          do_operator(do_lhs())
      else
        parse_error(lexer, "$token is not a binary, infix, or suffix operator")
    else
      ;; No operator found, must be end of expression
      drain(lhs, precedence)

  def drain(rhs, precedence)
    ;; drain operators to left whose right_precedence is not less than precedence
    ;; precedence is the left_precedence of the next operator after rhs
    def rhs := rhs
    while not empty?(binary_stack) and binary_stack.top.right_precedence >= precedence
      pop!(binary_stack)                                ; discard bundle
      rhs := call_expression(pop!(binary_stack),        ; binary operator name
                             pop!(lhs_stack),           ; left-hand side
                             rhs)                       ; right-hand side
    rhs

  ;; Start with a unit on the left hand side
  def lhs = do_unit()

  ;; Allow for operators and additional units, or false if not required?
  def result = lhs and do_operator(lhs)

  ;; Some consistency checking
  assert empty?(lhs_stack)
  assert empty?(unary_stack)
  assert empty?(binary_stack)

  ;; Return the parsed expression, or false if not required?
  result

;; To avoid making a new set every time parse_expression is called without modifiers
def empty_modifiers = set!()

;; The \ prefix macro
def parse_denatured_name(lexer)
  next!(lexer)                  ; swallow the backslash
  def result = next(lexer)
  if result in name
    next!(lexer)
    result
  else if result in string
    next!(lexer)
    name(result)
  else
    wrong_token_error(lexer, "a name or a string (after a backslash)")

Then the currying feature could have been added by

def parse_expression(lexer, indentation, scope, required?,
                     optional: precedence = -1, modifiers = empty_modifiers)
  def temps = stack()           ; currying parameters
  def result = apply_currying(parse_expression_without_currying(lexer, indentation,
                                                                scope, required?,
                                                                precedence, modifiers),
                              scope,
                              temps)
  if empty?(temps)
    ;; No currying
    result
  else
    ;; Result is a curried function
    def parameter_scope = formal_parameter_scope(scope)
    def parameters = formal_parameters(scope: parameter_scope,
                        required: mapf(fun(temp)
                                         def defn = formal_parameter_definition(
                                                        name: temp, type: everything)
                                         add_definition(parameter_scope, temp, defn)
                                         defn,
                                       temps))

    method_expression(method_head(name:              false,    ; anonymous
                                  modifiers:         method_modifiers(),
                                  formal_parameters: parameters,
                                  result_type:       everything),
                      ;; body is result
                      result)

;; Replace blanks in expr with unique names added to temps
def apply_currying(expr, scope scope, temps stack)
  ;; Default method does nothing
  expr

def apply_currying(expr call_expression, scope scope, temps stack)
  def initial_length = temps.length
  def new_parameters = mapf(fun(param)
                              if same_spelling?(param, #_)
                                def temp = name("temp", macro_context(scope))
                                push!(temps, temp)
                                temp
                              else
                                apply_currying(param, scope, temps),
                            expr.parameters)
  if temps.length = initial_length
    ;; Nothing changed
    expr
  else
    ;; Substitute new parameters
    (if expr in spread_call_expression then spread_call_expression else call_expression)(
      expr.function, new_parameters...)

TODO more examples TBD

TODO Enumeration Types example

TODO Regular Expressions example


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.