OK I looked more closely at the debug API and ran an experiment.
If I have this:
return doSomething(x) end,
_onA_ = function(x)
_onB_ = function(x)
Then if I store the result in t, and I use debug.getinfo(t.onA, 'L'), I get info.activelines containing keys which are the "valid" lines of the function. In this case, they are the two lines containing "return doSomething(x)" and "end,".
If I make the assumption that the first line of the function immediately precedes, then I can tell which are the original source file lines containing the function. I can use those to recover the source code for each function.
I think this would be good enough for my needs.
However, if I provide flag 'S' I get fields 'lastlinedefined' ("end,") and 'linedefined' ("_onA_ = function(x)") which is perfect.
'S' also has fields 'source' and 'short_src' which I thought might contain the actual source code of the function, but they just have the name of the file. Otherwise that would be even more convenient.
So the solution is to use 'S' to get the first and last defined line number for each handler function, then extract them from the original source file when round-tripping.