Nov 25 21:37 2013 sandwich_builder.lua Page 1 --[[ Sandwich DSL Builder Using Function Sequence & Context Variables CSci 658: Software Language Engineering, Fall 2013 Exam 1, Problem 3 H. Conrad Cunningham, Professor Computer and Information Science University of Mississippi 1234567890123456789012345678901234567890123456789012345678901234567890 2013-11-25: Prototyped builder module This module implements an internal Sandwich DSL in Lua. It uses Fowler's Function Sequence and Context Variables DSL patterns. The syntax has the structure: platter() sandwich() ingredient commands sandwich() ... end_platter() The ingredient commands consist of the following in any order, but without repetition within a sandwich: bread(b) meats(m1,m2,...) cheeses(c1,c2,...) vegetables(v1,v2,...) condiments(c1,c2,...) Note that only one bread may be specified, but one or more of the other ingredients may be specified in a command. A bread must be specified, but all the other commands are only given if ingredients of the associated type are to be added to the sandwich. Regardless of the order of the commands, the sandwich is constructed in Oxford Standard Order -- bread, meats, cheeses, vegetables, condiments, bread from bottom to top. The call get_platter() extracts the platter from the builder. --]] -- Import Sandwich Semantic Model local sm = require "sandwich_model" local makePlatter, makeSandwich, addSandwich, addLayer = sm.makePlatter, sm.makeSandwich, sm.addSandwich, sm.addLayer -- Available sandwich ingredients. In a more realistic DSL builder, -- it should be possible to easily change definitions such as these. local avail_bread = { white = true, wheat = true, rye = true } Nov 25 21:37 2013 sandwich_builder.lua Page 2 local avail_meat = { turkey = true, chicken = true, ham = true, tofu = true, roastbeef = true } local avail_cheese = { jack = true, cheddar = true, Swiss = true, American = true } local avail_vegetable = { tomato = true, lettuce = true, onion = true } local avail_condiment = { mayo = true, mustard = true, relish = true, Tabasco = true } -- Context variables for Sandwich Builder local cur_platter local cur_sandwich local cur_meats local cur_cheeses local cur_vegetables local cur_condiments -- Utility function map(f,xs) returns the new list resulting from -- application of function "f" to every element of list "xs", -- preserving the relative order of the elements. local function map(f,xs) local ys = {} for i,x in ipairs(xs) do ys[i] = f(x) end return ys end -- Internal procedure clear_sandwich_context() resets the sandwich -- context variables to the initial state. local function clear_sandwich_context() cur_sandwich = nil cur_bread, cur_meats, cur_cheeses, cur_vegetables, cur_condiments = nil, nil, nil, nil, nil end -- DSL command platter() begins a new platter. Internally, the -- command sets the platter into the cur_platter context variable and -- clears the context variables associated with sandwich construction. local function platter() if cur_sandwich then print("Beginning new platter before previous one finished.") end cur_platter = makePlatter() clear_sandwich_context() end Nov 25 21:37 2013 sandwich_builder.lua Page 3 -- Internal procedure finish_sandwich() completes the building of the -- current sandwich in Oxford Standard Order (OSO) and stores it on -- the current platter. local function finish_sandwich() if not cur_bread then -- cannot have sandwich without bread print("Sandwich must have a valid bread selection.") print("... Sandwich description ignored.") clear_sandwich_context() return end addLayer(cur_sandwich,cur_bread) -- bottom bread if cur_meats then for _,m in ipairs(cur_meats) do -- meats addLayer(cur_sandwich,m) end end if cur_cheeses then for _,c in ipairs(cur_cheeses) do -- cheeses addLayer(cur_sandwich,c) end end if cur_vegetables then for _,v in ipairs(cur_vegetables) do -- vegetables addLayer(cur_sandwich,v) end end if cur_condiments then for _,c in ipairs(cur_condiments) do -- condiments addLayer(cur_sandwich,c) end end addLayer(cur_sandwich,cur_bread) -- top bread addSandwich(cur_platter,cur_sandwich) clear_sandwich_context() end -- DSL command end_platter() completes building a platter and makes it -- available to users of the DSL. local function end_platter() if cur_sandwich then finish_sandwich() end end -- Accessor function get_platter() returns the completed platter. If -- the platter building is not yet finished, it first forces it to -- complete. local function get_platter() Nov 25 21:37 2013 sandwich_builder.lua Page 4 if cur_sandwich then end_platter() end return cur_platter end -- DSL command sandwich() ends the previous sandwich and begins -- defining a new one. local function sandwich() if cur_sandwich then finish_sandwich() end cur_sandwich = makeSandwich() cur_bread, cur_meats, cur_cheeses, cur_vegetables, cur_condiments = nil, nil, nil, nil, nil end -- DSL command bread(b) defines the bread for the current sandwich. local function bread(...) local bs = {...} if not cur_bread then local b = bs[1] if avail_bread[b] then cur_bread = b else print("Invalid bread selection, skipping " .. tostring(b)) end if #bs > 1 then local es = map(tostring,bs) table.remove(es,1) print( "Only one bread choice allowed on 'bread' command, skipping " .. table.concat(es,", ") ) end else local es = map(tostring,bs) print("Multiple 'bread' commands, skipping " .. table.concat(es,", ") ) end end -- Design note: Functions meats, cheeses, vegetables, and condiments -- are similar and could be refactored into a higher-order function. -- DSL command meats(m1,m2,...) defines the meats for the current -- sandwich. local function meats(...) local ms = {...} if not cur_meats then cur_meats = {} for _,m in ipairs(ms) do if avail_meat[m] then Nov 25 21:37 2013 sandwich_builder.lua Page 5 cur_meats[#cur_meats+1] = m else print("Skipping invalid meat selection " .. tostring(m)) end end else local es = map(tostring,ms) print("Multiple 'meats' commands, skipping " .. table.concat(es,", ")) end end -- DSL command cheeses(c1,c2,...) defines the cheeses for the current -- sandwich. local function cheeses(...) local cs = {...} if not cur_cheeses then cur_cheeses = {} for _,c in ipairs(cs) do if avail_cheese[c] then cur_cheeses[#cur_cheeses+1] = c else print("Skipping invalid cheese selection " .. tostring(c)) end end else local es = map(tostring,cs) print("Multiple 'cheeses' commands, skipping " .. table.concat(es,", ")) end end -- DSL command vegetables(v1,v2,...) defines the vegetables for the -- current sandwich. local function vegetables(...) local vs = {...} if not cur_vegetables then cur_vegetables = {} for _,v in ipairs(vs) do if avail_vegetable[v] then cur_vegetables[#cur_vegetables+1] = v else print("Skipping invalid vegetable selection " .. tostring(v)) end end else local es = map(tostring,vs) print("Multiple 'vegetables' commands, skipping " .. table.concat(es,", ")) Nov 25 21:37 2013 sandwich_builder.lua Page 6 end end -- DSL command condiments(c1,c2,...) defines the condiments for the -- current sandwich. local function condiments(...) local cs = {...} if not cur_condiments then cur_condiments = {} for _,c in ipairs(cs) do if avail_condiment[c] then cur_condiments[#cur_condiments+1] = c else print("Skipping invalid condiment selection " .. tostring(c)) end end else local es = map(tostring,vs) print("Multiple 'condiments' commands, skipping " .. table.concat(es,", ")) end end -- Module export return { platter = platter, end_platter = end_platter, get_platter = get_platter, sandwich = sandwich, bread = bread, meats = meats, cheeses = cheeses, vegetables = vegetables, condiments = condiments } -- Local methods finish_sandwich, clear_sandwich_context, and map are -- not exported.