This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
theoriginalbit's profile picture

Error Message Parsing W/ Patterns

Started by theoriginalbit, 18 October 2013 - 11:59 PM
theoriginalbit #1
Posted 19 October 2013 - 01:59 AM
Firstly, ugh sometimes I hate patterns. I'm still not the best I can be with patterns, there are much better people out there, so I've come to you all for help.

Problem

Ok so I'm currently writing a game, and I've got a function, lets call it validate, that can validate supplied values and invokes error when said values are incorrect. The problem I'm facing at the moment is that this validate function can be called from any level, but I want to blame to still be pointed to the original caller.

For example I've got a "class" tree, and lets say class x calls class y as it "extends" it, and class y throws and error because the data supplied to it by x, which is supplied by another function is wrong, the blame needs to be pointed at the function that calls x, not x which is how it currently stands, as such one specific level cannot be supplied for the error/assert function.


Problem Solution

So in order to fix this problem I've created a function that I call, called tryCatch which is the following

function tryCatch(func, ...)
  local ok, err = pcall(func, ...)
  if not ok then
	error(err, 3)
  end
  return err
end


Problem w/ Solution

The above solution works fine, however for the messages to be displayed nicely when raising an error anywhere I have to use a level of 0, otherwise the error string is polluted with for example "pcall: <<message>>", this is not ideal, I cannot assume that a level of 0 will always be used due to some of the functions that need to be called with the tryCatch function, I also plan on making this function available to anyone else wishing to add extra modules/content to the game.

As such I need to parse the error message that comes back from the pcall, so that no matter the level, the error message will be all that is raised again. This is where I'm running into problems. The error messages themselves can be a range of formats throughout, here are some examples of format (note, these are not my actual error messages)

1. error("Invalid arg #1: you dun-derped", 2) --# example output...  test:5: Invalid arg #1: you dun-derped
2. error("Not initialised", 0) --# example output...  Not initialised
3. error("Fail: you will always: fail: fear me!", 3) --# example output...  test:7: Fail: you will always: fail: fear me!
4. error("Nope: fail.png", 0) --# P.S. I know this one will be impossible, it's not important if this is handled correctly... example output...  Nope: fail.png


Attempts to Solve Solution Problem

Here are my attempts at message parsing
Spoiler

err:gsub("%w+: ?", "")
Fails on:
#1 removes the ": " from the message
#3 outputs "you will fear me!"
#4 outputs "fail.png"


err:match("%w+:(/>/>/>/>%w+):")
Fails on:
#1 outputs nothing
#3 outputs pcall line number
#4 outputs nothing


err:reverse():match("(.-) ?:%w+"):reverse()
Fails on:
#1 outputs "you dun-derped"
#3 outputs "fear me"
#4 outputs "fail.png"


err:match("^%a+:?%d?: ?(.+)")
Fails on:
#3 outputs "<<pcall line numer>>: Fail: you will always: fail: fear me!"
#4 outputs "fail.png" … again not overly concerned about this one
That is about all that I know how to do, like I stated, my knowledge in patterns isn't that great, but as you can see the last attempt is the closest I've got, I just cannot seem to get it to work for the third case, I don't really care about #4 it'd be great to have it supported, but I know it'll just be an edge case that get's handled wrong.

Hopefully what I've said makes sense, and you can understand my problem. Maybe you can see another way that I can handle the original problem instead of using tryCatch (do note however, it has to assume dumb users, and not require a throwback level argument). Any help is greatly appreciated.

— BIT
Lyqyd #2
Posted 19 October 2013 - 03:31 AM
Seems like it might be wise to return nil, "error text" if it should be thrown at a line number and error("error text", 0) if it shouldn't. Callers need to verify that a non-nil return value is returned and throw the appropriate error message from their location if an error message is returned.
Bomb Bloke #3
Posted 19 October 2013 - 04:16 AM
Assuming you want to stick with having your functions throwing "regular" errors for this tryCatch function to handle, I would stick "bios", "pcall" and "shell" in a table along with the script name and first check to see if the start of whatever error it gets exists in that table (using a basic string.sub for eg). Depending on which string from the table hit a match I'd apply a different regex (eg, in the case of #4 I'd get no match and apply no filtering at all).

It's not as elegant as a single regex (you'd have to iterate and all, assuming you can't cram all those strings into a regex and have it only look for a line number if certain ones are found??), but assuming random function names can't come back to muddy the waters (and I admit I'm also not familiar enough with errors to know) I don't see how it could be made more reliable. You'd have to go rather more out of your way to trip it up then in the examples given.

(Actually, reading further it looks like you can do conditional matches, so maybe look into that.)
theoriginalbit #4
Posted 19 October 2013 - 04:32 AM
Seems like it might be wise to return nil, "error text" if it should be thrown at a line number and error("error text", 0) if it shouldn't. Callers need to verify that a non-nil return value is returned and throw the appropriate error message from their location if an error message is returned.
Yeh I was considering doing that, but obviously if I could find a better solution that didn't rely on people remembering to use error("msg", 0) it would have been better. I think I may just have to go back error level with 0.

Assuming you want to stick with having your functions throwing "regular" errors for this tryCatch function to handle, I would stick "bios", "pcall" and "shell" in a table along with the script name and first check to see if the start of whatever error it gets exists in that table (using a basic string.sub for eg). Depending on which string from the table hit a match I'd apply a different regex (eg, in the case of #4 I'd get no match and apply no filtering at all).
I was also considering this at one point, I just couldn't find a solution that worked well enough, I though using patterns may do the job better.

Actually, reading further it looks like you can do conditional matches, so maybe look into that.
If only Lua used regex and not patterns :P/>

EDIT: Oh! Thought just came to me!
Maybe I could set the function's environment to override the error function, so that even when they provide a level, it just uses 0, then restore the environment after its run. That should work right?
EDIT 2: I'm thinking this should work, will test now
Code
Spoiler

function tryCatch(func, ...)
  assert(type(func) == "function", "Arg #1: Expected function, got "..type(func), 2)
  local oldmt = getmetatable(func) or _G
  setfenv(func, setmetatable({error = function(err) return _G.error(err, 0) end},{ __index = getfenv(func)}))
  local result = {pcall(func, ...)}
  setfenv(func, oldmt)
  if not result[1] then
	error(result[2], 3)
  end
  table.remove(result, 1)
  return unpack(result)
end
EDIT 3: Ok yep that seems to have worked perfectly, the blame was always thrown back to the expected line in this example with the correct error message… of course there was a simpler method than using patterns :P/>
Test code
Spoilerlocal function one()
error("This", 2)
end

function tryCatch(func, …)
assert(type(func) == "function", "Arg #1: Expected function, got "..type(func), 2)
local oldmt = getmetatable(func) or _G
setfenv(func, setmetatable({error = function(err) return _G.error(err, 0) end},{ __index = getfenv(func)}))
local result = {pcall(func, …)}
setfenv(func, oldmt)
if not result[1] then
error(result[2], 3)
end
table.remove(result, 1)
return unpack(result)
end

local function that()
tryCatch(one)
end

local function this()
tryCatch(one)
tryCatch(that)
end

print("This should say 27: ", this())
print("This should say 28: ", one()) –# comment out previous to test this one
Edited on 19 October 2013 - 03:03 AM