ea04415
Branches > main > ea04415 > files
Initial Commit
Commit: ea04415fd684a3b12edbf2ad6cbd9266a77c3f7b
11 changed files:
.gitignore view file
@@ -0,0 +1,3 @@+*.fdl+perf.lua+perf/LICENSE view file
@@ -0,0 +1,19 @@+Copyright © 2026 Grahp++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.README.md view file
@@ -0,0 +1,5 @@+# Faddle++Lisp for Lua.++https://grahp.dev/faddlefaddle.lua view file
@@ -0,0 +1,4 @@+-- #!/usr/bin/env lua+package.path = "src/?.lua;" .. package.path++require("org.faddle.repl").repl()src/org/faddle/lang/eval.lua view file
@@ -0,0 +1,126 @@+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 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(var_name, args, env)+ assert(is_symbol(var_name), "def must be called with a symbol")+ 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.+ env[env.current_module][var_name] = val+ return var_name+end++local function module(mod_name, env)+ if type(mod_name) ~= "string" then error("First argument to module must be a string!") end+ if package.loaded[mod_name] and not modules[mod_name] then+ error("Cannot override already defined Lua module " .. mod_name)+ else+ -- 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+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")++-- Should eventually be in core/boot ofc they're just here now so I can test functions before figuring out modules+local temp_functions =+{+ [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)+ return temp_functions[sym] or modules[env.current_module][sym]+end++-- Not quite sure how to do this...+local function eval_table(t, env)+ return t+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)src/org/faddle/lang/list.lua view file
@@ -0,0 +1,88 @@+-- Faddle's linked lists+-- Immutable outside of rawset++local LIST_METATABLE = {}+local FIRST = {}+local REST = {}+local COUNT = {}+local NEWINDEX = function() error("Cannot mutate list!") end++local EMPTY+EMPTY = setmetatable({}, {__index = {[COUNT] = 0, [REST] = EMPTY}, __metatable = LIST_METATABLE, __newindex = NEWINDEX})++local function cons(x, lst)+ if lst == nil then+ return setmetatable({}, {__index = {[COUNT] = 1, [FIRST] = x, [REST] = EMPTY}, __metatable = LIST_METATABLE, __newindex = NEWINDEX})+ elseif getmetatable(lst) ~= LIST_METATABLE then+ error("Cannot cons onto type " .. type(lst), 2)+ else+ return setmetatable({}, {__index = {[COUNT] = lst[COUNT] + 1, [FIRST] = x, [REST] = lst}, __metatable = LIST_METATABLE, __newindex = NEWINDEX})+ end+end++local function is_list(x)+ return getmetatable(x) == LIST_METATABLE+end++local function first(lst)+ if lst == nil then+ return+ elseif getmetatable(lst) == LIST_METATABLE then+ return lst[FIRST]+ else+ error("Cannot call first on type " .. type(lst), 2)+ end+end++local function count(lst)+ if lst == nil then+ return 0+ elseif getmetatable(lst) == LIST_METATABLE then+ return lst[COUNT]+ else+ error("Cannot get the count of a non list, type " .. type(lst), 2)+ end+end++local function rest(lst)+ if lst == nil then+ return EMPTY+ elseif getmetatable(lst) == LIST_METATABLE then+ return lst[REST]+ else+ error("Cannot call rest on type " .. type(lst), 2)+ end+end++-- Just builds up recursively with cons for now, but this could be much more efficient of course.+local function list(...)+ local lst = EMPTY+ local cnt = select("#", ...)+ for i = 1, cnt do+ -- The main branch of cons+ lst = setmetatable({}, {__index = {[COUNT] = i, [FIRST] = select(cnt - i + 1, ...), [REST] = lst}, __metatable = LIST_METATABLE, __newindex = NEWINDEX})+ end+ return lst+end++-- local function iterator(lst) end++local function expand(lst)+ if lst == EMPTY then return end+ return lst[FIRST], expand(lst[REST])+end++local function apply(f, lst)+ if getmetatable(lst) ~= LIST_METATABLE then error("Cannot apply to non-list") end+ if lst[COUNT] >= 256 then error("List is too long to apply, I think maybe idk") end+ return f(expand(lst))+end++return {is_list = is_list,+ cons = cons,+ first = first,+ rest = rest,+ count = count,+ list = list,+ empty = EMPTY,+ apply = apply}src/org/faddle/lang/print.lua view file
@@ -0,0 +1,117 @@+local list = require("org.faddle.lang.list")+local is_list = list.is_list+local first = list.first+local rest = list.rest+local count = list.count+local symbol = require("org.faddle.lang.symbol")+local is_symbol = symbol.is_symbol+local name = symbol.name+-- local module = symbol.module++local function quote(s)+ return '"' .. s:gsub('\\','\\\\')+ :gsub('"','\\"')+ :gsub('\n','\\n')+ :gsub('\r','\\r')+ :gsub('\t','\\t')+ :gsub('%z','\\0')+ .. '"'+end++local function print_symbol(sym)+ -- Symbols are unqualified as of now+ return name(sym)+end++local print_value++local function print_list(x, seen)+ local l = x+ local xs = {}+ while count(l) > 0 do+ xs[#xs+1] = print_value(first(l), seen)+ l = rest(l)+ end+ return "(" .. table.concat(xs, " ") .. ")"+end++-- Could be a good idea if multiple tables with the same "value" are keys, like {[{}] = 1, [{}] = 2}, to print them differently using an id?+-- Otherwise that prints as {{} 1, {} 2} which is quite weird+-- Like [#0x4089324 {} #0x302498 {}] idk.+-- You could also print them with an id so they read back even if they're recursive :D+local function print_associative_table(x, seen)+ if seen[x] then return "..." end+ seen[x] = true++ local xs = {}+ for k, v in pairs(x) do+ xs[#xs+1] = print_value(k, seen) .. " " .. print_value(v, seen)+ end+ return "{" .. table.concat(xs, ", ") .. "}"+end++local function print_sequential_table(x, seen)+ if seen[x] then return "..." end+ seen[x] = true++ local xs = {}+ for i = 1, #x do+ xs[#xs+1] = print_value(x[i], seen)+ end+ return "[" .. table.concat(xs, " ") .. "]"+end++local function is_sequential(t)+ local i = 1+ if #t == 0 then return false end+ for k in pairs(t) do+ if k == i then+ i = i + 1+ else+ return false+ end+ end++ return true+end++local function print_table(x, seen)+ return is_sequential(x) and print_sequential_table(x, seen) or print_associative_table(x, seen)+end++local has_debug = type(debug) == "table" and type(debug.getinfo) == "function"++local debug = debug++local function print_function(f)+ if has_debug then+ local info = debug.getinfo(f)+ local defined = info.short_src and info.linedefined and (info.name and " " or "") .. info.short_src .. ":" .. info.linedefined or "..."+ return "#fn [" .. (info.name or "") .. defined .. "]"+ else+ return "#fn [...]"+ end+end++print_value = function(x, seen)+ local t = type(x)+ if t == "nil" then return "." end+ if t == "boolean" or t == "number" then return tostring(x) endsrc/org/faddle/lang/read.lua view file
@@ -0,0 +1,165 @@+local list = require("org.faddle.lang.list")+local cons = list.cons+local empty = list.empty+local symbol = require("org.faddle.lang.symbol")+local intern = symbol.intern+local is_valid_symbol = symbol.is_valid_symbol++-- comments+-- multi-line strings+-- .f symbols+-- ' reader syntax++local whitespace = {[" "] = true,+ ["\t"] = true,+ ["\n"] = true,+ ["\r"] = true,+ [","] = true}++local delim = {['"'] = true,+ ["("] = true,+ [")"] = true,+ ["["] = true,+ ["]"] = true,+ ["{"] = true,+ ["}"] = true}++local function read(s)+ local pos = 1+ -- If #s returns the number of bytes this would be wrong+ local len = #s++ local read_value++ local function peek()+ return s:sub(pos,pos)+ end++ local function next_char()+ if pos > len then error("EOF while reading") end+ local c = s:sub(pos,pos)+ pos = pos + 1+ return c+ end++ local function skip_whitespace()+ while pos <= len and whitespace[peek()] do+ pos = pos + 1+ end+ end++ -- TODO comments+ local function next_token()+ skip_whitespace()+ if pos > len then error("EOF while reading") end++ local c = peek()++ if delim[c] then+ pos = pos + 1+ return c+ end++ local start = pos+ while pos <= len do+ c = peek()+ if delim[c] or whitespace[c] then+ break+ end+ pos = pos + 1+ end+ return s:sub(start, pos-1)+ end++ -- TODO multi-line strings+ -- Would require you to keep track of indentation...+ local function read_string()+ local buf = {}+ while true do+ local c = next_char()+ if c == '"' then break end+ if c == "\\" then+ local e = next_char()+ if e == "n" then buf[#buf+1] = "\n"+ elseif e == "r" then buf[#buf+1] = "\r"+ elseif e == "t" then buf[#buf+1] = "\t"+ elseif e == "\\" then buf[#buf+1] = "\\"+ elseif e == '"' then buf[#buf+1] = '"'+ elseif e == "0" then buf[#buf+1] = "\0"+ else buf[#buf+1] = e end+ else+ buf[#buf+1] = c+ end+ end+ return table.concat(buf)+ end++ local function read_list()+ local tok = next_token()+ if tok == ")" then return empty end++ return cons(read_value(tok), read_list())+ end++ local function read_sequential_table()+ local arr = {}+ local i = 1+ while true do+ local tok = next_token()+ if tok == "]" then break end+ arr[i] = read_value(tok)+ i = i + 1+ end+ return arr+ end++ local function read_associative_table()+ local t = {}+ while true do+ local k_tok = next_token()+ if k_tok == "}" then break end+ local k = read_value(k_tok)+ local v_tok = next_token()+ if v_tok == "}" then+ error("Associative table requires an even number of elements")+ end+ local v = read_value(v_tok)+ if k ~= nil then+ t[k] = v+ end+ end+ return t+ end++ read_value = function(token)+ token = token or next_token()+ if token == '"' then return read_string() end+ if token == "(" then return read_list() end+ if token == "[" then return read_sequential_table() end+ if token == "{" then return read_associative_table() end+ if token == "true" then return true end+ if token == "false" then return false end+ -- I don't think this is quite right, it has to be : followed by a-Z and a bunch of other things as well...+ if token:sub(1,1) == ":" and #token > 1 then+ return token:sub(2)+ end+ local n = tonumber(token)+ if n ~= nil then return n end+ if is_valid_symbol(token) then return intern(token) end+ error("Don't know how to read: " .. token)+ end++ local values = {}++ while true do+ skip_whitespace()+ if pos > len then break end++ -- read never returns nils+ table.insert(values, read_value())+ end++ return values+end++return {read = read}src/org/faddle/lang/symbol.lua view file
@@ -0,0 +1,68 @@+-- Symbol type+-- Interned and immutable outside of rawset+local symbol_registry = setmetatable({}, {__mode = "v"})++-- TODO is this lua's whole identifier namespace?+-- Also needs to support `.f`, +, -, ?, ->, etc, probably, if that's how I want to do this (bi-directional munging to lua names)+local symbol_regex = "^[%a_][%w_]*$"++local SYMBOL_METATABLE = {}+local NAME = {}+local MODULE = {}+local NEWINDEX = function() error("Cannot mutate list!") end++local function is_valid_part(s)+ return s:match(symbol_regex) ~= nil+end++local function is_valid_symbol(module, name)+ if name == nil then+ if module == "." then return true end+ return is_valid_part(module)+ else+ return is_valid_part(module) and is_valid_part(name)+ end+end++local function is_symbol(x)+ return getmetatable(x) == SYMBOL_METATABLE+end++local function name(sym)+ if is_symbol(sym) then+ return sym[NAME]+ else+ error("Cannot get the symbol name of type " .. type(sym), 2)+ end+end++local function module(sym)+ if is_symbol(sym) then+ return sym[MODULE]+ else+ error("Cannot get the symbol table of type " .. type(sym), 2)+ end+end++-- Eventually munge these names.+-- I guess table is referring to any table but perhaps it should refer strictly to an aliased module...+-- Maybe should be 'module'+local function intern(m, n)+ if not is_valid_symbol(m, n) then+ error("Cannot intern invalid symbol!", 2)+ end+ if n == nil then+ n = m+ m = nil+ end+ local id = (m or "") .. ":" .. n+ if not symbol_registry[id] then+ symbol_registry[id] = setmetatable({}, {__newindex = NEWINDEX, __index = {[NAME] = n, [MODULE] = m}, __metatable = SYMBOL_METATABLE})+ end+ return symbol_registry[id]+end++-- local function munge(sym) end+-- local function demunge(sym) end ?++return {is_symbol = is_symbol, intern = intern, is_valid_symbol = is_valid_symbol, name = name, module = module}src/org/faddle/repl.lua view file
@@ -0,0 +1,29 @@+local p = require("org.faddle.lang.print")+local eval = require("org.faddle.lang.eval").eval+local create_env = require("org.faddle.lang.eval").create_env+local read = require("org.faddle.lang.read").read++local function repl()+ print("Faddle, baby")+ local env = create_env("user")+ while true do+ io.write("> ")+ io.flush()+ local s = io.read()+ local ok_read, forms = pcall(read, s)+ if not ok_read then+ print("Syntax error: " .. forms)+ else+ for _, form in ipairs(forms) do+ local ok, res = pcall(eval, form, env)+ if ok then+ print(p.print(res))+ else+ print("Error: " .. res)+ end+ end+ end+ end+end++return {repl = repl}test.lua view file
@@ -0,0 +1,147 @@+package.path = "src/?.lua;" .. package.path+local LIST = require("org.faddle.lang.list")+local is_list = LIST.is_list+local first = LIST.first+local rest = LIST.rest+local cons = LIST.cons+local count = LIST.count+local list = LIST.list+local apply = LIST.apply+local lisp_print = require("org.faddle.lang.print").print++-- Returns whether 2 objects are structurally equal at the point of calling, even if they are mutable+local function eq(x,y)+ if x == y then return true end+ if is_list(x) and is_list(y) then+ if count(x) ~= count(y) then return false end+ while count(x) > 0 do+ if not eq(first(x), first(y)) then return false end+ x = rest(x)+ y = rest(y)+ end+ return true+ end+ if type(x) == "table" and type(y) == "table" then+ if #x ~= #y then return false end+ for k, v in pairs(x) do+ if not eq(y[k], v) then return false end+ end+ return true+ end+ return false+end++local function teq(x, y)+ if not eq(x, y) then+ error("Test failed! " .. lisp_print(x) .. " and " .. lisp_print(y) .. " are not equal!", 2)+ end+end++local function tneq(x, y)+ if eq(x, y) then+ error("Test failed! " .. lisp_print(x) .. " and " .. lisp_print(y) .. " are equal!", 2)+ end+end++---++print("Running tests...")++--- eq+teq({}, {})+teq({1, true}, {1, true})+teq({1, nil}, {1, nil})+teq({nil, 1}, {nil, 1})+tneq(42, 43)+tneq({1}, {"1"})+tneq(0/0, 0/0)++--- Lists+teq(cons(1, nil), cons(1, nil))+tneq(cons(2, nil), cons(1, nil))+tneq(nil, rest(cons(1, nil)))+teq(count(cons(1, nil)), 1)+teq(count(rest(cons(1, nil))), 0)+teq(first(cons({1}, nil)), {1})+teq(list(1, 2, 3), cons(1, cons(2, cons(3, nil))))+teq(list(), LIST.empty)+teq(apply(list, list(1, 2, 3)), list(1, 2,3))+teq(apply(function(x, y, z) return x + y + z end, list(1, 2, 3)), 6)+tneq(list(1, 2, 3, nil), list(1, 2, 3))+tneq(list(1, 2, 3, nil, nil), list(1, 2, 3, nil))++--- Reader+local read = require("org.faddle.lang.read").read+local function read_one(s) return read(s)[1] end+local intern = require("org.faddle.lang.symbol").intern++teq(read(""), {})+teq(read("1 2 3"), {1, 2, 3})+teq(read("1 2 3")[1], read_one("1 2 3"))+teq(read_one(""), nil)+teq(read_one("42"), 42)+teq(read_one("3.14"), 3.14)+teq(read_one("true"), true)+teq(read_one("false"), false)+tneq(read_one("nil"), nil)+teq(read_one("(1)"), cons(1, nil))+teq(read_one("[]"), {})+teq(read_one("{}"), {})+teq(read_one("foo"), intern("foo"))+teq(read_one("()"), list())+-- Symbol with a table/module... Not sure of the syntax.++teq(read_one('"foo"'), "foo")+teq(read_one(":foo"), "foo")++--- Print+teq(lisp_print(42), "42")+teq(lisp_print(nil), ".")