• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] GetStackTrace

Status
Not open for further replies.
Level 24
Joined
Jun 26, 2020
Messages
1,850
(There is an improved version of this system in the Eikonium's Debug Utils)

I discussed this system in the Advanced Scripting forum, since there is a while I got a feedback, I think is time to share it here.

This adds the function GetStackTrace() that is something like a replacement of a function that returns the stack trace and is missed in W3.
Lua:
do
    local prefixes = {"war3map.lua", "blizzard.j.lua"}
    local n = #prefixes
    local prefixesLen = {}
    for i = 1, n do
        prefixesLen[i] = string.len(prefixes[i])
    end
    local list = {}
    local lastMsg = nil

    local function getPos(msg, pos)
        error(msg, pos)
    end

    local function store(msg)
        lastMsg = msg
    end

    local function checkPrefixes()
        for i = 1, n do
            if string.sub(lastMsg, 1, prefixesLen[i]) == prefixes[i] then
                return true
            end
        end
        return false
    end

    ---Returns stack trace, but only the position of the called functions, not its names
    ---@return string
    function GetStackTrace()
        local stack = ""

        local i = 4
        local p = 1
        while true do
            xpcall(getPos, store, "- " .. p, i)
            if not checkPrefixes() then break end
            table.insert(list, lastMsg)
            i = i + 1
            p = p + 1
        end

        for j = #list, 1, -1 do
            stack = stack .. list[j] .. "\n"
        end

        list = {}

        return stack
    end
end

Example:

Let's imagine this is all the war3map.lua file:
Lua:
local function Fourth()     -- 1
    print(GetStackTrace())  -- 2
end                         -- 3
                            -- 4
local function Third()      -- 5
    Fourth()                -- 6
end                         -- 7
                            -- 8
local function Second()     -- 9
    Third()                 -- 10
end                         -- 11
                            -- 12
local function First()      -- 13
    Second()                -- 14
end                         -- 15
                            -- 16
First()                     -- 17
So the screen would show:
Code:
war3map.lua:2: - 5
war3map.lua:6: - 4
war3map.lua:10: - 3
war3map.lua:14: - 2
war3map.lua:17: - 1
Which are the positions of the functions that called the GetStackTrace function, you can use this to handle errors, but it has its limits:
  • You need to know where the error will happen and before of that call the GetStackTrace function.
  • Maybe you can overwrite the error function (that would include edit this system), but there would left the errors provoked by other things, like indexing a nil value or aritmetic operations with string values.
 
Last edited:
Level 24
Joined
Jun 26, 2020
Messages
1,850
Could you provide some example code? How would I use GetStackTrace in practice?
Well, first obviously you need the war3map.lua file to know where is positioned the function calls.
I was having a problem (and still) here with the Lua Damage Engine 2.0:
Lua:
local breakCheck = {}   ---@type function[]
local override          ---@type boolean

breakCheck[_DAMAGING]   = function() return override or current.userType == _TYPE_PURE end
breakCheck[_ARMOR]      = function() return current.damage <= 0.00 end
breakCheck[_LETHAL]     = function() return hasLethal and Damage.life > _DEATH_VAL end

---@return boolean
local function damageOrAfter() return current.damageType == DAMAGE_TYPE_UNKNOWN end
breakCheck[_DAMAGED]    = damageOrAfter
breakCheck[_AFTER]      = damageOrAfter

local function defaultCheck() end

---Common function to run any major event in the system.
---@param head damageEventRegistry
---@return boolean ran_yn
local function runEvent(head)
    local check = breakCheck[head] or defaultCheck
    if not current then
        print(GetStackTrace())
    end
    if dreaming or check() then
        return
    end
    userIndex = head.next
    if userIndex ~= head then
        Damage.enable(false)
        enableT(t3)
        dreaming = true
      
        --print("Start of event running")
        repeat
            if not userIndex.trigFrozen and userIndex.filters[userIndex.eFilter] and checkConfig() and not hasSource or (head ~= _SOURCE or (userIndex.minAOE and sourceAOE > userIndex.minAOE)) then
                userIndex.func()
            end
            userIndex = userIndex.next
        until userIndex == head or check()
        --print("End of event running")
      
        dreaming = nil
        Damage.enable(true)
        disableT(t3)
    end
    return true
end
And using this method, I could find the sequence of events, but I don't know what causes it, only know it happens when I use the Damage.apply provided function.
 
Level 20
Joined
Jul 10, 2009
Messages
477
It's good to see where you are coming from, but that's not exactly what I meant by example code.
Can you give me something that I can actually run on my own setup?

Edit: Example Code is meant to be simple and educative. I see some big chunk above that I don't understand. Neither do I understand, what the GetStackTrace call actually does. I'm not even sure, why it needs to be placed, where you placed it.
 
Level 24
Joined
Jun 26, 2020
Messages
1,850
It's good to see where you are coming from, but that's not exactly what I meant by example code.
Can you give me something that I can actually run on my own setup?

Edit: Example Code is meant to be simple and educative. I see some big chunk above that I don't understand. Neither do I understand, what the GetStackTrace call actually does. I'm not even sure, why it needs to be placed, where you placed it.
You mean a W3 map?, because in the thread I linked in the top are various, or a code that you can run in any program? Because I also used this as an example:
Lua:
local function Second()
    print(GetStackTrace())
end

local function First()
    Second()
end

First()
The GetStackTrace function only returns the position in the war3map.lua of the functions that call this, basically it will print the position of the print function then the position of where Second was called and then where First was called.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
Ok thanks, that explains something.

That means, if I want to investigate a bug that breaks my code, I need to:
  1. locate the bug (the line of occurance, e.g. using Try)
  2. insert GetStackTrace in the line before (which edits my code, so it requires me to restart the map)
  3. try to reproduce the error (which will likely produce a lot of unnecessary traces, because the erroneous function might get called multiple times, before the error actually occurs and GetStackTrace doesn't know, when the bug is going to occur).
That means, I would use this as a last resort, if I really can't figure out the trace myself on the fly.
I was thinking about whether I could include a trace into Try or not, but I currently think, that's not possible :(

You mean a W3 map?, because in the thread I linked in the top are various, or a code that you can run in any program? Because I also used this as an example
Not a W3 map, just normal code like the one with First and Second. The example from your prior post wasn't that useful to me, because it had external depencencies (supposebly coming from the Damage Engine), so I wasn't able to run it on the fly. Btw, my personal opinion is that you shouldn't require your readers to read through other threads to understand this resource. You have submitted it here and the submission should contain all information that the user needs to know ;)
 
Level 20
Joined
Jul 10, 2009
Messages
477
Yeah, I also assume people know what a stack trace is. Improving the general explanation and adding the code example should be sufficient.

I'm still thinking about how well this can be used for debugging. As described in my last post, the general procedure is a bit cumbersome, because it requires editing your code, restarting Wc3, reproducing the bug and finding the buggy trace among potentially many.
Despite that, I think this can prove really useful, so I might include a chapter about your resource in Debug Utils, if you don't mind?
 
Level 20
Joined
Jul 10, 2009
Messages
477
Just to let you know, I'm probably going with this shorter version of your resource for Debug Utils:
Lua:
---Returns the stack trace at the code position where this function is called.
---The returned string includes war3map.lua/blizzard.j.lua code positions of all functions from the stack trace in the order of execution (most recent call last). It does NOT include function names.
---@param oneline_yn? boolean default: false. Setting this to true will output the trace as a one-liner. False will include linebreaks between functions.
---@return string stracktrace
function GetStackTrace(oneline_yn)
    local trace, lastMsg, i, separator = "", "", 5, (oneline_yn and "; ") or "\n"
    local store = function(msg) lastMsg = msg:sub(1,-3) end --Passed to xpcall to handle the error message. Message is being saved to lastMsg for further use, excluding trailing space and colon.
    xpcall(error, store, "", 4) --starting at position 4 ensures that the functions "error", "xpcall" and "GetStackTrace" are not included in the trace.
    while lastMsg:sub(1,11) == "war3map.lua" or lastMsg:sub(1,14) == "blizzard.j.lua" do
        trace = separator .. lastMsg .. trace
        xpcall(error, store, "", i)
        i = i+1
    end
    return "Traceback (most recent call last)" .. trace
end
Ofc you will get credits for the original and I will link your thread ;)
 
Last edited:
Level 24
Joined
Jun 26, 2020
Messages
1,850
Just to let you know, I'm probably going with this shorter version of your resource for Debug Utils:
Lua:
function GetStackTrace()
    local trace, lastMsg, i = "", "", 5
    local store = function(msg) lastMsg = msg:sub(1,-3) end
    xpcall(error, store, "", 4)
    while lastMsg:sub(1,11) == "war3map.lua" or lastMsg:sub(1,14) == "blizzard.j.lua" do
        trace = lastMsg .. "\n" .. trace
        xpcall(error, store, "", i)
        i = i+1
    end
    return "Traceback (most recent call last):\n" .. trace
end
Ofc you will get credits for the original and I will link your thread ;)
Very well, in that case this thread should be moved to the Graveyard, I didn't considered simplify that the function, because I prefer pre-define all what I will need first, but its ok.

And there is something that I wanna point, in some cases I couldn't directly use the error function in the xpcall function, maybe I need the DebugUtils for that, I don't know.
 
Level 20
Joined
Jul 10, 2009
Messages
477
I have now finished the inclusion GetStackTrace into Debug Utils. Please let me know, what you think :)

Very well, in that case this thread should be moved to the Graveyard, I didn't considered simplify that the function, because I prefer pre-define all what I will need first, but its ok.
I leave that decision to you and the mods. But yeah, having the whole deal at one place probably makes more sense. Depending on how extensive the documentation in this thread is going to be, keeping this thread might also make sense. Regardless, thanks again for your work on the matter and your permission to include it into another resource!

And there is something that I wanna point, in some cases I couldn't directly use the error function in the xpcall function, maybe I need the DebugUtils for that, I don't know
That sounds weird. I didn't encounter the bug you mention, but I've seen some cases, where the stack trace provided was not complete. May be related. Let me know, when you find out more!
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I don't know, because there is already a thread of this in The Lab, and that should be double-posting.
Eh, someone else is welcome to disagree. I just don't think it's a big deal personally. A mod who oversees this particular forum should be able to merge your two threads if it's a matter of importance.
 
Status
Not open for further replies.
Top