diff options
author | Li Jin <dragon-fly@qq.com> | 2020-03-11 09:12:17 +0800 |
---|---|---|
committer | Li Jin <dragon-fly@qq.com> | 2020-03-11 09:12:17 +0800 |
commit | 61896f4e39c8f70a4cf7b804e1887f4c324a5ab7 (patch) | |
tree | 3243f68647271af395a869433f61c4f8dc3ba56f /src/MoonP/stacktraceplus.h | |
parent | 9705b3bfd45f536a3efc93a8782b96607547254a (diff) | |
download | yuescript-61896f4e39c8f70a4cf7b804e1887f4c324a5ab7.tar.gz yuescript-61896f4e39c8f70a4cf7b804e1887f4c324a5ab7.tar.bz2 yuescript-61896f4e39c8f70a4cf7b804e1887f4c324a5ab7.zip |
fix commit messed with gitignore.
Diffstat (limited to 'src/MoonP/stacktraceplus.h')
-rw-r--r-- | src/MoonP/stacktraceplus.h | 548 |
1 files changed, 548 insertions, 0 deletions
diff --git a/src/MoonP/stacktraceplus.h b/src/MoonP/stacktraceplus.h new file mode 100644 index 0000000..a309578 --- /dev/null +++ b/src/MoonP/stacktraceplus.h | |||
@@ -0,0 +1,548 @@ | |||
1 | R"lua_codes( | ||
2 | --[[ | ||
3 | Copyright (c) 2010 Ignacio BurgueƱo, modified by Li Jin | ||
4 | |||
5 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
6 | of this software and associated documentation files (the "Software"), to deal | ||
7 | in the Software without restriction, including without limitation the rights | ||
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
9 | copies of the Software, and to permit persons to whom the Software is | ||
10 | furnished to do so, subject to the following conditions: | ||
11 | |||
12 | The above copyright notice and this permission notice shall be included in | ||
13 | all copies or substantial portions of the Software. | ||
14 | |||
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
21 | THE SOFTWARE.]] | ||
22 | |||
23 | -- tables | ||
24 | local _G = _G | ||
25 | local string, io, debug, coroutine = string, io, debug, coroutine | ||
26 | |||
27 | -- functions | ||
28 | local tostring, require = tostring, require | ||
29 | local next, assert = next, assert | ||
30 | local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs | ||
31 | local error = error | ||
32 | |||
33 | assert(debug, "debug table must be available at this point") | ||
34 | |||
35 | local string_gmatch = string.gmatch | ||
36 | local string_sub = string.sub | ||
37 | local table_concat = table.concat | ||
38 | |||
39 | local moonp = require("moonp") | ||
40 | |||
41 | local _M = { | ||
42 | max_tb_output_len = 70, -- controls the maximum length of the 'stringified' table before cutting with ' (more...)' | ||
43 | dump_locals = true, | ||
44 | simplified = false | ||
45 | } | ||
46 | |||
47 | -- this tables should be weak so the elements in them won't become uncollectable | ||
48 | local m_known_tables = { [_G] = "_G (global table)" } | ||
49 | local function add_known_module(name, desc) | ||
50 | local ok, mod = pcall(require, name) | ||
51 | if ok then | ||
52 | m_known_tables[mod] = desc | ||
53 | end | ||
54 | end | ||
55 | |||
56 | add_known_module("string", "string module") | ||
57 | add_known_module("io", "io module") | ||
58 | add_known_module("os", "os module") | ||
59 | add_known_module("table", "table module") | ||
60 | add_known_module("math", "math module") | ||
61 | add_known_module("package", "package module") | ||
62 | add_known_module("debug", "debug module") | ||
63 | add_known_module("coroutine", "coroutine module") | ||
64 | |||
65 | -- lua5.2 | ||
66 | add_known_module("bit32", "bit32 module") | ||
67 | -- luajit | ||
68 | add_known_module("bit", "bit module") | ||
69 | add_known_module("jit", "jit module") | ||
70 | -- lua5.3 | ||
71 | if _VERSION >= "Lua 5.3" then | ||
72 | add_known_module("utf8", "utf8 module") | ||
73 | end | ||
74 | |||
75 | |||
76 | local m_user_known_tables = {} | ||
77 | |||
78 | local m_known_functions = {} | ||
79 | for _, name in ipairs{ | ||
80 | -- Lua 5.2, 5.1 | ||
81 | "assert", | ||
82 | "collectgarbage", | ||
83 | "dofile", | ||
84 | "error", | ||
85 | "getmetatable", | ||
86 | "ipairs", | ||
87 | "load", | ||
88 | "loadfile", | ||
89 | "next", | ||
90 | "pairs", | ||
91 | "pcall", | ||
92 | "print", | ||
93 | "rawequal", | ||
94 | "rawget", | ||
95 | "rawlen", | ||
96 | "rawset", | ||
97 | "require", | ||
98 | "select", | ||
99 | "setmetatable", | ||
100 | "tonumber", | ||
101 | "tostring", | ||
102 | "type", | ||
103 | "xpcall", | ||
104 | |||
105 | -- Lua 5.1 | ||
106 | "gcinfo", | ||
107 | "getfenv", | ||
108 | "loadstring", | ||
109 | "module", | ||
110 | "newproxy", | ||
111 | "setfenv", | ||
112 | "unpack", | ||
113 | -- TODO: add table.* etc functions | ||
114 | } do | ||
115 | if _G[name] then | ||
116 | m_known_functions[_G[name]] = name | ||
117 | end | ||
118 | end | ||
119 | |||
120 | local m_user_known_functions = {} | ||
121 | |||
122 | local function safe_tostring (value) | ||
123 | local ok, err = pcall(tostring, value) | ||
124 | if ok then return err else return ("<failed to get printable value>: '%s'"):format(err) end | ||
125 | end | ||
126 | |||
127 | -- Private: | ||
128 | -- Parses a line, looking for possible function definitions (in a very naive way) | ||
129 | -- Returns '(anonymous)' if no function name was found in the line | ||
130 | local function ParseLine(line) | ||
131 | assert(type(line) == "string") | ||
132 | local match = line:match("^%s*function%s+(%w+)") | ||
133 | if match then | ||
134 | --print("+++++++++++++function", match) | ||
135 | return match | ||
136 | end | ||
137 | match = line:match("^%s*local%s+function%s+(%w+)") | ||
138 | if match then | ||
139 | --print("++++++++++++local", match) | ||
140 | return match | ||
141 | end | ||
142 | match = line:match("^%s*local%s+(%w+)%s+=%s+function") | ||
143 | if match then | ||
144 | --print("++++++++++++local func", match) | ||
145 | return match | ||
146 | end | ||
147 | match = line:match("%s*function%s*%(") -- this is an anonymous function | ||
148 | if match then | ||
149 | --print("+++++++++++++function2", match) | ||
150 | return "(anonymous)" | ||
151 | end | ||
152 | return "(anonymous)" | ||
153 | end | ||
154 | |||
155 | -- Private: | ||
156 | -- Tries to guess a function's name when the debug info structure does not have it. | ||
157 | -- It parses either the file or the string where the function is defined. | ||
158 | -- Returns '?' if the line where the function is defined is not found | ||
159 | local function GuessFunctionName(info) | ||
160 | -- print("guessing function name") | ||
161 | if type(info.source) == "string" and info.source:sub(1,1) == "@" then | ||
162 | local fname = info.source:sub(2) | ||
163 | local text | ||
164 | if moonp.file_exist(fname) then | ||
165 | text = moonp.read_file(fname) | ||
166 | end | ||
167 | if not text then | ||
168 | -- print("file not found: "..tostring(err)) -- whoops! | ||
169 | return "?" | ||
170 | end | ||
171 | local line | ||
172 | local count = 0 | ||
173 | for lineText in (text.."\n"):gmatch("(.-)\n") do | ||
174 | line = lineText | ||
175 | count = count + 1 | ||
176 | if count == info.linedefined then | ||
177 | break | ||
178 | end | ||
179 | end | ||
180 | if not line then | ||
181 | --print("line not found") -- whoops! | ||
182 | return "?" | ||
183 | end | ||
184 | return ParseLine(line) | ||
185 | else | ||
186 | local line | ||
187 | local lineNumber = 0 | ||
188 | for l in string_gmatch(info.source, "([^\n]+)\n-") do | ||
189 | lineNumber = lineNumber + 1 | ||
190 | if lineNumber == info.linedefined then | ||
191 | line = l | ||
192 | break | ||
193 | end | ||
194 | end | ||
195 | if not line then | ||
196 | -- print("line not found") -- whoops! | ||
197 | return "?" | ||
198 | end | ||
199 | return ParseLine(line) | ||
200 | end | ||
201 | end | ||
202 | |||
203 | --- | ||
204 | -- Dumper instances are used to analyze stacks and collect its information. | ||
205 | -- | ||
206 | local Dumper = {} | ||
207 | |||
208 | Dumper.new = function(thread) | ||
209 | local t = { lines = {} } | ||
210 | for k,v in pairs(Dumper) do t[k] = v end | ||
211 | |||
212 | t.dumping_same_thread = (thread == coroutine.running()) | ||
213 | |||
214 | -- if a thread was supplied, bind it to debug.info and debug.get | ||
215 | -- we also need to skip this additional level we are introducing in the callstack (only if we are running | ||
216 | -- in the same thread we're inspecting) | ||
217 | if type(thread) == "thread" then | ||
218 | t.getinfo = function(level, what) | ||
219 | if t.dumping_same_thread and type(level) == "number" then | ||
220 | level = level + 1 | ||
221 | end | ||
222 | return debug.getinfo(thread, level, what) | ||
223 | end | ||
224 | t.getlocal = function(level, loc) | ||
225 | if t.dumping_same_thread then | ||
226 | level = level + 1 | ||
227 | end | ||
228 | return debug.getlocal(thread, level, loc) | ||
229 | end | ||
230 | else | ||
231 | t.getinfo = debug.getinfo | ||
232 | t.getlocal = debug.getlocal | ||
233 | end | ||
234 | |||
235 | return t | ||
236 | end | ||
237 | |||
238 | -- helpers for collecting strings to be used when assembling the final trace | ||
239 | function Dumper:add (text) | ||
240 | self.lines[#self.lines + 1] = text | ||
241 | end | ||
242 | function Dumper:add_f (fmt, ...) | ||
243 | self:add(fmt:format(...)) | ||
244 | end | ||
245 | function Dumper:concat_lines () | ||
246 | return table_concat(self.lines) | ||
247 | end | ||
248 | |||
249 | --- | ||
250 | -- Private: | ||
251 | -- Iterates over the local variables of a given function. | ||
252 | -- | ||
253 | -- @param level The stack level where the function is. | ||
254 | -- | ||
255 | function Dumper:DumpLocals (level) | ||
256 | if not _M.dump_locals then return end | ||
257 | |||
258 | local prefix = "\t " | ||
259 | local i = 1 | ||
260 | |||
261 | if self.dumping_same_thread then | ||
262 | level = level + 1 | ||
263 | end | ||
264 | |||
265 | local name, value = self.getlocal(level, i) | ||
266 | if not name then | ||
267 | return | ||
268 | end | ||
269 | self:add("\tLocal variables:\r\n") | ||
270 | while name do | ||
271 | if type(value) == "number" then | ||
272 | self:add_f("%s%s = number: %g\r\n", prefix, name, value) | ||
273 | elseif type(value) == "boolean" then | ||
274 | self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value)) | ||
275 | elseif type(value) == "string" then | ||
276 | self:add_f("%s%s = string: %q\r\n", prefix, name, value) | ||
277 | elseif type(value) == "userdata" then | ||
278 | self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value)) | ||
279 | elseif type(value) == "nil" then | ||
280 | self:add_f("%s%s = nil\r\n", prefix, name) | ||
281 | elseif type(value) == "table" then | ||
282 | if m_known_tables[value] then | ||
283 | self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value]) | ||
284 | elseif m_user_known_tables[value] then | ||
285 | self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value]) | ||
286 | else | ||
287 | local txt = "{" | ||
288 | for k,v in pairs(value) do | ||
289 | txt = txt..safe_tostring(k)..":"..safe_tostring(v) | ||
290 | if #txt > _M.max_tb_output_len then | ||
291 | txt = txt.." (more...)" | ||
292 | break | ||
293 | end | ||
294 | if next(value, k) then txt = txt..", " end | ||
295 | end | ||
296 | self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt.."}") | ||
297 | end | ||
298 | elseif type(value) == "function" then | ||
299 | local info = self.getinfo(value, "nS") | ||
300 | local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value] | ||
301 | if info.what == "C" then | ||
302 | self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value))) | ||
303 | else | ||
304 | local source = info.short_src | ||
305 | if source:sub(2,7) == "string" then | ||
306 | source = source:sub(9) | ||
307 | end | ||
308 | --for k,v in pairs(info) do print(k,v) end | ||
309 | fun_name = fun_name or GuessFunctionName(info) | ||
310 | self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source) | ||
311 | end | ||
312 | elseif type(value) == "thread" then | ||
313 | self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value)) | ||
314 | end | ||
315 | i = i + 1 | ||
316 | name, value = self.getlocal(level, i) | ||
317 | end | ||
318 | end | ||
319 | |||
320 | local function getMoonLineNumber(fname, line) | ||
321 | local moonCompiled = require("moonp").moon_compiled | ||
322 | local source = moonCompiled["@"..fname] | ||
323 | if not source then | ||
324 | source = moonCompiled["@="..fname] | ||
325 | end | ||
326 | if not source then | ||
327 | if moonp.file_exist(fname) then | ||
328 | local codes = moonp.read_file(fname) | ||
329 | local moonFile = codes:match("^%s*--%s*%[moon%]:%s*([^\n]*)") | ||
330 | if moonFile then | ||
331 | fname = moonFile:gsub("^%s*(.-)%s*$", "%1") | ||
332 | source = codes | ||
333 | end | ||
334 | end | ||
335 | end | ||
336 | if source then | ||
337 | local i, target = 1, tonumber(line) | ||
338 | for lineCode in source:gmatch("([^\n]*)\n") do | ||
339 | if i == target then | ||
340 | local num = lineCode:match("--%s*(%d*)%s*$") | ||
341 | if num then | ||
342 | return fname, num | ||
343 | end | ||
344 | break | ||
345 | end | ||
346 | i = i + 1 | ||
347 | end | ||
348 | end | ||
349 | return fname, line | ||
350 | end | ||
351 | |||
352 | --- | ||
353 | -- Public: | ||
354 | -- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc. | ||
355 | -- This function is suitable to be used as an error handler with pcall or xpcall | ||
356 | -- | ||
357 | -- @param thread An optional thread whose stack is to be inspected (defaul is the current thread) | ||
358 | -- @param message An optional error string or object. | ||
359 | -- @param level An optional number telling at which level to start the traceback (default is 1) | ||
360 | -- | ||
361 | -- Returns a string with the stack trace and a string with the original error. | ||
362 | -- | ||
363 | function _M.stacktrace(thread, message, level) | ||
364 | if type(thread) ~= "thread" then | ||
365 | -- shift parameters left | ||
366 | thread, message, level = nil, thread, message | ||
367 | end | ||
368 | |||
369 | thread = thread or coroutine.running() | ||
370 | |||
371 | level = level or 1 | ||
372 | |||
373 | local dumper = Dumper.new(thread) | ||
374 | |||
375 | if type(message) == "table" then | ||
376 | dumper:add("an error object {\r\n") | ||
377 | local first = true | ||
378 | for k,v in pairs(message) do | ||
379 | if first then | ||
380 | dumper:add(" ") | ||
381 | first = false | ||
382 | else | ||
383 | dumper:add(",\r\n ") | ||
384 | end | ||
385 | dumper:add(safe_tostring(k)) | ||
386 | dumper:add(": ") | ||
387 | dumper:add(safe_tostring(v)) | ||
388 | end | ||
389 | dumper:add("\r\n}") | ||
390 | elseif type(message) == "string" then | ||
391 | local fname, line, msg = message:match('(.+):(%d+): (.*)$') | ||
392 | local nfname, nline, nmsg = fname:match('(.+):(%d+): (.*)$') | ||
393 | if nfname then | ||
394 | fname = nmsg | ||
395 | end | ||
396 | if fname then | ||
397 | fname = fname:gsub("%[string \"", "") | ||
398 | fname = fname:gsub("\"%]", "") | ||
399 | fname = fname:gsub("^%s*(.-)%s*$", "%1") | ||
400 | local extension = fname:match("%.([^%.\\/]*)$") | ||
401 | if not extension then | ||
402 | local fext = fname .. ".lua" | ||
403 | if moonp.file_exist(fext) then | ||
404 | fname = fext | ||
405 | else | ||
406 | fext = fname .. ".moon" | ||
407 | if moonp.file_exist(fext) then | ||
408 | fname = fext | ||
409 | end | ||
410 | end | ||
411 | end | ||
412 | fname, line = getMoonLineNumber(fname, line) | ||
413 | if _M.simplified then | ||
414 | message = table.concat({ | ||
415 | "", fname, ":", | ||
416 | line, ": ", msg}) | ||
417 | message = message:gsub("^%(moonplus%):%s*%d+:%s*", "") | ||
418 | message = message:gsub("%s(%d+):", "%1:") | ||
419 | else | ||
420 | message = table.concat({ | ||
421 | "[string \"", fname, "\"]:", | ||
422 | line, ": ", msg}) | ||
423 | end | ||
424 | end | ||
425 | dumper:add(message) | ||
426 | end | ||
427 | |||
428 | local moonp = require("moonp") | ||
429 | if moonp._hide_stacktrace_ then | ||
430 | local msg = dumper:concat_lines() | ||
431 | moonp._hide_stacktrace_ = nil | ||
432 | return message | ||
433 | end | ||
434 | |||
435 | dumper:add("\r\n") | ||
436 | dumper:add[[ | ||
437 | Stack Traceback | ||
438 | =============== | ||
439 | ]] | ||
440 | |||
441 | local level_to_show = 1 | ||
442 | if dumper.dumping_same_thread then level = level + 1 end | ||
443 | |||
444 | local info = dumper.getinfo(level, "nSlf") | ||
445 | while info do | ||
446 | if info.source and info.source:sub(1,1) == "@" then | ||
447 | info.source = info.source:sub(2) | ||
448 | elseif info.what == "main" or info.what == "Lua" then | ||
449 | info.source = info.source | ||
450 | end | ||
451 | local fname = info.source | ||
452 | local extension = fname:match("%.([^%.\\/]*)$") | ||
453 | if not extension then | ||
454 | local fext = fname .. ".lua" | ||
455 | if moonp.file_exist(fext) then | ||
456 | fname = fext | ||
457 | else | ||
458 | fext = fname .. ".moon" | ||
459 | if moonp.file_exist(fext) then | ||
460 | fname = fext | ||
461 | end | ||
462 | end | ||
463 | end | ||
464 | info.source, info.currentline = getMoonLineNumber(fname, info.currentline) | ||
465 | if info.what == "main" then | ||
466 | if _M.simplified then | ||
467 | dumper:add_f("(%d) '%s':%d\r\n", level_to_show, info.source, info.currentline) | ||
468 | else | ||
469 | dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, info.source, info.currentline) | ||
470 | end | ||
471 | elseif info.what == "C" then | ||
472 | --print(info.namewhat, info.name) | ||
473 | --for k,v in pairs(info) do print(k,v, type(v)) end | ||
474 | local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func) | ||
475 | dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name) | ||
476 | --dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value))) | ||
477 | elseif info.what == "tail" then | ||
478 | --print("tail") | ||
479 | --for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name) | ||
480 | dumper:add_f("(%d) tail call\r\n", level_to_show) | ||
481 | dumper:DumpLocals(level) | ||
482 | elseif info.what == "Lua" then | ||
483 | local source = info.source | ||
484 | local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name | ||
485 | if source:sub(2, 7) == "string" then | ||
486 | source = source:sub(10,-3) | ||
487 | end | ||
488 | local was_guessed = false | ||
489 | if not function_name or function_name == "?" then | ||
490 | --for k,v in pairs(info) do print(k,v, type(v)) end | ||
491 | function_name = GuessFunctionName(info) | ||
492 | was_guessed = true | ||
493 | end | ||
494 | -- test if we have a file name | ||
495 | local function_type = (info.namewhat == "") and "function" or info.namewhat | ||
496 | if info.source and info.source:sub(1, 1) == "@" then | ||
497 | if _M.simplified then | ||
498 | dumper:add_f("(%d) '%s':%d%s\r\n", level_to_show, info.source:sub(2), info.currentline, was_guessed and " (guess)" or "") | ||
499 | else | ||
500 | dumper:add_f("(%d) Lua %s '%s' at file '%s':%d%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") | ||
501 | end | ||
502 | elseif info.source and info.source:sub(1,1) == '#' then | ||
503 | if _M.simplified then | ||
504 | dumper:add_f("(%d) '%s':%d%s\r\n", level_to_show, info.source:sub(2), info.currentline, was_guessed and " (guess)" or "") | ||
505 | else | ||
506 | dumper:add_f("(%d) Lua %s '%s' at template '%s':%d%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "") | ||
507 | end | ||
508 | else | ||
509 | if _M.simplified then | ||
510 | dumper:add_f("(%d) '%s':%d\r\n", level_to_show, source, info.currentline) | ||
511 | else | ||
512 | dumper:add_f("(%d) Lua %s '%s' at chunk '%s':%d\r\n", level_to_show, function_type, function_name, source, info.currentline) | ||
513 | end | ||
514 | end | ||
515 | dumper:DumpLocals(level) | ||
516 | else | ||
517 | dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what) | ||
518 | end | ||
519 | |||
520 | level = level + 1 | ||
521 | level_to_show = level_to_show + 1 | ||
522 | info = dumper.getinfo(level, "nSlf") | ||
523 | end | ||
524 | |||
525 | return dumper:concat_lines() | ||
526 | end | ||
527 | |||
528 | -- | ||
529 | -- Adds a table to the list of known tables | ||
530 | function _M.add_known_table(tab, description) | ||
531 | if m_known_tables[tab] then | ||
532 | error("Cannot override an already known table") | ||
533 | end | ||
534 | m_user_known_tables[tab] = description | ||
535 | end | ||
536 | |||
537 | -- | ||
538 | -- Adds a function to the list of known functions | ||
539 | function _M.add_known_function(fun, description) | ||
540 | if m_known_functions[fun] then | ||
541 | error("Cannot override an already known function") | ||
542 | end | ||
543 | m_user_known_functions[fun] = description | ||
544 | end | ||
545 | |||
546 | return _M | ||
547 | |||
548 | )lua_codes"; | ||