A new language

Over the weekend I created a new programming language with functional programming elements, objects, and macros.

Language

The language is expression based.

Expressions and functions

Let’s look at some code. To define a function, use the def function.

def(my_plus (a b)
  {a + b}
)

To call it, add the arguments in brackets.

my_plus(3 2)
>>> 5

Here is a function that operate on lists.

def(my_function (c)
  {c[0] + c[1]}
)
my_function(list(1 2 3))
>>> 3

Loops

Loops are done with loop:

def(fizzbuzz ()
  loop(for i from 0 to 100 do 
    switch(
      {{{i mod 3} = 0} and {{i mod 5} = 0}}
        print("FizzBuzz")
      {{i mod 3} = 0} 
        print("Fizz")
      {{i mod 5} = 0} 
        print("Buzz")
      otherwise print(i)
    )
  )
)

fizzbuzz()

Note that white-space is mostly not significant.

Hash-maps

A dictionary (hash-table) can be created with dict.

def(run ()
  let((h)
    {h := dict()}
    {h[3] := 10}
    print(h[3])
  )
)
run()
>>> 10

Note that we introduce a new variable with the let keyword.

Macros

Macros are defined with macro:

macro(my_log (f) 
  print(f)
)
my_log(print(1))
>>> (print 1)
>>> 1

Classes and objects

Classes are defined with class:

class(point 
  (x y z)
)

An instance of this class is created with new:

{v := new('point)}
{v['x] := 1}
v['x]
>>> 1

Methods with method:

method(reset_point ((obj point))
    {obj[x] := 0}
)
reset_point(v)

Which means that the method specializes obj of type point.

Implementation

So this is of course not really a new language. You might have noticed a striking similarity to … common lisp!

Basically I’ve gone through the noob lisp rite-of-passage by playing with custom lisp syntax. I can recommend this method to quickly learn more about lisp!

The first change is to implement function call syntax. There are multiple packages for this – I used neoteric-expressions from the readable project.

(ql:quickload "readable")
(readable:enable-neoteric)

This also gives us infix notation within curly braces, e.g. {x + 1} is actually read as (+ x 1). (readable also allows white-space based syntax, but somehow I enjoyed playing with that less – just function-call and infix already makes things much more familiar.)

Now define and rename our own “keywords” for function and macro definitions:

defmacro(def (&rest p)
  `eval-when(
    :compile-toplevel(:load-toplevel :execute)
    defun(,@p)
  )
)
defmacro(macro (&rest p)
  `defmacro(,@p)
)

The readable library also allows us to create a custom macro for interpreting square brackets by implementing $bracket-apply$. At run time, inspect the type of object outside the brackets and call the appropriate access method. (Various disadvantages to this being run-time, but it works.)

def(access (e index)
  cond(((eq type-of(e) 'cons)
        (nth car(index) e))
       ((eq type-of(e) 'hash-table)
        (gethash car(index) e))
       (t (slot-value e car(index)))
  )
)

def((setf access) (new-value e index)
  cond(((eq type-of(e) 'cons)
       (setf nth(car(index) e) new-value))
      ((eq type-of(e) 'hash-table)
       (setf gethash(car(index) e) new-value))
      (t (setf (slot-value e car(index)) new-value))
  )
)

macro(readable:$bracket-apply$ (e &rest index)
  `access(,e ',index)
)
(readable:enable-neoteric)

I’ve also experimented with custom infix assignment, and shortened class, object creation, and method names:

setf(macro-function(':=) macro-function('setf))
setf(fdefinition('dict) #'make-hash-table)
setf(fdefinition('class) #'defclass)
setf(fdefinition('new) #'make-instance)
setf(fdefinition('method) #'defmethod)

Lastly, let’s create an alternative to cond that uses less parentheses:

def(group-pairwise (list)
  loop(:for (a b) :on list :by #'cddr :while b 
    :collect list(a b)
  )
)

macro(switch (&rest args)
  let((v)
    {v := group-pairwise(args)}
    cons('cond v)
  )
)

Conclusion

I’m also indenting in a style that is more familiar to someone coming from languages with algol-style syntax.

Advantages of what is here:

  • The syntax is more familiar to most people coming from most other languages.
  • Arguably easier to read.
  • Can easily mix both styles in the same file.

But after spending about 10 minutes writing lisp I got used to the syntax. I quickly started to see why more experienced lispers can’t understand why anyone would be put off with the syntax – it quickly fades into the background.

Disadvantages:

  • Macros do not directly map to the language – although I was surprised at how easily it still mapped.
  • Biggest disadvantage for me was the editor couldn’t send top-level definitions to the repl if it started as def(...) rather than (def ...). So while developing I mostly kept the outside braces.
  • Also the editor (I was using SLIME) indented completely differently than what I wanted in this case, so lots of manual re-indenting afterwards.
  • The infix assignment (e.g. {v := 3}) looks awkward.
  • The normal lisp indenting is nice and compact – no reason to double the lines of code by closing braces on their own lines.
Written on November 5, 2022