Category talk:Wren-debug

From Rosetta Code

Debugging

At present debugging a Wren script consists of strategically placing one or more System.print statements in your code and examining the output. This tends to be a tedious approach since, if you want to (say) print out the values of two variables and label them, you need to do something like this:

var myvar1 = 42
var myvar2 = "forty two"
System.print(["myvar1 = ", myvar, "myvar2 = ", myvar2])

which produces the following output:

[myvar1 = , 42, myvar2 = , forty two]

A list needs to be used since System.print only takes a single argument. You also have the problem of locating and removing debugging code in your 'release' version as there is no obvious way to differentiate such code from 'normal' code.

Now there is ongoing work on creating a debugger for Wren though the current indications are that this is not going to be ready for use any time soon. Whilst there are obvious limitations in what can be done solely from Wren code (as we don't have reflection either), the aim of this module is to try and provide a more structured approach to debugging with the tools we do have. The above code for example could be replaced by this:

import "./debug" for Debug

var myvar1 = 42
var myvar2 = "forty two"
Debug.print("myvar1|myvar2", 5, myvar1, myvar2)

and would produce the following output with each expression's value being automatically labelled, typed and printed on separate lines:

EXPR on line 5 of type Int    : myvar1 = 42
EXPR on line 5 of type String : myvar2 = forty two

Moreover, debugging can be turned off (it would be 'on' by default when you imported the module) or re-enabled using the following simple statements:

import "./debug" for Debug
//....
Debug.off
//....
Debug.on

As all methods are static and contained within the Debug class, it is relatively easy to locate debugging code by simply searching for Debug. in your editor and, if you want to leave it in the release version in case subsequent problems surface, you can simply turn debugging off with very little runtime penalty.

Now, as RC task solutions are (hopefully) fully debugged versions, this module is clearly going to see little use on this site. However, as I've been finding it useful in my own efforts, I thought I would share it with others who are interested in writing and testing Wren code but are frustrated with the current lack of support in this area.

Source code

/* Module "debug.wren" */

import "./check" for Check

/*
    Debug is a class containing static methods which may be useful for debugging purposes.
    Apart from 'on' and 'off, all other methods do nothing if debugging is turned off.
    All debugging output starts with or is preceded by an upper case string to label it as such.
*/
class Debug {
    // Turns debugging on.
    static on  { __on = true }

    // Turns debugging off.
    static off { __on = false }

    // Prints a message to indicate that module 'm' is being debugged.
    static module(m) {
        if (__on) System.print("DEBUGGING %(m)")
    }

    // Prints a new line.
    static nl {
        if (__on) System.print("NL")
    }

    // Prints a comment.
    static comment(text) {
        if (__on) System.print("COMMENT %(text)")
    }

    // Private helper method for the various 'print' methods.
    static print_(labelStr, line, args) {
        if (!__on) return
        Check.str("labelStr", labelStr)
        Check.nonNegInt("line", line)
        var labels = labelStr.split("|")
        var ac = args.count
        while (labels.count < ac) labels.add("?")
        var lMaxLen = 0
        for (i in 0...ac) {
            labels[i] = labels[i].trim()
            if (labels[i] == "") labels[i] = "?"
            var lc = labels[i].count
            if (lc > lMaxLen) lMaxLen = lc
        }
        var tMaxLen = 0
        var types = List.filled(ac, null)
        for (i in 0...ac) {
            var arg = args[i]
            types[i] = arg.type.name
            if ((arg is Num) && arg.isInteger) types[i] = "Int"
            var tc = types[i].count
            if (tc > tMaxLen) tMaxLen = tc
        }
        for (i in 0...ac) {
            var arg = args[i]
            var label = labels[i]
            var lc = label.count
            if (lc < lMaxLen) {
                var spaces = " " * (lMaxLen - lc)
                label = spaces + label
            }
            var type = types[i]
            var tc = type.count
            if (tc < tMaxLen) {
                var spaces = " " * (tMaxLen - tc)
                type = type + spaces
            }
            System.print("EXPR on line %(line) of type %(type) : %(label) = %(arg)")
        }
    }

    // Prints the types and values of each argument (there can be up to 6) on separate lines.
    // 'line' indicates the line number. You can pass 0 if you don't know or care about the line number.
    // A label is attached to each argument in order by splitting 'lineStr' on the vertical bar character '|'.
    // If there are insufficient labels, '?' is used for the missing ones.
    // If there are too many labels, excess labels are ignored.
    static print(labelStr, line, arg1)                               { print_(labelStr, line, [arg1]) }
    static print(labelStr, line, arg1, arg2)                         { print_(labelStr, line, [arg1, arg2]) }
    static print(labelStr, line, arg1, arg2, arg3)                   { print_(labelStr, line, [arg1, arg2, arg3]) }
    static print(labelStr, line, arg1, arg2, arg3, arg4)             { print_(labelStr, line, [arg1, arg2, arg3, arg4]) }
    static print(labelStr, line, arg1, arg2, arg3, arg4, arg5)       { print_(labelStr, line, [arg1, arg2, arg3, arg4, arg5]) }
    static print(labelStr, line, arg1, arg2, arg3, arg4, arg5, arg6) { print_(labelStr, line, [arg1, arg2, arg3, arg4, arg5, arg6]) }

    // As print except the arguments are passed as a list and so there can be more than 6 of them.
    static lprint(labelStr, line, args)                              { print_(labelStr, line, args) }

    // Checks that 'arg' is true or truthy (not null or false). If it is, it does nothing.
    // If it isn't, it issues a message showing the label ('?' if empty) and line number (0 acceptable).
    // It then aborts the fiber.
    static assert(label, line, arg) {
        if (!__on) return
        Check.str("label", label)
        Check.nonNegInt("line", line)
        if (!arg) {
            label = label.trim()
            if (label == "") label = "?"
            System.print("ASSERTION on line %(line) labelled '%(label)' failed. Aborting fiber.")
            Fiber.abort("Assertion failure.")
        }
    }

    // Aborts the fiber without any mesage.
    static abort() { __on ? Fiber.abort("") : null }

    // Aborts the fiber with message 'msg'.
    static abort(msg) { __on ? Fiber.abort("%(msg)") : null }

    // Runs a function 'fn' which takes no arguments.
    // Any output or errors are handled normally.
    // If the function ends without a fatal error, this is indicated.
    static run(fn) {
        if (!__on) return
        Check.func("fn", fn, 0)
        System.print("RUNNING function()")
        fn.call()
        System.print("ENDED function")
    }

    // Runs a function 'fn' which takes a single argument 'arg'.
    // Otherwise behaves the same as run(fn).
    static run(arg, fn) {
        if (!__on) return
        Check.func("fn", fn, 1)
        System.print("RUNNING function(%(arg))")
        fn.call(arg)
        System.print("ENDED function")
    }

    // Tries to run a function 'fn' which takes no arguments in a new fiber.
    // Any error which occurs is captured and printed.
    // Any other output is handled normally.
    // An indication is given when the function ends.
    static try(fn) {
        if (!__on) return
        Check.func("fn", fn, 0)
        System.print("TRYING function()")
        var fib = Fiber.new(fn)
        var err = fib.try()
        if (err) System.print("ERROR %(err)")
        System.print("ENDED function")
    }

    // Tries to run a function 'fn' which takes a single argument 'arg' in a new fiber.
    // Otherwise behaves the same as try(fn).
    static try(arg, fn) {
        if (!__on) return
        Check.func("fn", fn, 1)
        System.print("TRYING function(%(arg))")
        var fib = Fiber.new(fn)
        var err = fib.try(arg)
        if (err) System.print("ERROR %(err)")
        System.print("ENDED function")
    }
}

Debug.on