An experiment of error reasoning in calcit-js

For calcit-js, read Introducing calcit-js: toy language inspired by cljs .

It might be a pain in Lisps to figure out why an error was produced since Lisp is dynamic meanwhile deeply nested. And sometimes the errors are behind macros and macros, which makes it even harder.

Calcit itself is a dialect of Lisp with macro so it also suffered problemed in error reasoning. I suffered that a lot while I was building Calcit, such many strange errors. So for my own usages, I made a simple tool for reasoning errors, by capturing the whole stack.

Usages

it prints the stack when an error is produced:

failed, invalid types for &+: 1 ({} (:a 1))

call stack:
  app.main/&+
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib
  app.main/if
  app.main/fib

run `cat .calcit-error.cirru` to read stack details.

failed to reload, invalid types for &+: 1 ({} (:a 1))

besides, it emits a .calcit-error.cirru file containing the informations about the error:

{}
  :stack $ []
    {} (:code nil) (:def |app.main/&+)
      :args $ [] 1
        {} $ :a 1
      :kind :proc
    {}
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :kind :syntax
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :def |app.main/if
    {} (:def |app.main/fib) (:kind :fn)
      :args $ [] 1
      :code $ quote
        fib $ - n 1
    {} (:def |app.main/if)
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :kind :syntax
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
    {} (:kind :fn)
      :args $ [] 2
      :def |app.main/fib
      :code $ quote
        fib $ - n 1
    {}
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :def |app.main/if
      :kind :syntax
    {} (:def |app.main/fib)
      :code $ quote
        fib $ - n 1
      :kind :fn
      :args $ [] 3
    {} (:kind :syntax)
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :def |app.main/if
    {} (:def |app.main/fib) (:kind :fn)
      :code $ quote
        fib $ - n 1
      :args $ [] 4
    {}
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :kind :syntax
      :def |app.main/if
    {}
      :args $ [] 5
      :def |app.main/fib
      :code $ quote
        fib $ - n 1
      :kind :fn
    {} (:def |app.main/if)
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :kind :syntax
    {} (:def |app.main/fib)
      :code $ quote
        fib $ - n 1
      :kind :fn
      :args $ [] 6
    {}
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :def |app.main/if
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :kind :syntax
    {} (:kind :fn)
      :args $ [] 7
      :code $ quote
        fib $ - n 1
      :def |app.main/fib
    {}
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :kind :syntax
      :def |app.main/if
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
    {}
      :args $ [] 8
      :code $ quote
        fib $ - n 1
      :kind :fn
      :def |app.main/fib
    {}
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :def |app.main/if
      :kind :syntax
    {} (:def |app.main/fib)
      :code $ quote
        fib $ - n 1
      :args $ [] 9
      :kind :fn
    {} (:kind :syntax) (:def |app.main/if)
      :code $ quote
        if (< n 2)
          &+ 1 $ &{} :a 1
          +
            fib $ - n 1
            fib $ - n 2
      :args $ [] ([] '< 'n 2)
        [] '&+ 1 $ [] '&{} :a 1
        [] '+
          [] 'fib $ [] '- 'n 1
          [] 'fib $ [] '- 'n 2
    {} (:def |app.main/fib) (:kind :fn)
      :code $ quote (fib 10)
      :args $ [] 10
  :message "|invalid types for &+: 1 ({} (:a 1))"

plus that, I built a simple tool to display the error:

or…

In this way, all the details related to evaluation of this error are displayed and it become a lot easy to figure out why it was thrown.

and for macros, Calcit also captures the syntax tree nodes passed to the macro, we can still get informations:

if you are really interested, I recorded a video then you can see how I was using this tool:

thoughts

This is more like an experimenting tool. Calcit is a very simplified Lisp dialect, more like a JavaScript generator with macros. It runs slowly by interpreting the syntax tree. It might not be possible to do this if I was building a bytecode interpreter. And there also performance penalties I didn’t measured, which makes it not a nice solution for large programs.

I’m not sure if this solution will help improving Clojure error messages. But such a tool is still light-weighted compared a attaching to a real debugger.