read.lua

Faddle · faddle.git · Tickets

Branches > main > d2ef4c4 > files / org / faddle / lang / read.lua

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}