--[[ Lair Language LPEG Parser/Builder (BuilderLPEG1) Lair Configuration DSL Case Study H. Conrad Cunningham, Professor Computer and Information Science University of Mississippi Developed for CSCI 658, Software Language Engineering, Fall 2013 1234567890123456789012345678901234567890123456789012345678901234567890 2013-10-21: Developed prototype parser with semantic actions This program uses the Lua Parsing Expression Grammar (LPEG) library to parse the following grammar for a Lair configuration DSL, to capture significant strings during parsing, and to generate the corresponding Lair semantic model objects. Config ::= Item^* Item ::= "item" Name Attribute^* Attribute ::= Uses | Provides | Depends Uses ::= "uses" ResourceInstance Provides ::= "provides" ResourceInstance Depends ::= "requires" Name ResourceInstance ::= Name [ "{" Assign {"," Assign} "}" ] Assign ::= Name "=" Value Name ::= alphabetic followed by alphanumerics Value ::= optional minus followed by one or more digits Limitation: Values can only be integers and identifiers. This should be expanded to include strings. The same configuration used in the other DSL examples can be expressed as follows: item secure_air_vent item acid_bath uses acid {type = hcl, grade = 5} uses electricity {power = 12} item camera uses electricity{power = 1 } item small_power_plant provides electricity {power = 11} requires secure_air_vent --]] -- MODULES INCLUDED -- Load semantic model module and define local names for convenience local sm = require "model" local Configuration, Item, S = sm.Configuration, sm.Item, sm.S local Resource, Electricity, Acid = sm.Resource, sm.Electricity, sm.Acid -- Load LPEG module and define local names for convenience local lpeg = require "lpeg" local C, Ct, P, R, S, V = lpeg.C, lpeg.Ct, lpeg.P, lpeg.R, lpeg.S, lpeg.V -- UTILITY FUNCTIONS -- Function "treeConcat" takes a recursive list of lists in argument -- "t" and returns the corresponding parenthesized traversal string. -- A list is stored in the low positive integer indices of an -- array-style Lua table. This function is intended for formatting -- error messages, debugging, and testing. (This code was borrowed -- from the author's KILT parser utility module.) local function treeConcat(t) if type(t) ~= "table" then return tostring(t) end local res = {} for i = 1, #t do res[i] = treeConcat(t[i]) end return "(" .. table.concat(res," ") .. ")" end -- PARSER -- Lexical Elements local Space = S(" \n\t")^0 local Alpha = R("az") + R("AZ") + P("_") local Num = R("09") local AlphaNum = Alpha + Num local NegSym = P("-") local Kitem = P("item") local Kuses = P("uses") local Kprovides = P("provides") local Krequires = P("requires") local Keyword = Kitem + Kuses + Kprovides + Krequires local Id = (Alpha * AlphaNum^0) - Keyword local OpenBr = P("{") * Space local CloseBr = P("}") * Space local ListSep = P(",") * Space local Equals = P("=") * Space local TItem = Kitem * Space local TUses = C(Kuses) * Space -- capture string local TProvides = C(Kprovides) * Space -- capture string local TRequires = C(Krequires) * Space -- capture string local Number = C(NegSym^-1 * Num^1) * Space -- capture string local Name = C(Id) * Space -- capture string local Value = Number + Name -- Semantic Actions (build semantic domain objects) -- Configuration Context Variable local config local function get_config() return config end -- Argument t = { item1, item2, ..., itemN }, N >= 0 -- Argument not used because item inserted Item in Configuration local function xConfig(t) return config end -- Argument t = { name, {attr_type, value}, ... } local function xItem(t) if not config then -- first item creates configuration config = Configuration:make() end local item = Item:make(t[1]) for i = 2, #t do local att = t[i] local typ = att[1] if lpeg.match(Kuses,typ) then item:add_usage(att[2]) elseif lpeg.match(Kprovides,typ) then item:add_provision(att[2]) elseif lpeg.match(Krequires,typ) then item:add_dependency(config:get_item(att[2])) else error("Unknown attribute type " .. typ .. " for item " .. name) end end config:add_item(item) return item end -- Argument t = {resource_name { property_name, value }, ...} local function xResInst(t) local name = t[1] local resource if name == "acid" then resource = Acid:make() -- properties type and grade supported for i = 2, #t do local prop = t[i] local pn, pv = prop[1], prop[2] if pn == "type" then resource:set_type(tostring(pv)) elseif pn == "grade" then resource:set_grade(tonumber(pv)) else error("Unknown resource Acid property name " ..totstring(pn)) end end elseif name == "electricity" then -- property power required if #t >= 2 and t[2][1] == "power" then resource = Electricity:make(tonumber(t[2][2])) else error("Resource Electricity must have power property.") end else error("Resource type not supported " .. name) end return resource end -- Grammar Rules local Config, Resource, Item = V"Config", V"Resource", V"Item" local Attribute, Uses, Provides, Requires = V"Attribute", V"Uses", V"Provides", V"Requires" local ResInst, PropList, Assign = V"ResInst", V"PropList", V"Assign" local G = P { Config; Config = Ct( (Item)^0 ) / xConfig ; Item = TItem * Ct(Name * (Attribute^0)) / xItem ; Attribute = Uses + Provides + Requires ; Uses = Ct(TUses * ResInst) ; -- captures on Attributes Provides = Ct(TProvides * ResInst) ; Requires = Ct(TRequires * Name) ; ResInst = Ct(Name * (OpenBr * PropList * CloseBr)^-1) / xResInst ; PropList = (Assign * (ListSep * Assign)^0) ; Assign = Ct(Name * Equals * Value) ; } -- Allow leading spaces and disallow any trailing non-space characters G = Space * G * -1 -- Parse Function local function parse(s) assert(type(s) == "string","parse requires string argument.") return lpeg.match(G, s) end -- Variables "header_code" and "trailer_code" hold boilerplate code -- strings that are concatenated in front of and behind, respectively, -- the DSL script being executed by "execute_dsl". local header_code = "" local trailer_code = "" -- Function "execute_dsl" loads a DSL script from a string, compiles -- it, executes it, and then returns the resulting Configuration -- object. local function execute_dsl(scriptFile) -- read dsl script file into string t local f = assert(io.open(scriptFile,"r")) local t = f:read("*a") f:close() -- combine with needed header/trailer and parse script local script = header_code .. t .. trailer_code return parse(script) end -- MODULE EXPORT return { parse = parse, execute_dsl = execute_dsl, get_config = get_config }