ea04415

Faddle · faddle.git · Tickets

Branches > main > ea04415 > files

Initial Commit

Grahp, 2026-03-22

Commit: ea04415fd684a3b12edbf2ad6cbd9266a77c3f7b

11 changed files:

.gitignore added (view file)

@@ -0,0 +1,3 @@+*.fdl+perf.lua+perf/

LICENSE added (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 added (view file)

@@ -0,0 +1,5 @@+# Faddle++Lisp for Lua.++https://grahp.dev/faddle

faddle.lua added (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 added (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)

Show all 127 lines

src/org/faddle/lang/list.lua added (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 added (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) end+  if t == "string" then return quote(x) end+  if is_list(x) then return print_list(x, seen) end+  if is_symbol(x) then return print_symbol(x) end+  if t == "table" then return print_table(x, seen) end+  if t == "function" then return print_function(x) end+  error("idk how to print that man: " .. t)+end++local function lisp_print(...)+  local cnt = select("#", ...)+  local xs = {"."}+  for i = 1, cnt do+    xs[i] = print_value(select(i, ...), {})+  end+  return table.concat(xs, ", ")+end++return {print = lisp_print}

src/org/faddle/lang/read.lua added (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

Show all 166 lines

src/org/faddle/lang/symbol.lua added (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 added (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 added (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), ".")

Show all 148 lines