ketos
ketos copied to clipboard
Line and column numbers for compile and execution errors
Currently ketos outputs the line and column number for parse errors, but not for compile errors. This makes debugging very painful, because the location of the error has to be guessed or tediously reconstructed by commenting out code and recompiling. It would be a big improvement to get the line and column number of the error.
The same applies for execution errors.
I agree this is a problem. However, implementing a solution is non-trivial. The reason for this is that, while most languages use some AST type to represent source code, Ketos instead uses (as most Lisp dialects tend to do, I think) the very same representation of runtime values. Therefore, while the parser has line:column information that it receives from the lexer, this information is lost when the parser produces Value
instances to represent the source code.
For runtime errors, an adequate (in my view) remedy is to use stack traces. I haven't implemented this yet because I'm not sure how it should fit into the Error
type, but it will be a more straightforward process once that decision is made. Perhaps something similar could be done for compile-time errors.
Other lisp implementations seem to be able to provide line and column numbers. Maybe they store the additional data separately from the expression tree?
I'm seeing about implementing this now. Can you give me an example of a Lisp dialect with line/column numbers in error messages? Hopefully, an open source implementation?
Racket does have column and line numbers for all errors, and it is open source.
I've decided against attempting to integrate line/column numbers into runtime and compile-time errors for now. I just don't see any way of getting it done without making a mess of things. Things are already a bit messy.
Instead, I've implemented a type of stack trace that is used for both runtime and compile-time errors (because runtime and compile-time are somewhat nebulous concepts in Ketos; each can lead into the other). This will likely see some improvements in the future, perhaps including an expression for context. Feedback is welcome.
I'll leave this issue open for any further discussion about either of these error reporting mechanisms.
The traces are not optimal, see this example:
(define (fizzbuzz a b)
(if (<= a b)
(do
(let ((divisible-by-3 (= (rem a 3) 0))
(divisible-by-5 (= (rem a 5) 0)))
(cond
((and divisible-by-3 divisible-by-5)
(println "FizzBuzz"))
(divisible-by-3
(4 println "Fizz"))
(divisible-by-5
(println "Buzz"))
(else
(println "~a" a))))
(fizzbuzz (+ a 1) b))))
(fizzbuzz 1 100)
This fails to compile:
Traceback:
In main, define fizzbuzz
In main, operator if
In main, operator do
In main, operator let
In main, operator cond
compile error: invalid call expression of type `integer`
Why is divisible-by-3
missing from the stack trace?
Because divisible-by-3
is an expression. The trace contains only names of operators and function calls, not expressions for cond
branches. It would need to store an entire Value
in order to handle any possible expression.
Traces could definitely benefit from having at least one expression Value
to more clearly show where the error is. The question is, should only the expression that generated the error be present or should there be an abbreviated chunk of an expression at each step?
The final expression would be helpful, because you could search for it in your editor. Not sure about the expressions at other steps. It sounds potentially helpful. How are other Lisp implementations doing it?
Final expression and trace are working out pretty well. I don't think I want to clutter things up by storing every expression as the compiler goes along.
The other part of the issue is for runtime errors. It's not currently an option to print code expressions for runtime errors, but some types of errors could print the offending values. For example, (let ((a "foo")) (+ a))
currently shows expected number; found string
, but could instead show expected number; found string: "foo"
.
I decided to implement the above plan for runtime type errors. I hope it doesn't bloat Error
/ExecError
instances too much. I'm not sure how much of a problem that could turn out to be, but I suspect it won't be a huge problem. Reasonable programs will probably not have a great demand for producing many hundreds of thousands of errors per second, anyway.
I've pushed the above mentioned changes into the repo.
Nice, this is very useful!
I was thinking about this issue for a bit. Shouldn't it be easy to reconstruct the line and column numbers given the expression and the source file? If trace had a bit more information (that is the number of the argument), it would be possible to unambiguously reconstruct the position in the file by hand, so it should be relatively easy to do so by parsing the source file again.
With the example above, enriched by the argument numbers:
Traceback:
In main, define fizzbuzz, argument 2
In main, operator if, argument 1
In main, operator do, argument 1
In main, operator let, argument 2
In main, operator cond, argument 2
(4 println "Fizz")
compile error: invalid call expression of type `integer`
By looking at the source file, this implies the offending expression is at line 10, column 13.
(I assume the compiler does not rearrange expressions.)
Macros might be a problem though.
I don't see any reason why that would be impossible, but I'm not sure if it's worth the trouble. Traces are a general error-reporting mechanism and this method of source searching would only apply to a subset of compile-time errors. Runtime execution would not have the information to express which of their arguments had caused an error.
Clojure keeps track of row/col position for stack traces. It does this with its LispReader
Java class file that essentially tokenizes on whitespace and dispatches to other readers based upon chars/regex matches ... since it's pretty much going character-by-character, it's able to track the position ... I've been using this to explore doing something similar in a Go Lisp.
See:
- https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/LispReader.java
- https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/LineNumberingPushbackReader.java
- https://github.com/clojure/clojure/blob/master/src/clj/clojure/main.clj#L368
- https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Compiler.java