Fennel

Yesterday, System Crafters did a live episode on Fennel, a Clojure-inspired Lisp that compiles to Lua. What fun!

Fennel is easy to install because it's just one file. Here's what I did on my Debian 12 box.

Lua

If you don't already have Lua installed, start with that. There isn't a Debian package called lua, there are several different versions. The latest is called lua5.4.

sudo apt install lua5.4

The resulting executable is called lua.

❯ which lua
/usr/bin/lua

❯ lua -v
Lua 5.4.4  Copyright (C) 1994-2022 Lua.org, PUC-Rio

We'll want lua-mode for Emacs.

M-x package-install RET lua-mode RET

There is a Lua Language Server too. We'll compile that from source.

sudo apt install ninja-build
git clone https://github.com/LuaLS/lua-language-server
cd lua-language-server
./make.sh

The Lua language server expects to find files in the current working directory, so instead of adding a symbolic link to it from a convenient bin directory in your path, make a wrapper script.

❯ cat ~/.local/bin/lua-language-server 
#!/bin/bash
exec "/home/tim/lua/lua-language-server/bin/lua-language-server" "$@"

In Emacs, eglot will find the Lua language server when invoked from a Lua file.

❯ cat ~/.emacs.d/config/programming/lua.el 
(add-hook 'lua-mode-hook 'eglot-ensure)

Now eglot will find it whenever we open a Lua file.

Fennel

Once we have Lua, Fennel is easy to install because it's just one additional file!

mkdir fennel
cd fennel
wget https://fennel-lang.org/downloads/fennel-1.4.0
chmod +x fennel-1.4.0

We can check the signature too.

wget https://fennel-lang.org/downloads/fennel-1.4.0.asc
curl https://technomancy.us/8F2C85FFC1EBC016A3B683DE8BD38C28CCFD2DA6.txt | gpg --import -
gpg --verify fennel-1.4.0.asc

Now create a symbolic link to it from a convenient bin directory in your path

ln -s ~/fennel/fennel-1.4.0 ~/.local/bin/fennel

If we start the Fennel REPL now, it suggests installing readline.lua from luarocks. But Debian provides readline.lua in package lua-readline

sudo apt install lua-readline

We may wish to install LuaRocks in the future, but this is fine for now. Running fennel gives us the REPL with GNU readline functionality.

❯ fennel
Welcome to Fennel 1.4.0 on PUC Lua 5.4!
Use ,help to see available commands.
>> (+ 1 2 3)
6

We can persist our history with a .fennelrc file.

❯ cat ~/.fennelrc
; persist repl history
(match package.loaded.readline
  rl   (rl.set_options {:histfile  "~/.fennel_history" ; default:"" (don't save)
                        :keeplines 10_000}))           ; default:1000

There's a fennel-mode for Emacs.

M-x package-install RET fennel-mode RET

But I'm not aware of language server for it.

Update: There is a Fennel Language Server! Thanks, @technomancy@hey.hagelb.org!

git clone https://git.sr.ht/~xerool/fennel-ls
cd fennel-ls
make
make install PREFIX=$HOME

Well, that was easy! Now configure Emacs to use it whenever we open a Fennel file. Also, since Fennel is a Lisp, we'll turn on parinfer for it.

❯ cat ~/.emacs.d/config/programming/fennel.el
(add-hook 'fennel-mode-hook 'parinfer-rust-mode)

(add-hook 'fennel-mode-hook 'eglot-ensure)

(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs '(fennel-mode . ("fennel-ls"))))

And away we go! Here's fizzbuzz in Fennel.

./fennel-ls.png

Mixing Fennel and Lua

Just as we can use any Java library from Clojure, we can use any Lua library from Fennel. In particular, there is a terrific Lua library called Lume. It has all kinds of things that didn't make it into the Lua standard library, but maybe should have.

(local lume (require :lib.lume))

(print (string.format "uuid: %s" (lume.uuid)))

The Lume README.md contains this lovely example, which gave me a chuckle (Oh hi mark).

(print (lume.format "{b} hi {a}" {:a "mark" :b "Oh"})) ;; Oh hi mark

I even created my own library by copying the pairsByKeys function from Programming in Lua and calling it from Fennel. Here is the Lua library file (cat lib/sorted.lua)

local sorted = {}

function sorted.bykeys (t, f)
   local a = {}
   for n in pairs(t) do table.insert(a, n) end
   table.sort(a, f)
   local i = 0      -- iterator variable
   local iter = function ()   -- iterator function
      i = i + 1
      if a[i] == nil then return nil
      else return a[i], t[a[i]]
      end
   end
   return iter
end

return sorted

and here is some Fennel that calls it (cat main.fnl)

#!/usr/bin/env fennel

(local sorted (require :lib.sorted))

(let [start-clock (os.clock)
      current-epoch (os.time)
      current-datestring (os.date)
      current-table (os.date "*t")]

  (print (string.format "\ncurrent epoch: %s" current-epoch))

  (print (string.format "\ncurrent date: %s" current-datestring))
  (each [k v (sorted.bykeys current-table)]
     (print (string.format "  %s: %s" k v)))
  
  (print (string.format "\nelapsed time: %.5f\n" (- (os.clock) start-clock))))

Running it gives

❯ ./main.fnl 

current epoch: 1704562499

current date: Sat Jan  6 12:34:59 2024
  day: 6
  hour: 12
  isdst: false
  min: 34
  month: 1
  sec: 59
  wday: 7
  yday: 6
  year: 2024

elapsed time: 0.00006

Note that the date components (day, hour, &c.) are in lexicographical order. If we had iterated over pairs instead of sorted.bykeys, they would have come out in more or less random order.

Update: I just noticed Hugo isn't rendering the colors for fennel-mode on gitlab. It looks fine locally. Here is a screenshot of my Emacs frame.

./emacs.png