From 96e7b60285a771ddaeecd20985cdb88904ebadb8 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Thu, 14 May 2020 19:11:57 +0100 Subject: [PATCH] Display function arguments and positions in the REPL - cc.pretty.pretty now accepts two additional options: - function_args: Show function arguments - function_source: Show where functions are defined. - Expose the two options as lua.* settings (defaulting function_args to true, and function_source to false). These are then used in the Lua REPL. Closes #361 --- .../assets/computercraft/lua/bios.lua | 12 ++++ .../lua/rom/modules/main/cc/pretty.lua | 59 ++++++++++++++++--- .../computercraft/lua/rom/programs/lua.lua | 5 +- .../test-rom/spec/modules/cc/pretty_spec.lua | 19 +++++- 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/main/resources/assets/computercraft/lua/bios.lua b/src/main/resources/assets/computercraft/lua/bios.lua index 55688a4f2..4573f9826 100644 --- a/src/main/resources/assets/computercraft/lua/bios.lua +++ b/src/main/resources/assets/computercraft/lua/bios.lua @@ -938,11 +938,23 @@ settings.define("motd.path", { description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]], type = "string", }) + settings.define("lua.warn_against_use_of_local", { default = true, description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessable on the next input.]], type = "boolean", }) +settings.define("lua.function_args", { + default = true, + description = "Show function arguments when printing functions.", + type = "boolean", +}) +settings.define("lua.function_source", { + default = false, + description = "Show where a function was defined when printing functions.", + type = "boolean", +}) + if term.isColour() then settings.define("bios.use_multishell", { default = true, diff --git a/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/pretty.lua b/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/pretty.lua index 65718f949..33fce2c7a 100644 --- a/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/pretty.lua +++ b/src/main/resources/assets/computercraft/lua/rom/modules/main/cc/pretty.lua @@ -19,8 +19,12 @@ -- local pretty = require "cc.pretty" -- pretty.write(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) -local expect = require "cc.expect".expect -local type, getmetatable, setmetatable, colours, str_write = type, getmetatable, setmetatable, colours, write +local expect = require "cc.expect" +local expect, field = expect.expect, expect.field + +local type, getmetatable, setmetatable, colours, str_write, tostring = type, getmetatable, setmetatable, colours, write, tostring +local debug_info = type(debug) == "table" and type(debug.getinfo) == "function" and debug.getinfo +local debug_local = type(debug) == "table" and type(debug.getlocal) == "function" and debug.getlocal --- @{table.insert} alternative, but with the length stored inline. local function append(out, value) @@ -343,13 +347,38 @@ local function key_compare(a, b) return false end -local function pretty_impl(obj, tracking) +local function show_function(fn, options) + local info = debug_info and debug_info(fn, "Su") + + -- Include function source position if available + local name + if options.function_source and info and info.short_src and info.linedefined and info.linedefined >= 1 then + name = "function<" .. info.short_src .. ":" .. info.linedefined .. ">" + else + name = tostring(fn) + end + + -- Include arguments if a Lua function and if available. Lua will report "C" + -- functions as variadic. + if options.function_args and info and info.what == "Lua" and info.nparams and debug_local then + local args = {} + for i = 1, info.nparams do args[i] = debug_local(fn, i) or "?" end + if info.isvararg then args[#args + 1] = "..." end + name = name .. "(" .. table.concat(args, ", ") .. ")" + end + + return name +end + +local function pretty_impl(obj, options, tracking) local obj_type = type(obj) if obj_type == "string" then local formatted = ("%q"):format(obj):gsub("\\\n", "\\n") return text(formatted, colours.red) elseif obj_type == "number" then return text(tostring(obj), colours.magenta) + elseif obj_type == "function" then + return text(show_function(obj, options), colours.lightGrey) elseif obj_type ~= "table" or tracking[obj] then return text(tostring(obj), colours.lightGrey) elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then @@ -371,15 +400,15 @@ local function pretty_impl(obj, tracking) local v = obj[k] local ty = type(k) if ty == "number" and k % 1 == 0 and k >= 1 and k <= length then - append(doc, pretty_impl(v, tracking)) + append(doc, pretty_impl(v, options, tracking)) elseif ty == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then append(doc, text(k .. " = ")) - append(doc, pretty_impl(v, tracking)) + append(doc, pretty_impl(v, options, tracking)) else append(doc, obracket) - append(doc, pretty_impl(k, tracking)) + append(doc, pretty_impl(k, options, tracking)) append(doc, cbracket) - append(doc, pretty_impl(v, tracking)) + append(doc, pretty_impl(v, options, tracking)) end end @@ -393,12 +422,24 @@ end -- This can then be rendered with @{write} or @{print}. -- -- @param obj The object to pretty-print. +-- @tparam[opt] { function_args = boolean, function_source = boolean } options +-- Controls how various properties are displayed. +-- - `function_args`: Show the arguments to a function if known (`false` by default). +-- - `function_source: Show where the function was defined, instead of +-- `function: xxxxxxxx` (`false` by default). -- @treturn Doc The object formatted as a document. -- @usage Display a table on the screen -- local pretty = require "cc.pretty" -- pretty.print(pretty.pretty({ 1, 2, 3 })) -local function pretty(obj) - return pretty_impl(obj, {}) +local function pretty(obj, options) + expect(2, options, "table", "nil") + options = options or {} + + local actual_options = { + function_source = field(options, "function_source", "boolean", "nil") or false, + function_args = field(options, "function_args", "boolean", "nil") or false, + } + return pretty_impl(obj, actual_options, {}) end return { diff --git a/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua b/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua index 7ab9bda42..7418a017f 100644 --- a/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua +++ b/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua @@ -95,7 +95,10 @@ while bRunning do local n = 1 while n < tResults.n or n <= nForcePrint do local value = tResults[n + 1] - local ok, serialised = pcall(pretty.pretty, value) + local ok, serialised = pcall(pretty.pretty, value, { + function_args = settings.get("lua.function_args"), + function_source = settings.get("lua.function_source"), + }) if ok then pretty.print(serialised) else diff --git a/src/test/resources/test-rom/spec/modules/cc/pretty_spec.lua b/src/test/resources/test-rom/spec/modules/cc/pretty_spec.lua index 2949c1a75..174751606 100644 --- a/src/test/resources/test-rom/spec/modules/cc/pretty_spec.lua +++ b/src/test/resources/test-rom/spec/modules/cc/pretty_spec.lua @@ -165,7 +165,7 @@ describe("cc.pretty", function() describe("pretty", function() -- We make use of "render" here, as it's considerably easier than checking against the actual structure. -- However, it does also mean our tests are less unit-like. - local function pretty(x, width) return pp.render(pp.pretty(x), width) end + local function pretty(x, width, options) return pp.render(pp.pretty(x, options), width) end describe("tables", function() it("displays empty tables", function() @@ -201,8 +201,21 @@ describe("cc.pretty", function() expect(pretty("hello\nworld")):eq('"hello\\nworld"') end) - it("shows functions", function() - expect(pretty(pretty)):eq(tostring(pretty)) + describe("functions", function() + it("shows functions", function() + expect(pretty(pretty)):eq(tostring(pretty)) + end) + + it("shows function arguments", function() + local f = function(a, ...) end + expect(pretty(f, nil, { function_args = true })):eq(tostring(f) .. "(a, ...)") + end) + + it("shows the function source", function() + local f = function(a, ...) end + expect(pretty(f, nil, { function_source = true })) + :str_match("^function<.*pretty_spec%.lua:%d+>$") + end) end) end) end)