Skip to content

Instantly share code, notes, and snippets.

@loopspace
Created May 21, 2013 19:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loopspace/5622444 to your computer and use it in GitHub Desktop.
Save loopspace/5622444 to your computer and use it in GitHub Desktop.
CModule Release v1.00 -Module loading facility.
--[[
v0.1.0: (5/21/2013)
* added search path for locating relative module paths; search path can be
set/inspected using cmodule.path(); pass no args for a list of projects in the path,
or pass a variable list of projects, i.e. cmodule.path("project1", "project2", "project3").
search path may only be set once.
* added optional fallback project param to cimport/cload and cmodule.loaded/exists/unload;
if a module is not found in the running project or in the search path, an attempt will
be made to load from the fallback project if specified.
* __pathto no longer returns fully qualified path to current module when no
module name is specified; use __pathto(__file) instead.
* no longer generating module closures for cload; use cload(__proj "MyModule") instead.
* renamed overridden module cimport to cinclude; cinclude only takes a module
name, not an absolute path; it is intended for loading from the containing project
only (it is syntactic sugar for cimport(__pathto "MyModule"))
* memoized module cinclude/__pathto closures, which can reduce memory
footprint considerably, and speed up loading/importing as well by not having
to create 2 closures per model per owning project (now it's 2 closures per owning project)
* removed redundant cmodule.import and cmodule.load. use cimport/cload instead.
v0.0.9: (5/17/2013):
* removed cmodule.null. it is no longer necessary since module
module returns are no longer weak referenced, and all modules
loaded with cimport are kept alive unless explicitly unloaded.
* replaced cmodule.nocache with cmodule.cache; to achieve the same effect,
put this line of code in your module file: _M[cmodule.cache] = false
v0.0.8 (5/17/2013):
* cmodule now keeps strong references to module return values
* added API cmodule.unload() to unload a loaded module.
v0.0.7 (5/17/2013):
* cimport/cload now only return 1 value, the value returned from the loaded module
* added cmodule.resolve, which returns the project and file names for the given path
if it is valid, nil otherwise.
v0.0.6 (5/17/2013):
* cmodule.loaded now auto-applies the .lua extension if no extension is specified.
* cmodule.loaded auto pre-pends the running project name if no project is specified
v0.0.5 (5/16/2013):
* fixed bug from 0.0.4 where cmodule would not return nil for newly loaded
modules that return cmodule.null
* added function cmodule.gexport, that accepts a table of key value pairs
that are batch exported to the global environment
* add value cmodule.nocache that can be returned from a module to indicate
* that cmodule should not cache the module (useful for running once-off scripts)
v0.0.4 (5/15/2013):
* added unique null type, accessible via cmodule.null
* cimport will now return nil for modules that return cmodule.null
--]]
-- cmodule
-- v0.1.0
-- todo:
-- * investigate better ways to handle dependency cycles
-- * support exported projects
-- - detect whether program is running in Codea or exported project
-- - reverse engineer exported project file layout
-- - load from alternate location when running an exported project
-- import global functions
local loadstring, fopen = loadstring, io.open
local insert = table.insert
local format, rep = string.format, string.rep
local setfenv, setmetatable = setfenv, setmetatable
local pairs, ipairs = pairs, ipairs
-- loaded modules
local _modules = {}
-- search path projects
local _path = nil
-- unique module environment key to specify caching options
local function opt_cache() return opt_cache end
-- path to Codea's document's directory
local _docs = os.getenv("HOME") .. "/Documents/"
-- name of currently running project
local _this = nil -- set by cmodule()
-- boilerplate function headers for modules
local _importHeader = "return function(_M, __proj, __file, __pathto, cinclude)"
local _loadHeader = "return function(__proj, __file, __pathto)"
-- convert codea path to filesystem path
local function _fspath(p, f)
return _docs .. p .. ".codea/" .. f
end
local function _appendExt(f)
return f:find('%.') and f or (f..".lua")
end
local function _removeExt(f)
local dot = f:find('.', 1, true)
return dot and f:sub(1, dot-1) or f
end
-- parse for module name when compiler exceptions occur
local function _shortsource(s, maxlength)
local max = 60
s, maxlength = s:sub(1, max), maxlength or 30
max = 1 + s:len()
local n, c
for i = 1, max do
n, c = i, s:sub(i, i)
if not(c == ' ' or c == '\n') then
break
end
end
if n == max then return '' end
local start = n
for i = start, max do
c = s:sub(i, i)
if not(c == '\n' or i >= start + maxlength) then
n = i
else break end
end
return s:sub(start, n)
end
-- TODO: when Codea allows optional tab execution,
-- we'll need to take into account added boilerplate line
-- at the top of the module when reporting errors
local function _traceback(minlevel)
minlevel = minlevel or 2
local t = {}
for level = minlevel, math.huge do
local info = debug.getinfo(level, "Sln")
if not info then break end
if info.what == "C" then
insert(t, 'C function');
else
local name, desc = info.name, _shortsource(info.source)
if desc == '' then desc = 'unknown' end
desc = '[' .. desc .. ']'
if name then desc = desc .. ":" .. name end
local currentline = info.currentline
if info.currentline == -1 then currentline = '?' end
s = desc .. ":" .. currentline .. "\n"
insert(t, s)
end
end
return "\ntrace:\n" .. table.concat(t)
end
-- remove the block comment wrapper, and
-- wrap the contents of a file with our boilerplate code
local function _wrap(s, p, t, header)
-- TODO: present error on malformed long comment wrapper
-- find the first occurrence of a long comment, so
-- we can see how many = are in it to refine our search
local b, e = s:find('--%[[%=]*%[')
local ne = rep('[%=]', (e-b)-3)
-- do end first
b, e = s:find('--%]' .. ne .. '%]')
e = s:find('\n', e, true)
local cs = s:sub(b, e):gsub('%]', '%%%]')
s = s:gsub(cs, 'end')
-- now do function header
b, e = s:find('--%[' .. ne .. '%[')
e = s:find('\n', e, true)
-- wrap the source and return it
local oc = '--[[ ' .. p .. ':' .. t .. ' --]] '
cs = s:sub(b, e-1):gsub('%[', '%%%[')
return s:gsub(cs, oc..header)
end
-- check to see if a module has already been loaded;
-- if so, return it's data
local function _findLoaded(cpath, fallback)
local spos = cpath:find(":", 1, true)
if spos ~= nil then
cpath = _appendExt(cpath)
return _modules[cpath], true, cpath, cpath:sub(1, spos-1), cpath:sub(spos+1)
else
-- check running project first
local file, cpath = cpath, _appendExt(_this .. ":" .. cpath)
local mod = _modules[cpath]
if mod ~= nil then
return mod, false, cpath, _this, cpath:sub(#_this+2)
end
-- now check all of the projects in the search path
if _path then
local project
for i = 1, #_path do
project = _path[i]
cpath = _appendExt(project .. ":" .. file)
mod = _modules[cpath]
if mod ~= nil then
return mod, false, cpath, project, cpath:sub(#project+2)
end
end
end
-- check fallback
if fallback then
cpath = _appendExt(fallback .. ":" .. file)
mod = _modules[cpath]
if mod ~= nil then
return mod, false, cpath, fallback, cpath:sub(#fallback+2)
end
end
-- module is not loaded
return nil, false, nil, nil, _appendExt(file)
end
end
-- search for a module's file in the search chain
local function _search(f, fallback)
-- check running project first
local file = fopen(_fspath(_this, f), "r")
if file ~= nil then
return _this, file
end
-- check search path
if _path then
local p = _this
for i = 1, #_path do
p = _path[i]
file = fopen(_fspath(p, f), "r")
if file ~= nil then
return p, file
end
end
end
-- check fallback
if fallback then
file = fopen(_fspath(fallback, f), "r")
if file ~= nil then
return fallback, file
end
end
end
-- load a module given a file, project name, file name, and module header
local function _readmodule(file, p, f, header)
local s = file:read("*a")
file:close()
local tab = _removeExt(f)
local chunk, e = loadstring(_wrap(s, p, tab, header), tab)
if e then
error(format("\n\n%s\n%s", e, _traceback(4)))
end
return chunk
end
-- forward declare for _include
local import
-- memoize overridden cinclude closures
-- since we only need one per owning project.
local _overrides = {}
-- provide syntactic sugar for cimport when overriding
-- search path to load from containing project. e.g.:
-- cinclude "SomeModule"
-- is the same as
-- cimport(__proj "SomeModule")
local function _override(prefix)
local override = _overrides[prefix]
if not override then
override = function(modulename)
return _import(prefix .. modulename)
end
_overrides[prefix] = override
end
return override
end
-- default module metatable/index
local _defaultMT = {__index = _G}
-- memoize __pathto closures, since we only need
-- one per owning project
local _pathto = {}
-- prepare sandboxed environment for loaded modules
local function _makeEnv(p, f, e)
e = setmetatable({}, e and {__index = e} or _defaultMT)
local prefix = p .. ":"
local pathto = _pathto[prefix]
if not pathto then
pathto = function(f) return prefix .. f end
_pathto[prefix] = pathto
end
return e, pathto, prefix
end
-- return a string listing the current project path
local function _pathString()
local spath
if _path and #_path > 0 then
spath = "{"
for i, v in ipairs(_path) do
spath = spath .. v
if i < #_path then
spath = spath .. ", "
end
end
return spath .. "}"
end
return "{none}"
end
-- keep track of modules that are currently loading,
-- so that we can detect dependency cycles
local _loading = {}
-- import a module. imported module is memoized so
-- successive imports do not have to re-load the module
-- each time. If nothing is returned, the module environment
-- itself will be imported (i.e. the module's _M table)
function _import(codeapath, fallback)
local mod, absolute, cpath, p, f = _findLoaded(codeapath)
if mod == nil then
local file
if absolute then
if p then
file = fopen(_fspath(p, f), "r")
end
else
p, file = _search(f, fallback)
if p then cpath = p .. ":" .. f end
end
if not file then
local spath = _pathString()
error(format("Module not found: %s\n\nin search path:%s\n\nfallback:%s\n\n%s", codeapath,
spath, fallback or "none", _traceback(3)))
end
if _loading[cpath] then
error(format("circular dependency detected loading %s\n%s", cpath, _traceback(3)))
end
_loading[cpath] = true
local loaded = _readmodule(file, p, f, _importHeader)()
local env, pathto, prefix = _makeEnv(p, f)
mod = setfenv(loaded, env)(env, p, f, pathto, _override(prefix)) or env
_loading[cpath] = nil
local cache = env[opt_cache]
if cache then
env[opt_cache] = nil
if cache == false then
return mod
end
end
_modules[cpath] = mod
end
return mod
end
-- load a data module. if an environment is provided, it will
-- be the sandboxed module environment. if no environment
-- is provided, the global table will be exposed to the
-- module environment.
local function _load(codeapath, environment, fallback)
if not fallback and type(environment) == "string" then
fallback, environment = environment
end
local spos, p, f, cpath, file = codeapath:find(":", 1, true)
if spos ~= nil then
cpath = _appendExt(codeapath)
p, f = cpath:sub(1, spos-1), cpath:sub(spos+1)
file = fopen(_fspath(p, f), "r")
else
f = _appendExt(codeapath)
p, file = _search(f, fallback)
if p then cpath = p .. ":" .. f end
end
if not file then
local spath = _pathString()
error(format("Module not found: %s\n\nin search path:%s\n\nfallback:%s\n\n%s", codeapath,
spath or "{none}", fallback or "none", _traceback(3)))
end
if _loading[cpath] then
error(format("circular dependency detected loading %s\n%s", cpath, _traceback(3)))
end
_loading[cpath] = true
local loaded = _readmodule(file, p, f, _loadHeader)()
local env, pathto = _makeEnv(p, f, environment)
mod = setfenv(loaded, env)(p, f, pathto) or env
_loading[cpath] = nil
return mod
end
---------------------------
-- exports
---------------------------
-- import a source module; identical to cmodule.import
_G.cimport = _import
-- load a data module; identical to cmodule.load
_G.cload = _load
-- cmodule utitlities
_G.cmodule = setmetatable({
-- set/get the project path. the path may only be set once
path = function(...)
local narg = select("#", ...)
-- return the current path if no params are specified
if narg == 0 then
-- return a copy of the path so it can't be modified
return _path and {unpack(_path)} or nil
end
if _path then
error("cmodule.path may only be set once")
end
_path = {...}
end,
-- return the name of the running project
project = function() return _this end,
-- unload a loaded module
-- params: module name or absolute module path
unload = function(codeapath, fallback)
local mod, _, cpath = _findLoaded(codeapath, fallback)
if mod then
_modules[cpath] = nil
end
end,
-- params: module name or absolute module path
-- returns: true if a module can be located and loaded; else false
exists = function(codeapath, fallback)
local spos = codeapath:find(":", 1, true)
local absolute = (spos ~= nil)
local p, f, cpath, file
if absolute then
cpath = _appendExt(codeapath)
p, f = cpath:sub(1, spos-1), cpath:sub(spos+1)
file = fopen(_fspath(p, f), "r")
else
f = _appendExt(codeapath)
p, file = _search(f, fallback)
if p then cpath = p .. ":" .. f end
end
if file then
file:close()
return true, cpath
end
return false
end,
-- params: none, module name, or absolute module path
-- returns: if no params are specified, array of loaded modules;
-- otherwise, module path if module is found, or nil if not found
loaded = function(codeapath, fallback)
if codeapath then
local _, _, cpath = _findLoaded(codeapath, fallback)
return cpath
else
local loaded = {}
for k in pairs(_modules) do insert(loaded, k) end
return loaded
end
end,
-- split an absolute path into separate project and file components;
-- if relative path is used, currently running project name is appended
-- params: absolute module path or module name in current project
-- returns: project name, tab name
resolve = function(cpath)
cpath = _appendExt(cpath)
local i = cpath:find(":", 1, true)
if i then
return cpath:sub(1, i-1), cpath:sub(i+1), cpath
end
return _this, cpath, _this .. ":" .. cpath
end,
-- utility to export multiple variables to the global namespace
-- params: table containing key/value pairs to be exported to _G
gexport = function(exports)
for k, v in pairs(exports) do
_G[k] = v
end
end,
-- by default, all modules loaded by cimport are cached.
-- to disable caching for a module, add this to your module file:
-- _M[cmodule.cache] = false
cache = opt_cache,
}, {
-- params: project name.
-- returns: cmodule.import, as a convenience.
-- notes: this should be executed before any
-- other code in your program.
__call = function (_, thisProject)
-- set currently running project name
_this = thisProject
-- return import to allow for a chained initialization/import,
-- for example: cmodule "myProject" "setup"
return _import
end,
})
-- Main
-- see cmodule_tests at: https://gist.github.com/apendley/5594141
-- for very basic examples of cmodule usage;
-- better tests are on the way.
GIST_VERSION = 1
function setup()
AutoGist.setProjectInfo("CModule","toadkick","Module loading facility.",GIST_VERSION)
AutoGist.backup(true)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment