Introduction to Moonli for Common Lispers

Moonli is a syntax layer that transpiles to Common Lisp.

For example,

defun sum(args):
  if null(args):
    0
  else:
    first(args) + sum(rest(args))
  end
end

transpiles to:

(defun sum (args)
   (cond ((null args)
          0)
         (t
          (+ (first args)
             (sum (rest args))))))

See moonli-sample.asd and sample/sample.moonli to include in your project.

Table of Contents

Due to tools like paredit and lispy (beyond macros and metaprogramming), s-expression (Lists) based syntax of lisps is very powerful. However, not every one has the time or patience to become comfortable with them, especially when it comes to reading code, or sharing it with your colleagues.

In the 21st century, very many more people are familiar with python, matlab and julia than they are with lisps. Given the power and flexibility of common lisp, moonli is an attempt to provide a thin syntax layer over common lisp. It is thin in the sense it can be easily transpiled to common lisp. The semantics remain the same and clean as common lisp. (Common Lisp is also good for reasons beyond macros.)

Contrasts with Common Lisp

  • Case sensitive, but invert-case reader to maintain common lisp compatibility
  • Extensible using moonli:define-moonli-macro and moonli:define-short-moonli-macro. See src/macros/ directory for examples.
  • Inability to access internal symbols of another package through "A::B" syntax; this syntax rather translates to (the B A)

Syntax

As with lisp, everything is an expression.

Moonli’s syntax can be understood in terms of a (i) Core Syntax, and (ii) Macros. The core syntax makes space for macros, and macros provide extensibility. A third part concerns the with-special macros.

Core Syntax

  Lisp                       Moonli
  -------------------------- -------------------------
  #\a                        'a'
  "hello world"              "hello world"
  2, 2.0, 2d-3, 2.0d-3       2, 2.0, 2d-3, 2.0d-3
  'quoted-symbol             $quoted-symbol
  package:exported-symbol    package:exported-symbol
  package::internal-symbol   <WONTDO>
  (the type expr)            expr :: type
  (list form-1 form-2)       (form-1, form-2)
  (fn arg1 arg2)             fn(arg1, arg2)
  #c(re, im)                 <TODO>

Macros

One defines a Moonli Macro or a Moonli Short Macro that expands to a Common Lisp macro or special form. These can be defined by moonli:define-moonli-macro and moonli:define-moonli-short-macro respectively. The difference between a Moonli Macro and a Moonli Short Macro is that the former end with end and can stretch over multiple lines, while the latter are expected to either span a single line or have their components be separated by non-newline whitespaces. See src/macros for examples.

Several Moonli macros are predefined as part of Moonli system, and you can add more Moonli macros as part of your own library or application.

Example transpilations for these predefined Moonli macros are given below:

declaim

declaim inline(foo)

transpiles to

(declaim (inline foo))
declaim type(hash-table, *map*)

transpiles to

(declaim (type hash-table *map*))

declare

declare type(single-float, x, y)

transpiles to

(declare (type single-float x y))
declare type(single-float, x, y), optimize(debug(3))

transpiles to

(declare (type single-float x y)
         (optimize (debug 3)))

defclass

defclass point():
end

transpiles to

(defclass point nil nil)
defclass point():
  options:
    metaclass: standard-class;
  end
end

transpiles to

(defclass point nil nil (:metaclass standard-class))
defclass point():
  options:
    metaclass: standard-class;
    documentation: "A class for Points!";
  end
end

transpiles to

(defclass point nil nil (:metaclass standard-class)
          (:documentation "A class for Points!"))
defclass point():
  slots:
  end
end

transpiles to

(defclass point nil nil)
defclass point():
  slots:
    x;
    y;
  end
end

transpiles to

(defclass point nil ((x) (y)))
defclass point():
  slots:
    x:
      initform: 2.0,
      type: single-float,
      accessor: point-x;
  end
end

transpiles to

(defclass point nil ((x :initform 2.0 :type single-float :accessor point-x)))
defclass point():
  slots:
    x:
      initform: 2.0,
      type: single-float,
      accessor: point-x;
    y:
      initform: 2.0,
      type: single-float,
      accessor: point-y;
  end
end

transpiles to

(defclass point nil
          ((x :initform 2.0 :type single-float :accessor point-x)
           (y :initform 2.0 :type single-float :accessor point-y)))
defclass point():
  slots:
    x:
      initform: 2.0,
      type: single-float,
      accessor: point-x;
    y:
      initform: 2.0,
      type: single-float,
      accessor: point-y;
  end
  options:
    metaclass: standard-class;

    documentation: "Two dimensional points.";

  end
end

transpiles to

(defclass point nil
          ((x :initform 2.0 :type single-float :accessor point-x)
           (y :initform 2.0 :type single-float :accessor point-y))
          (:metaclass standard-class)
          (:documentation "Two dimensional points."))

defgeneric

defgeneric area(shape)

transpiles to

(defgeneric area
    (shape))

defmethod

defmethod our-identity(x): x end

transpiles to

(defmethod our-identity (x) x)
defmethod :before our-identity(x):
  format(t, "Returning identity~%")
end

transpiles to

(defmethod :before our-identity (x) (format t "Returning identity~%"))
defmethod :after our-identity(x):
  format(t, "Returned identity~%")
end

transpiles to

(defmethod :after our-identity (x) (format t "Returned identity~%"))
defmethod add (x :: number, y :: number):
 x + y
end

transpiles to

(defmethod add ((x number) (y number)) (+ x y))
defmethod add (x :: number, y :: number, &rest, others):
  x + if null(others):
    y
  else:
    apply(function(add), y, others)
  end
end

transpiles to

(defmethod add ((x number) (y number) &rest others)
  (+ x (cond ((null others) y) (t (apply #'add y others)))))
defmethod add (x :: number, y :: number, &rest, others):
  x + (if null(others):
    y
  else:
    apply(function(add), y, others)
  end)
end

transpiles to

(defmethod add ((x number) (y number) &rest others)
  (+ x (cond ((null others) y) (t (apply #'add y others)))))
defmethod add (x :: string, y):
  uiop:strcat(x, y)
end

transpiles to

(defmethod add ((x string) y) (uiop/utility:strcat x y))

defpackage

defpackage foo
  :use cl;
end

transpiles to

(defpackage foo
  (:use cl))

defparameter

defparameter a = 5

transpiles to

(defparameter a 5)

defstruct

defstruct foo:
  a;
  b;
end

transpiles to

(defstruct foo a b)
defstruct foo:
  (a = 4) :: number;
  b;
end

transpiles to

(defstruct foo (a 4 :type number) b)
defstruct foo:
  (a = 4), :read-only = t;
  b;
end

transpiles to

(defstruct foo (a 4 :read-only t) b)
defstruct foo:
  (a = 4), :read-only = t;
  (b = 2.0) :: single-float, :read-only = t;
end

transpiles to

(defstruct foo (a 4 :read-only t) (b 2.0 :type single-float :read-only t))
defstruct foo:
  a = 4;
  b = 2.0;
end

transpiles to

(defstruct foo (a 4) (b 2.0))

deftype

defun

defun our-identity(x): x end

transpiles to

(defun our-identity (x) x)
defun add (&rest, args):
 args
end defun

transpiles to

(defun add (&rest args) args)
defun add(args):
  if null(args):
    0
  else:
    first(args) + add(rest(args))
  end if
end

transpiles to

(defun add (args) (cond ((null args) 0) (t (+ (first args) (add (rest args))))))
defun foo(&optional, a = 5): a end

transpiles to

(defun foo (&optional (a 5)) a)

defvar

defvar a = 5

transpiles to

(defvar a 5)

for

for:for (i,j) in ((1,2),(3,4)):
  print(i + j)
end

transpiles to

(for-minimal:for (((i j) in (list (list 1 2) (list 3 4))))
  (print (+ i j)))
for:for i in (1,2,3), j in (2,3,4):
  print(i + j)
end

transpiles to

(for-minimal:for ((i in (list 1 2 3)) (j in (list 2 3 4)))
  (print (+ i j)))

if

if a: b end if

transpiles to

(cond (a b) (t nil))
if a:
  b; c
end

transpiles to

(cond (a b c) (t nil))
if a: b
else: c
end if

transpiles to

(cond (a b) (t c))
if a:
   b; d
else:
   c; e
end if

transpiles to

(cond (a b d) (t c e))
if a: b
elif c: d; e
else: f
end if

transpiles to

(cond (a b) (c d e) (t f))
(if a: b else: c; end)::boolean

transpiles to

(the boolean (cond (a b) (t c)))
if null(args): 0; else: 1 end

transpiles to

(cond ((null args) 0) (t 1))
if null(args):
    0
else:
    first(args)
end if

transpiles to

(cond ((null args) 0) (t (first args)))
if null(args):
  0
else:
  2 + 3
end if

transpiles to

(cond ((null args) 0) (t (+ 2 3)))
if null(args):
  0
else:
  first(args) + add(rest(args))
end if

transpiles to

(cond ((null args) 0) (t (+ (first args) (add (rest args)))))

ifelse

ifelse a 5

transpiles to

(if a
    5
    nil)
ifelse a :hello :bye

transpiles to

(if a
    :hello
    :bye)

in-package

labels

labels foo(x):
         bar(x - 1)
       end,
       bar(x):
         if (x < 0): nil else: foo(x - 1) end
       end:
  foo(42)
end

transpiles to

(labels ((foo (x)
           (bar (- x 1)))
         (bar (x)
           (cond ((< x 0) nil) (t (foo (- x 1))))))
  (foo 42))
labels foo(x):
         if (x < 0): nil else: foo(x - 1) end
       end:
  foo(42)
end

transpiles to

(labels ((foo (x)
           (cond ((< x 0) nil) (t (foo (- x 1))))))
  (foo 42))
labels :
  nil
end

transpiles to

(labels ()
  nil)

lambda

lambda (): nil end

transpiles to

(lambda () nil)
lambda (x):
  x
end

transpiles to

(lambda (x) x)
lambda (x, y):
  let sum = x + y:
    sum ^ 2
  end
end

transpiles to

(lambda (x y)
  (let ((sum (+ x y)))
    (expt sum 2)))

let

let a = 2, b = 3:
   a + b
end

transpiles to

(let ((a 2) (b 3))
  (+ a b))
let a = 2, b = 3:
   a + b
end let

transpiles to

(let ((a 2) (b 3))
  (+ a b))

let+

let-plus:let+ x = 42: x
end

transpiles to

(let+ ((x 42))
  x)
let-plus:let+ (a,b) = list(1,2):
  a + b
end

transpiles to

(let+ (((a b) (list 1 2)))
  (+ a b))
let-plus:let+ let-plus:&values(a,b) = list(1,2):
  a + b
end

transpiles to

(let+ (((&values a b) (list 1 2)))
  (+ a b))
let-plus:let+
  let-plus:&values(a,b) = list(1,2),
  (c,d,e) = list(1,2,3):
  {a,b,c,d,e}
end

transpiles to

(let+ (((&values a b) (list 1 2)) ((c d e) (list 1 2 3)))
  (fill-hash-set a b c d e))

lm

lm (): nil

transpiles to

(lambda () nil)
lm (x): x

transpiles to

(lambda (x) x)
lm (x, y): x + y

transpiles to

(lambda (x y) (+ x y))

loop

loop end loop

transpiles to

(loop)
loop :repeat n :do
  print("hello")
end

transpiles to

(loop :repeat n
      :do (print "hello"))
loop :for i :below n :do
  print(i + 1)
end

transpiles to

(loop :for i :below n
      :do (print (+ i 1)))

time

time length("hello world")

transpiles to

(time (length "hello world"))

with

Standard Common Lisp code uses a number of with- macros. These can all be generated using the with- special macro of Moonli. This is “special” because

  1. Unlike standard macros which are identified by symbols, the with-block is identified using the keyword “with”, regardless of the package under consideration.

  2. with pkg:symbol expands into pkg:with-symbol without the interning of pkg:symbol.

Example transpilations:

with open-file(f, "/tmp/a.txt"):
  f
end

transpiles to

(with-open-file (f "/tmp/a.txt") f)
with-open-file(f, "/tmp/a.txt"):
  f
end

transpiles to

(with-open-file (f "/tmp/a.txt") f)
with output-to-string(*standard-output*),
     open-file(f, "/tmp/a.txt"):
  write-line(read-line(f))
end

transpiles to

(with-output-to-string (*standard-output*)
  (with-open-file (f "/tmp/a.txt")
    (write-line (read-line f))))
with alexandria:gensyms(a,b,c):
  list(a,b,c)
end

transpiles to

(alexandria:with-gensyms (a b c)
  (list a b c))
with alexandria:gensyms(a,b,c),
     open-file(f, "/tmp/a.txt", :direction, :output):
  write(list(a,b,c), f)
end

transpiles to

(alexandria:with-gensyms (a b c)
  (with-open-file (f "/tmp/a.txt" :direction :output)
    (write (list a b c) f)))
defstruct pair:
  x;
  y;
end

with access:dot():
  let pair = make-pair(:x, 2, :y, 3):
     format(t, "~&x + y = ~a~%", pair.x + pair.y)
  end
end

transpiles to

(defstruct pair x y)

(access:with-dot
  (let ((pair (make-pair :x 2 :y 3)))
    (format t "~&x + y = ~a~%" (+ pair.x pair.y))))