eval.lua

Faddle · faddle.git · Tickets

Branches > main > 21dffc2 > files / org / faddle / lang / eval.lua

local LIST = require("org.faddle.lang.list")
local is_list = LIST.is_list
local cons = LIST.cons
local count = LIST.count
local first = LIST.first
local rest = LIST.rest
local apply = LIST.apply
local empty = LIST.empty
local symbol = require("org.faddle.lang.symbol")
local is_symbol = symbol.is_symbol
local intern = symbol.intern
local munge = symbol.munge
local munge_part = symbol.munge_part
local module = symbol.module
local name = symbol.name

local compiler = require("org.faddle.lang.compile")
local compile = compiler.compile
local execute = compiler.execute

local eval

-- Table of module names -> module tables?
-- Or var tables, idk.
-- Right now it's symbols -> objects and maybe var tracking should be separate idk
local modules = {}

-- So this should add both to a registry of vars, as well as the munged names mapping to the values in the module return table
-- And re-defining updates the var and calls its handler or whatever, or calls an update_var() idk, and it also updates the module table
local function def(sym, args, env)
  assert(is_symbol(sym) and not module(sym), "def must be called with a simple symbol")
  assert(modules[env.current_module] ~= nil, "Cannot define var in non-existent module '" .. env.current_module .. "'")
  local val = eval(first(args), env)
  -- Doesn't add to the return table right now
  -- Probably want to munge names so they're easier to use from Lua.
  -- I'm not sure whether like 'foo?' and 'is_foo' should be the same symbol or just munged to produce the same thing.
  local var_name = munge(sym)
  modules[env.current_module][var_name] = val
  return sym
end

local function module_form(mod_name, env)
  if type(mod_name) ~= "string" then error("First argument to module must be a string!") end
  -- This is not the return table but the module object...
  local M = modules[mod_name]
  if not M then
    M = {}
    modules[mod_name] = M
  end
  env.current_module = mod_name
  return mod_name
end

-- Would be nice if these were real functions/macros?
-- And qualified as idk org.faddle.compiler:def that way you can safely shadow def in core and elsewhere.
local DEF = intern("def")
local MODULE = intern("module")
local QUOTE = intern("quote")
local FN = intern("fn")
local LET = intern("let")
local REQUIRE = intern("require")

-- Should eventually be in core/boot ofc they're just here now so I can test functions before figuring out modules
local temp_vars =
{
  [intern("list")] = LIST.list,
  [intern("plus")] = function(...)
    local x = 0
    for i = 1, select("#", ...) do
      local n = select(i, ...)
      if n == nil then n = 0 end
      x = x + n
    end
    return x
  end
}

-- Perhaps special forms should be looked up like anything else, and be shadowable?
-- If you lookup say 'def', it returns nil which is strange
local function lookup(sym, env)
  -- Should fail if you're looking up a non-existent local, but not otherwise
  -- There's currently no way to tell the difference between an undefined local and a nil local
  if not module(sym) then
    -- If special form, error saying so
    return temp_vars[sym] or modules[env.current_module][munge(sym)]
  else
    -- Needs to find the table in the aliases, and then look up that table
    local global_alias = modules[env.current_module][munge_part(module(sym))]
    if global_alias then
      return global_alias[munge_part(name(sym))]
    end

    error("Cannot lookup qualified symbol")
  end

  -- This is always looking up a global
  -- So this is either a symbol with a module or a simple symbol that's been referred...
  -- If the sym refers to a macro, error saying you can't do that
end

local function eval_table(t, env)
  local ret = {}
  for k, v in pairs(t) do
    -- Could end up with holes, but that's the user's fault
    local eval_k = eval(k, env)
    if eval_k == nil then
      error("Table key must be a value, but nothing was provided")
    else
      ret[eval_k] = eval(v, env)
    end
  end
  return ret
end

local function map_eval(lst, env)
  if lst == empty then return lst end
  local res = eval(first(lst), env)
  return cons(res, map_eval(rest(lst), env))
end

local function second(lst) return first(rest(lst)) end

local function call(f, args)
  if type(f) == "number" or type(f) == "string" then
    local t = first(args)
    local not_found = second(args)
    if t == nil then
      return not_found
    else
      local res = t[f]
      if res == nil then return not_found else return res end
    end
  else
    return apply(f, args)
  end
end

local function req(form)
  local mod = second(form)
  assert(type(mod) == "string")
  return require(mod)
end

-- So is this what loads core and alises?
local function create_env(mod_name)
  local env = {current_module = mod_name}
  module_form(mod_name, env)
  return env
end

eval = function(form, env)
  if is_list(form) then
    if count(form) == 0 then return form end
    local f = first(form)
    if f == QUOTE   then return second(form) end
    if f == MODULE  then return module_form(second(form), env) end
    if f == DEF     then return def(second(form), rest(rest(form)), env) end
    if f == FN      then return execute(compile(form, true), "eval_fn") end
    if f == LET     then return execute(compile(form, true), "eval_let") end
    if f == REQUIRE then return req(form) end

    form = map_eval(form, env)
    return call(first(form), rest(form))
  end
  if is_symbol(form) then return lookup(form, env) end
  if type(form) == "table" then return eval_table(form, env) end
  return form -- string, number, function, thread, userdata, nil
end

return {eval = eval, create_env = create_env, modules = modules}