ketos icon indicating copy to clipboard operation
ketos copied to clipboard

Line and column numbers for compile and execution errors

Open vks opened this issue 8 years ago • 14 comments

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.

vks avatar Feb 24 '16 11:02 vks

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.

murarth avatar Feb 25 '16 00:02 murarth

Other lisp implementations seem to be able to provide line and column numbers. Maybe they store the additional data separately from the expression tree?

vks avatar Feb 25 '16 08:02 vks

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?

murarth avatar Mar 01 '16 02:03 murarth

Racket does have column and line numbers for all errors, and it is open source.

vks avatar Mar 01 '16 09:03 vks

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.

murarth avatar Mar 02 '16 00:03 murarth

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?

vks avatar Mar 02 '16 10:03 vks

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?

murarth avatar Mar 02 '16 17:03 murarth

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?

vks avatar Mar 02 '16 22:03 vks

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".

murarth avatar Mar 02 '16 22:03 murarth

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.

murarth avatar Mar 03 '16 02:03 murarth

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.)

vks avatar Mar 03 '16 11:03 vks

Macros might be a problem though.

vks avatar Mar 03 '16 14:03 vks

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.

murarth avatar Mar 03 '16 19:03 murarth

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

oubiwann avatar May 22 '19 17:05 oubiwann