git @ Cat's Eye Technologies Decoy / master src / decoy / registry.lua
master

Tree @master (Download .tar.gz)

registry.lua @masterraw · history · blame

--
-- 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