tcl

How to get line number and filename locations for errors that occur while using interp eval?


In case it is relevant, I am observing this behavior in Tcl 8.6.13.

Normally, errors in Tcl include line numbers and filenames when applicable.

However, I'm finding that when errors occur in scripts executed with interp eval, I do not get this information.

The two examples below are exactly the same, except that Example 1 evaluates the code in the main/parent interpreter, while Example 2 evaluates it in the child interpreter.

Example 1

#!/bin/sh    
# This line continues for Tcl, but is a single line for 'sh' \    
exec tclsh "$0" ${1+"$@"}    
    
::safe::interpCreate i    
eval expr {"a" + 1}

Output

./example.tcl 
invalid bareword "a"
in expression "a + 1";
should be "$a" or "{a}" or "a(...)" or ...
    (parsing expression "a + 1")
    invoked from within
"expr "a" + 1"
    ("eval" body line 1)
    invoked from within
"eval expr {"a" + 1}"
    (file "./example.tcl" line 6)

Example 2

#!/bin/sh    
# This line continues for Tcl, but is a single line for 'sh' \    
exec tclsh "$0" ${1+"$@"}    
    
::safe::interpCreate i    
i eval expr {"a" + 1}

Output

./example.tcl 
invalid bareword "a"
in expression "a + 1";
should be "$a" or "{a}" or "a(...)" or ...
    (parsing expression "a + 1")
    invoked from within
"expr "a" + 1"
    invoked from within
"i eval expr {"a" + 1}"
    (file "./example.tcl" line 6)

The error messages are nearly the same, except one line is missing in Example 2's output: ("eval" body line 1)

In this example, missing that part of the error message is not a problem, since there is only one line of code being evaluated; if it were a large script, or if the error occurred when source'ing a file, that might be a different story.

This behavior seems weird; partially because because it is inconsistent, but also because the child interpreter must know which code it is executing, so it should be able to report the line numbers of errors in that code; also, when sourceing a file, it should know the file it is reading the code from, since the source command was invoked from the child.

So is there any way to get line and file information when using interp eval? Alternatively, is there a way to write this code differently that could provide better error messages in scripts run in child interpreters?

Some additional examples (their output still misses the same line):

Example 3 (passing code to child interpreter as a single argument).

#!/bin/sh    
# This line continues for Tcl, but is a single line for 'sh' \    
exec tclsh "$0" ${1+"$@"}    
    
::safe::interpCreate i    
i eval {expr {"a" + 1}}

Output

./example.tcl 
can't use non-numeric string as operand of "+"
    invoked from within
"expr {"a" + 1}"
    invoked from within
"i eval {expr {"a" + 1}}"
    (file "./example.tcl" line 6)

Example 4 (passing code to child interpreter as a single argument in a list).

#!/bin/sh    
# This line continues for Tcl, but is a single line for 'sh' \    
exec tclsh "$0" ${1+"$@"}    
    
::safe::interpCreate i    
i eval [list expr {"a" + 1}]

Output

./example.tcl 
can't use non-numeric string as operand of "+"
    while executing
"expr {"a" + 1}"
    invoked from within
"i eval [list expr {"a" + 1}]"
    (file "./example.tcl" line 6)

Example 5 (passing code to child interpreter as a single argument built from lists).

#!/bin/sh    
# This line continues for Tcl, but is a single line for 'sh' \    
exec tclsh "$0" ${1+"$@"}    
    
::safe::interpCreate i    
i eval [list expr [list "a" + 1]]

Output

./example.tcl 
invalid bareword "a"
in expression "a + 1";
should be "$a" or "{a}" or "a(...)" or ...
    (parsing expression "a + 1")
    invoked from within
"expr {a + 1}"
    invoked from within
"i eval [list expr [list "a" + 1]]"
    (file "./example.tcl" line 6)

I also tried Examples 3, 4 and 5 with normal eval, i.e. in the main/parent interpreter. In those cases, they produced the same output as when run with the child interpreter, except they included the missing line.


Solution

  • This is fully expected behavior, in my opinion, because you has basically the line where exactly in file the error happens, so jump to the line and see it. And if you'd catch it inside or outside the interpreter, you'd also get the full info there in the -errorinfo of the result dict. The frame info collected in a backtrace (call-stack) is basically provided from every frame (file, namespace, eval/catch, proc/apply, etc).

    it creates difficulties in the context of implementing something like a plugin system that interp evals scripts, as the writers of the plugin scripts would receive less useful information than might be desired.

    Well normally nobody (even not plugin interface writers) developing everything at global level, one would rather use namespaces/procs, which then would provide the line number relative the proc.

    is there a way to write this code differently that could provide better error messages in scripts run in child interpreters?

    Sure, if you need the info of the code evaluating in child-interp only, here you go:

    #!/usr/bin/env tclsh
    
    interp create -safe i
    
    set code {
      # multi-line code
      # more comments
      expr {1/0}; # this is line 4 in code
    }
    
    if { [ i eval [list catch $code res opt] ] } {
      lassign [i eval {list $res $opt}] res opt
      puts stderr "ERROR: Plug-in code failed in line [dict get $opt -errorline]:\n[dict get $opt -errorinfo]
        while executing plug-in\n\"code-content [list [string range $code 0 255]...]\"
       (\"eval\" body line [dict get $opt -errorline])"
    }
    
    interp delete i
    

    Output:

    $ example.tcl
    ERROR: Plug-in code failed in line 4:
    divide by zero
        invoked from within
    "expr {1/0}"
        while executing plug-in
    "code-content {
      # multi-line code
      # more comments
      expr {1/0}; # this is line 4 in code
    ...}"
       ("eval" body line 4)   <-- line in code
    

    If you rather don't want catch the error in the code, but rather rethrow it (e. g. from proc evaluating some safe code), it is also very simple:

    #!/usr/bin/env tclsh
    
    interp create -safe i
    
    proc safe_eval {code} {
      if { [ i eval [list catch $code res opt] ] } {
        lassign [i eval {list $res $opt}] res opt
        # add the line (info of catch) into stack trace:
        dict append opt -errorinfo "\n    (\"eval\" body line [dict get $opt -errorline])"
        return {*}$opt -level 2 $res
      }
    }
    
    safe_eval {
      # multi-line code
      # more comments
      expr {1/0}; # this is line 4 in code
    }
    
    interp delete i
    

    Output:

    $ example.tcl
    divide by zero
        invoked from within
    "expr {1/0}"
        ("eval" body line 4)   <-- line in code
        invoked from within
    "safe_eval {
      # multi-line code
      # more comments
      expr {1/0}; # this is line 4 in code
    }"
        (file "example.tcl" line 14)