aboutsummaryrefslogtreecommitdiff
const tracing = (() => {
    const log = [];
    const objects = new Map();

    let call_stack_top = null;

    function record_value(log_entry, name, value, as_array=false) {
        log_entry[name] = value;

        for (const _value of as_array? value : [value]) {
            if (_value !== null && _value !== undefined
                && _value !== false && _value !== true) {
                const relevant_log_entries = objects.get(_value) || [];

                relevant_log_entries.push(log_entry);
                objects.set(_value, relevant_log_entries);
            }
        }

        return value;
    }

    function with_log_entry(op_name, line, column, cb) {
        const log_entry = {
            op_name, line, column,
            id: log.length,
            parent_call: call_stack_top
        };
        const saved_stack_top = call_stack_top;

        log.push(log_entry);

        call_stack_top = log_entry;

        try {
            return cb(log_entry);
        } catch(ex) {
            record_value(log_entry, "error", ex);

            throw ex;
        } finally {
            call_stack_top = saved_stack_top;
        }
    }

    return {
        get_objects: () => objects,

        get_log: () => log,

        record_binary: function(line, column, operation_name, operation,
                                left_producer, right_producer) {
            function go(log_entry) {
                const left = record_value(log_entry, "left", left_producer());

                const right = record_value(log_entry,
                                           "right",
                                           right_producer());

                const result = operation(left, right);

                return record_value(log_entry, "result", result);
            }

            return with_log_entry(operation_name, line, column, go);
        },

        record_lazy_binary: function(line, column, operation_name, operation,
                                     left_producer, right_producer) {
            function go(log_entry) {
                const left = record_value(log_entry, "left", left_producer());

                const result = operation(left, right_producer);

                return record_value(log_entry, "result", result);
            }

            return with_log_entry(operation_name, line, column, go);
        },

        record_call: function(line, column, function_producer, optional,
                              args_producer) {
            function go(log_entry) {
                const function_object = record_value(log_entry,
                                                     "function_object",
                                                     function_producer());

                const record_args = () => record_value(log_entry,
                                                       "args",
                                                       args_producer(),
                                                       true);

                const result = optional ?
                      function_object?.(...record_args()) :
                      function_object(...record_args());

                return record_value(log_entry, "result", result);
            }

            return with_log_entry("call", line, column, go);
        },

        record_prop_call: function(line, column, this_producer,
                                   property_optional, property_producer,
                                   optional, args_producer) {
            function go(log_entry) {
                const bound_this = record_value(log_entry,
                                                "bound_this",
                                                this_producer());

                const record_property = () => record_value(
                    log_entry, "property", property_producer()
                );

                const function_object =
                      record_value(log_entry,
                                   "function_object",
                                   property_optional ?
                                   bound_this?.[record_property()] :
                                   bound_this[record_property()]);

                if ((function_object === null ||
                     function_object === undefined) &&
                    optional)
                    return undefined;

                const record_args = () => record_value(log_entry,
                                                       "args",
                                                       args_producer(),
                                                       true);

                const result = Function.apply.call(function_object,
                                                   bound_this,
                                                   record_args());

                return record_value(log_entry, "result", result);
            }

            return with_log_entry(
                `obj${property_optional}[prop]${optional}() call`,
                line, column,
                go
            );
        }
    };
})();