diff options
| author | jacqueline <me@jacqueline.id.au> | 2023-12-13 16:10:08 +1100 |
|---|---|---|
| committer | jacqueline <me@jacqueline.id.au> | 2023-12-13 16:10:08 +1100 |
| commit | 64b106c13e18c33be0f2b0de532054e0ed3f731d (patch) | |
| tree | b54b1c90d941bc456b4d51e864970720bdf2d648 /lib/lua-repl/repl/init.lua | |
| parent | 5a2f0b08e0e3f20cda977b510b680d5843ae7283 (diff) | |
| download | tangara-fw-64b106c13e18c33be0f2b0de532054e0ed3f731d.tar.gz | |
add a cool lua repl
Diffstat (limited to 'lib/lua-repl/repl/init.lua')
| -rw-r--r-- | lib/lua-repl/repl/init.lua | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/lib/lua-repl/repl/init.lua b/lib/lua-repl/repl/init.lua new file mode 100644 index 00000000..b5499f59 --- /dev/null +++ b/lib/lua-repl/repl/init.lua @@ -0,0 +1,416 @@ +-- Copyright (c) 2011-2015 Rob Hoelz <rob@hoelz.ro> +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-- @class repl +--- This module implements the core functionality of a REPL. + +local plugins_lookup_meta = { __mode = 'k' } + +local repl = { _buffer = '', _plugins = setmetatable({}, plugins_lookup_meta), _features = {}, _ifplugin = {}, _iffeature = {}, VERSION = 0.10 } +local compat = require 'repl.compat' +local select = select +local dtraceback = debug.traceback +local setmetatable = setmetatable +local sformat = string.format +local smatch = string.match +local error = error +local setfenv = require('repl.utils').setfenv + +local function gather_results(success, ...) + local n = select('#', ...) + return success, { n = n, ... } +end + +local function tcopy(t, copy) + copy = copy or {} + + for k, v in pairs(t) do + copy[k] = v + end + + return copy +end + +--- Returns the prompt for a given level. +-- @param level The prompt level. Either 1 or 2. +function repl:getprompt(level) + return level == 1 and '>' or '>>' +end + +--- Displays a prompt for the given prompt level. +-- @param level The prompt level. Either 1 or 2. +function repl:prompt(level) + self:showprompt(self:getprompt(level)) +end + +--- Returns the name of the REPL. For usage in chunk compilation. +-- @return The REPL's name. +-- @see load +function repl:name() + return 'REPL' +end + +--- Gets a traceback for an error. +-- @param ... All of the stuff that xpcall passes to error functions. +-- @see xpcall +-- @return A stack trace. The default implementation returns a simple string-based trace. +function repl:traceback(...) + return dtraceback(...) +end + +--- Uses the compilation error to determine whether or not further input +--- is pending after the last line. You shouldn't have to override this +--- unless you use an implementation of Lua that varies in its error +--- messages. +-- @param err The compilation error from Lua. +-- @return Whether or not the input should continue after this line. +function repl:detectcontinue(err) + return smatch(err, "'<eof>'$") or smatch(err, "<eof>$") +end + +function repl:compilechunk(chunk) + return compat.loadstring(chunk, self:name()) +end + +--- Evaluates a line of input, and displays return value(s). +-- @param line The line to evaluate +-- @return The prompt level (1 or 2) +function repl:handleline(line) + local chunk = self._buffer .. line + local f, err = self:compilechunk(chunk) + + if f then + self._buffer = '' + + setfenv(f, self:getcontext()) + local success, results = gather_results(xpcall(f, function(...) return self:traceback(...) end)) + if success then + self:displayresults(results) + else + self:displayerror(results[1]) + end + else + if self:detectcontinue(err) then + self._buffer = chunk .. '\n' + return 2 + else + self:displayerror(err) + self._buffer = '' + end + end + + return 1 +end + +--- Creates a new REPL object, so you can override methods without fear. +-- @return A REPL clone. +function repl:clone() + local plugins_copy = tcopy(self._plugins, setmetatable({}, plugins_lookup_meta)) + local features_copy = tcopy(self._features) + local ifplugin_copy = {} + local iffeature_copy = {} + + for k, v in pairs(self._ifplugin) do + ifplugin_copy[k] = tcopy(v) + end + + for k, v in pairs(self._iffeature) do + iffeature_copy[k] = tcopy(v) + end + + return setmetatable({ + _buffer = '', + _plugins = plugins_copy, + _features = features_copy, + _ifplugin = ifplugin_copy, + _iffeature = iffeature_copy, + }, { __index = self }) +end + +--- Displays the given prompt to the user. Must be overriden. +-- @param prompt The prompt to display. +function repl:showprompt(prompt) + error 'You must implement the showprompt method' +end + +--- Displays the results from evaluate(). Must be overriden. +-- @param results The results to display. The results are a table, with the integer keys containing the results, and the 'n' key containing the highest integer key. + +function repl:displayresults(results) + error 'You must implement the displayresults method' +end + +--- Displays errors from evaluate(). Must be overriden. +-- @param err The error value returned from repl:traceback(). +-- @see repl:traceback +function repl:displayerror(err) + error 'You must implement the displayerror method' +end + +--- Checks whether this REPL object has loaded the given plugin. +-- @param plugin The plugin that the REPL may have loaded. +function repl:hasplugin(plugin) + return self._plugins[plugin] +end + +function repl:hasfeature(feature) + return self._features[feature] +end + +function repl:requirefeature(feature) + if not self:hasfeature(feature) then + error(sformat('required feature %q not present', feature), 2) + end +end + +function repl:ifplugin(plugin, action) + if self:hasplugin(plugin) then + action() + else + local pending_actions = self._ifplugin[plugin] + + if not pending_actions then + pending_actions = {} + self._ifplugin[plugin] = pending_actions + end + + pending_actions[#pending_actions + 1] = action + end +end + +--- If the given feature has been loaded, call `action`. Otherwise, if the +-- feature is ever loaded in the future, call `action` after that loading occurs. +function repl:iffeature(feature, action) + if self:hasfeature(feature) then + action() + else + local pending_features = self._iffeature[feature] + + if not pending_features then + pending_features = {} + self._iffeature[feature] = pending_features + end + + pending_features[#pending_features + 1] = action + end +end + +local function setup_before(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(...) + value(...) + return old_value(...) + end + end + + return setmetatable({}, mt) +end + +local function setup_after(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(...) + local _, results = gather_results(true, old_value(...)) + value(...) + return compat.unpack(results, 1, results.n) + end + end + + return setmetatable({}, mt) +end + +local function setup_around(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(self, ...) + return value(self, old_value, ...) + end + end + + return setmetatable({}, mt) +end + +local function setup_override(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = value + end + + return setmetatable({}, mt) +end + +local function setup_repl(repl) + local mt = {} + + function mt:__newindex(key, value) + local old_value = repl[key] + + if old_value ~= nil then + error(sformat("The '%s' method already exists", key), 2) + end + + repl[key] = value + end + + function mt:__index(key) + local value = repl[key] + + if type(value) == 'function' then + -- XXX cache this? + return function(_, ...) + return value(repl, ...) + end + end + + return value + end + + return setmetatable({}, mt) +end + +-- TODO use lua-procure for this (eventually) +local function findchunk(name) + for _, loader in pairs(compat.package.searchers) do + local chunk = loader(name) + + if type(chunk) == 'function' then + return chunk + end + end + + error('unable to locate plugin', 3) +end + +function repl:loadplugin(chunk) + if self:hasplugin(chunk) then + error(sformat('plugin %q has already been loaded', tostring(chunk)), 2) + end + self._plugins[chunk] = true + + local plugin_actions = self._ifplugin[chunk] + self._ifplugin[chunk] = nil + + if type(chunk) == 'string' then + chunk = findchunk('repl.plugins.' .. chunk) + end + + local plugin_env = { + repl = setup_repl(self), + before = setup_before(self), + after = setup_after(self), + around = setup_around(self), + override = setup_override(self), + init = function() end, + } + + local function ro_globals(_, key, _) + error(sformat('global environment is read-only (key = %q)', key), 2) + end + + plugin_env._G = plugin_env + plugin_env.features = {} + setmetatable(plugin_env, { __index = _G, __newindex = ro_globals }) + + setfenv(chunk, plugin_env) + local _, results = gather_results(nil, chunk()) + + local features = plugin_env.features or {} + + if type(features) == 'string' then + features = { features } + end + + for _, feature in ipairs(features) do + if self._features[feature] then + error(sformat('feature %q already present', feature), 2) + end + + self._features[feature] = true + + local feature_actions = self._iffeature[feature] + self._iffeature[feature] = nil + if feature_actions then + for _, action in ipairs(feature_actions) do + action() + end + end + end + + if plugin_actions then + for _, action in ipairs(plugin_actions) do + action() + end + end + + return compat.unpack(results, 1, results.n) +end + +-- XXX how to guarantee this gets called? +function repl:shutdown() +end + +function repl:getcontext() + return _G +end + +return repl |
