require("nellie.helper") -- CLASS DEFINITIONS Expression = class("Expression") -- Construct an expression from items (or not) function Expression:new(items) items = items or {} assert_type("table")(items) self.items = {} for item in iterate(items) do self:insert(item) end end -- Return the iterator through the expression function Expression:iterate() return iterate(self.items) end -- Is the expression empty? function Expression:empty() return #self.items == 0 end --[[ Insert a word or sub expression into this one Note: The expression has a .items = {} list. ]] function Expression:insert(...) -- closures for nvim highlighting local function insert_word(word) -- Whitespace might confuse bindings, remove them local cleaned = word:match("^%s*(.-)%s*$") if cleaned ~= "" then table.insert(self.items,cleaned) end end local function insert_expression(expr) assert_meta(expr,Expression) table.insert(self.items,expr) end for item in iterate({...}) do case_of({ [Expression] = function(expr) insert_expression(expr) end, string = function(word) insert_word(word) end })(type_of(item),item) end end --[[ Converts an expression to meaningful text format. This should be reparseable. ]] function Expression:string() local parts = {} for index,item in pairs(self.items) do parts[index] = tostring(item) end return string.format("(%s)",table.concat(parts," ")) end --[[ Similarly to abstract, returns this expression with subexpression:reduce(depth-1) Useful for more informative debugging ]] function Expression:reduce(depth) local parts = {} for index,item in pairs(self.items) do parts[index] = case_of({ [Expression] = function(item) if depth == 0 then return Expression({"..."}) else return item:reduce(depth - 1) end end, string = id })(type_of(item),item) end end --[[ Returns this expression with all sub expressions simplified to text by :text() or '...' ]] function Expression:abstract(infer) local parts = {} local count = 0 for index,item in pairs(self.items) do parts[index] = case_of({ [Expression] = function(item) local text if infer then text = item:text() if not text then count = count + 1 text = tostring(count) end else text = "..." end return Expression({text}) end, string = id })(type_of(item),item) end return Expression(parts) end --[[ Return the single 'words' part of this expression if it is alone. Otherwise return nil. ]] function Expression:text() -- Get the text local text = self.items[#self.items] -- Ensure that the subexpression is valid abstract if type(text) ~= "string" then return end return text end --[[ Give this expression a meaningful debug location referring to some source code. ]] function Expression:locate(str,uri) self.location = str self.source = uri return self end --[[ Return the meaningful debug location as a string msg. ]] function Expression:link(msg) return string.format( "in '%s' @%s (%s)", msg, tostring(self:abstract()), tostring(self.location or "?"), tostring(self.source or "?.nel") ) end Binding = class("Binding") -- Construct from a neli string to infer a binding -- note that you must wrap this definition in "()"! function Binding:new(expression,callback) assert_meta(Expression)(expression,"Arg 'expression' #1: %s") if type_of(callback) == "function" then self.callback = callback else self.value = callback end self.template = expression:abstract(true) end function Binding:string() return string.format("Binding %s: %s",self.template,tostring(self.value or self.callback)) end function Binding:call(s,e,c) for item in iterate(e) do assert_meta(Expression)(item) end assert_meta(Scope)(s,"Arg 'scope' #1: %s") assert_meta(Chain)(c,"Arg 'chain' #3: %s") return self.value or self.callback(s,e,c) end -- Expecting an abstract expression! function Binding:check(expression,candid) assert_meta(Expression)(expression,"Arg 'expression' #1: %s") candid = candid or expression:abstract() assert_meta(Expression)(candid,"Arg 'abstract' #2: %s") local data = {} local index local loop = true if #candid.items ~= #self.template.items then return false end while loop do local old = index index,template_item = next(self.template.items,old) index,candid_item = next(candid.items,old) local result = case_of({ [Expression] = function() -- item is not an expression, cannot bind if type_of(candid_item) ~= Expression then loop = false else local text = template_item:text() if not text then error( string.format( [[There was a subexpression without just text in this expression: %s]], tostring(candid) ) ) end data[text] = expression.items[index] assert_meta(Expression)(expression.items[index]) end end, string = function() -- words don't match, cannot bind if candid_item ~= template_item then loop = false end end, ["nil"] = function() -- base case where both expressions terminate together if not candid_item then return data end end })(type_of(template_item)) if result then return result end end end Chain = class("Chain") function Chain:new(data) assert_type("string")(data,"Arg 'data' #1: %s") self.data = data end function Chain:from(previous) assert_meta(Chain)(previous,"Arg 'previous' #1: %s") previous.next = self self.previous = previous return self end function Chain:string() return string.format("Chain:%s",table.concat(self:flatten(),", ")) end function Chain:flatten(tab) tab = tab or {} if self.next then self.next:flatten(tab) end table.insert(tab,self.data) return tab end function Chain:pcall(func) local success,result = xpcall(func,function(msg) -- If this is a 'nel:' error, strip the line responsible -- Otherwise just return the whole traceback since something went really bad if not msg then msg = "no message provided" end local match = msg:match("nel:%s(.*)") if match then return match else return "internal: "..debug.traceback(msg,2) end end,self) if not success then log(string.format( "nel: %s\nNEL traceback:\n\t%s", result, table.concat(self:flatten(),"\n\t") )) end return success,result end function Chain:call(func) local success,result = self:pcall(func) if not success then error("Chain: Halt!") end return result end -- The scope of a neli expression under evaluation -- without exact faith to traditional language scopes Scope = class("Scope") function Scope:new(bindings) self.bindings = {} for binding in iterate(bindings or {}) do self:insert(binding) end end function Scope:string() return string.format("Scope: {\n\t%s\n}",table.concat(map(self.bindings,tostring),"\n\t")) end function Scope:insert(binding) assert_meta(Binding)(binding,"Arg 'binding' #1: %s") for competitor in iterate(self.bindings) do if competitor:check(binding.template) then error( string.format( "nel: Conflicting sibling binding in scope for '%s': '%s'.", binding.template, competitor.template ) ) end end table.insert(self.bindings,binding) end function Scope:run(expression,chain,original) original = original or self assert_meta(Expression)(expression,"Arg 'expression' #1: %s") assert_meta(Chain)(chain,"Arg 'chain' #2: %s") assert_meta(Scope,original,"Arg 'scope' #3: %s") -- Could be multiple bindings so make a table local binds = {} local abstract = expression:abstract() for binding in iterate(self.bindings) do local bind = binding:check(expression,abstract) -- Insert into table if bind then binds[binding] = bind end end local binding,bind = next(binds) -- Check for multiple if not bind then if self.parent then return self.parent:run(expression,chain,original) else local extra = "" if #expression.items == 1 then local item = expression.items[1] if type_of(item) == Expression then extra = extra .. "\nNote: This expression is formed (just bracketed) of another single expression. Maybe too many brackets were used here? Are you using curly braces?" else extra = extra .. "\n\tNote: This is a 'word only' expression, it has no subexpressions. It is most likely that an expected object definition was not bound to the scope or there is a typo." end end error( string.format( "nel: No binding for '%s' in scope.%s", tostring(expression), extra ),3 ) end -- Should never happen! elseif next(binds,binding) then local candidates = {} for binding,bind in pairs(binds) do table.insert(candidates,tostring(binding.template)) end error( string.format( "nel: Ambiguous bindings in scope for '%s': %s", tostring(expression:abstract()), table.concat(candidates) ) ,2) else for index,item in pairs(binds) do print("index:",index,"item:",item) end if not binding then error("No binding") end -- todo: improve error return binding:call( original, bind, chain ) end end