--
-- decoy.registry
--
-- SPDX-FileCopyrightText: Copyright (c) 2023-2024 Chris Pressey, Cat's Eye Technologies.
-- This work is distributed under a 2-clause BSD license. For more information, see:
-- SPDX-License-Identifier: LicenseRef-BSD-2-Clause-X-Decoy
table = require "table"
io = require "io"
require "decoy.model"
local ast = require "decoy.ast"
local envi = require "decoy.env"
local Parser = require "decoy.parser"
local Desugarer = require "decoy.desugar"
local Evaluator = require "decoy.eval"
local Compiler = require "decoy.compile_js"
local Module = require "decoy.module"
local Registry = {}
-------------------
--[[ UTILS ]]--
-------------------
local try_load_file = function(filename)
local f = io.open(filename, "r")
if f == nil then
return nil
end
local program_text = f:read("*all")
f:close()
return program_text
end
local load_file = function(filename)
local f = assert(io.open(filename, "r"))
local program_text = f:read("*all")
f:close()
return program_text
end
--------------------------------------
--[[ MODULE common processing ]]--
--------------------------------------
--
-- Process an individual toplevel expression.
--
function Registry:process_toplevel(expr, env, module)
if self.evaluator ~= nil then
--
-- We're evaluating
--
debug("registry", "process_toplevel_for_eval: " .. depict(expr))
if ast.Define.is_class_of(expr) then
local dvalue = self.evaluator:eval_expr(expr.defn, env)
env = envi.bind(expr.name, dvalue, env)
module:register_symbol(expr.name, dvalue)
elseif ast.ImportFrom.is_class_of(expr) then
do_debug("registry", function()
print("import_from", env, self, expr.name, render(expr.imports))
end)
env = self:import_from(env, expr.name, expr.imports)
else
expr = self.desugarer:desugar_expr(expr)
local result = self.evaluator:eval_expr(expr, env)
print(depict(result))
end
return env
else
--
-- We're compiling.
-- We don't actually do any compiling here because we just
-- collect it all and compile it after collecting it all.
--
if ast.Define.is_class_of(expr) then
dvalue = expr.defn
env = envi.bind(expr.name, dvalue, env)
module:register_symbol(expr.name, dvalue)
elseif ast.ImportFrom.is_class_of(expr) then
do_debug("registry", function()
print("import_from", env, self, expr.name, render(expr.imports))
end)
env = self:import_from(env, expr.name, expr.imports)
else
-- We do nothing with immediate toplevel expressions.
end
return env
end
end
--
-- Given the program text of a Decoy module, process the top-level
-- expressions in it, using the supplied `process_toplevel` when doing so.
-- This is used to implement both evaluation and compiling, by passing
-- an appropriate `process_toplevel`.
--
-- Top-level expressions are passed the pre-loaded environment, and
-- are given a chance to modify this environment.
--
-- The modules database is updated with this module.
--
-- The expressions parsed from the file, as well as the resulting
-- environment, are returned.
--
function Registry:process_module(program_text)
debug("registry", "process_module: " .. string.sub(program_text, 1, 20))
local module = Module.new()
local env = self.preload_env
local parser = Parser.new(program_text)
local exprs = parser:parse_toplevels()
for j, expr in ipairs(exprs) do
env = self:process_toplevel(expr, env, module)
end
do_debug("registry", function()
module:foreach_symbol(function(name, value)
print(name .. ": " .. depict(value))
end)
end)
module_name = module:get_module_name()
if module_name ~= nil then
self.modules[module_name] = module
else
debug("registry", "WARNING: unnamed module!")
end
-- Compile exprs. We need to do this after we have
-- processed all of them so that they're all in scope
-- so that we can resolve circular references among them.
if self.compiler ~= nil then
for j, expr in ipairs(exprs) do
debug("registry", "compiling: " .. depict(expr))
expr = self.desugarer:desugar_expr(expr)
self.compiler:compile_toplevel(expr, env)
end
end
return exprs, env
end
-----------------------------
--[[ IMPORT handling ]]--
-----------------------------
--
-- Given an environment (which will be modified by return),
-- a module name (as a Lua string), and an import list
-- (either a Lua table whose elements are either symbols
-- or 3-element lists where the middle symbol is `as`,
-- or the Lua string "*" to indicate "all symbols"),
-- locate the module in the module index and load it,
-- returning the modified environment.
--
function Registry:import_from(env, module_name, imports)
debug("registry", "import_from: " .. module_name)
--
-- If the module hasn't been loaded yet, load it.
--
local imported_module = self.modules[module_name]
if imported_module == nil and self.module_path ~= nil then
if self.evaluator ~= nil then
--
-- We are evaluating.
--
-- First, try to load a Lua implementation module.
--
debug("registry", "loading: " .. module_name)
local lua_mod_impl_filename = (
self.module_path .. "lua/" .. module_name .. ".lua"
)
local program_text = try_load_file(lua_mod_impl_filename)
if program_text ~= nil then
local make_module = dofile(lua_mod_impl_filename)
self.modules[module_name] = make_module()
else
--
-- If that didn't work, load a Decoy implementation module.
--
self:process_module_file(
self.module_path .. "decoy/" .. module_name .. ".scm"
)
end
else
--
-- We are compiling.
--
-- First, try to load a target-language (JS...) implementation module.
--
debug("registry", "loading: " .. module_name)
local js_mod_impl_filename = (
self.module_path .. "js/" .. module_name .. ".mjs"
)
local program_text = try_load_file(js_mod_impl_filename)
if program_text ~= nil then
local module = Module.new()
module:populate_from_native(program_text)
self.modules[module_name] = module
--
-- FIXME: this is not where this should go.
-- compile_toplevel should have access to this
-- and should handle it when compiling import-from.
-- Moreover it should only emit the relevant symbols.
--
self.compiler:compile_raw(program_text)
else
--
-- If that didn't work, load a Decoy implementation module.
--
self:process_module_file(
self.module_path .. "decoy/" .. module_name .. ".scm"
)
end
end
imported_module = self.modules[module_name]
elseif self.module_path == nil then
error("No module path to load modules from!")
else
debug("registry", "already loaded: " .. module_name .. ": " .. render(self.modules[module_name]))
end
if imported_module == nil then
error("Could not locate module: " .. module_name)
end
debug("registry", "loaded: " .. render(imported_module))
assert(Module.is_class_of(imported_module), render(imported_module))
--
-- Now it's been ensured the module has loaded, import from it.
--
if imports == "*" then
local symbol_name, symbol_value
imported_module:foreach_symbol(function(symbol_name, symbol_value)
do_debug("registry", function()
print("importing:", symbol_name, depict(symbol_value))
end)
local symbol = Symbol.new(symbol_name)
env = envi.bind(symbol:text(), symbol_value, env)
end)
else
local symbol, imported_symbol_name, exposed_symbol_name
do_debug("registry", function()
print("imports: " .. render(imports))
end)
for _, symbol in ipairs(imports) do
if Cons.is_class_of(symbol) then
if symbol:tail():head():text() ~= "as" then
error("Rename during import must use `as`: " .. depict(symbol))
end
imported_symbol_name = symbol:head():text()
exposed_symbol_name = symbol:tail():tail():head():text()
else
imported_symbol_name = symbol:text()
exposed_symbol_name = imported_symbol_name
end
symbol_value = imported_module:lookup_symbol(imported_symbol_name)
do_debug("registry", function()
print(
"importing from " .. module_name .. ": " ..
imported_symbol_name .. " as " .. exposed_symbol_name ..
" = " .. depict(symbol_value)
)
end)
env = envi.bind(exposed_symbol_name, symbol_value, env)
end
end
return env
end
------------------------
--[[ PUBLIC API ]]--
------------------------
function Registry:set_module_path(path)
self.module_path = path
end
function Registry:preload_module(module_name)
debug("registry", "preload_module: " .. module_name)
self.preload_env = self:import_from(self.preload_env, module_name, "*")
end
function Registry:process_module_file(filename)
program_text = load_file(filename)
local _exprs, _env = self:process_module(program_text)
end
------------------------------
--[[ CLASS DEFINITION ]]--
------------------------------
Registry.new = function(mode)
local evaluator = nil
local compiler = nil
if mode == "eval" then
evaluator = Evaluator.new()
elseif mode == "compile" then
compiler = Compiler.new(io.stdout)
compiler:compile_raw([[
const _is_cons = function(s) {
return (typeof s === 'object' && s !== null && Object.hasOwn(s, "head") && Object.hasOwn(s, "tail"))
}
let depict_tail, render;
const depict_list = function(s) {
if (s === null || s === undefined) {
return "()";
} else {
return "(" + depict_tail(s);
}
}
depict_tail = function(s) {
let x = render(s.head);
if (s.tail === null) {
return x + ")"
} else if (_is_cons(s.tail)) {
return x + " " + depict_tail(s.tail);
} else {
return x + " . " + render(s.tail) + ")";
}
}
render = function(s) {
if (s === true) s = "#t";
else if (s === false) s = "#f";
else if (s === null) s = "()";
else if (typeof s === 'string') s = "\"" + s + "\"";
else if (_is_cons(s)) s = depict_list(s);
return s;
}
const prn = function(obj) {
console.log(render(obj));
};
const truthy = function(b) {
return typeof b === 'boolean' ? b : true;
}
]])
else
error("Mode must be either 'eval' or 'compile'")
end
local self = {
modules = {}, -- map from string to Module object
module_path = nil,
preload_env = {},
desugarer = Desugarer.new(),
evaluator = evaluator,
compiler = compiler,
}
setmetatable(self, {__index = Registry})
return self
end
return Registry