diff options
Diffstat (limited to 'Tools/LuaMacro/macro')
-rw-r--r-- | Tools/LuaMacro/macro/Getter.lua | 320 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/TokenList.lua | 201 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/all.lua | 5 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/assert.lua | 74 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/builtin.lua | 161 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/clexer.lua | 169 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/do.lua | 75 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/forall.lua | 70 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/ifelse.lua | 90 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/lambda.lua | 22 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/lc.lua | 343 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/lexer.lua | 179 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/lib/class.lua | 35 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/lib/test.lua | 144 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/module.lua | 132 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/try.lua | 47 | ||||
-rw-r--r-- | Tools/LuaMacro/macro/with.lua | 31 |
17 files changed, 2098 insertions, 0 deletions
diff --git a/Tools/LuaMacro/macro/Getter.lua b/Tools/LuaMacro/macro/Getter.lua new file mode 100644 index 0000000..af58b3c --- /dev/null +++ b/Tools/LuaMacro/macro/Getter.lua @@ -0,0 +1,320 @@ +--- Getter class. Used to get values from the token stream. The first +-- argument `get` of a macro substitution function is of this type. +-- +-- M.define ('\\',function(get,put) +-- local args, body = get:idens('('), get:list() +-- return put:keyword 'function' '(' : idens(args) ')' : +-- keyword 'return' : list(body) : space() : keyword 'end' +-- end) +-- +-- The second argument `put` is a `TokenList` object. +-- @see macro.TokenList +-- @module macro.Getter + +local TokenList = require 'macro.TokenList' +local append = table.insert +local setmetatable = setmetatable + +local Getter = { + __call = function(self) + return self.fun() + end +} +local M = Getter + +Getter.__index = Getter; + +local scan_iter + +function Getter.new (get) + return setmetatable({fun=get},Getter) +end + +function Getter.from_tl(tl) + return Getter.new(scan_iter(tl)) +end + +local Tok = { + __tostring = function(self) + return self[2] + end +} + +local function tok_new (t) + return setmetatable(t,Tok) +end + +-- create a token iterator out of a token list +function Getter.scan_iter (tlist) + local i,n = 1,#tlist + return function(k) + if k ~= nil then + k = i + k + if k < 1 or k > n then return nil end + return tlist[k] + end + local tv = tlist[i] + if tv == nil then return nil end + i = i + 1 + return tv[1],tv[2] + end +end + +scan_iter = Getter.scan_iter + +--- get the next non-whitespace token. +-- @return token type +-- @return token value +-- @function Getter.next +function Getter.next(get) + local t,v = get() + while t == 'space' or t == 'comment' do + t,v = get() + end + return t,v +end + +local TL,LTL = TokenList.new, TokenList.new_list + + +local function tappend (tl,t,val) + val = val or t + append(tl,{t,val}) +end + +--- get a balanced block. +-- Typically for grabbing up to an `end` keyword including any nested +-- `if`, `do` or `function` blocks with their own `end` keywords. +-- @param tok the token stream +-- @param begintokens set of tokens requiring their own nested *endtokens* +-- (default: `{['do']=true,['function']=true,['if']=true}`) +-- @param endtokens set of tokens ending a block (default:`{['end']=true}`) +-- @return list of tokens +-- @return block end token in form `{type,value}` +-- @usage +-- -- copy a balanced table constructor +-- get:expecting '{' +-- put '{':tokens (get:block ({['{']=true}, {['}']=true}) '}') +function Getter.block(tok,begintokens,endtokens) + begintokens = begintokens or {['do']=true,['function']=true,['if']=true} + endtokens = endtokens or {['end']=true} + local level = 1 -- used to count expected matching `endtokens` + local tl = TL() + local token,value + repeat + token,value = tok() + if not token then return nil,'unexpected end of block' end + if begintokens[value] then + level = level + 1 + elseif endtokens[value] then + level = level - 1 + end + if level > 0 then -- final end token is returned separately + tappend(tl,token,value) + end + until level == 0 + return tl,tok_new{token,value} +end + +--- get a delimited list of token lists. +-- Typically used for grabbing argument lists like ('hello',a+1,fred(c,d)); will count parens +-- so that the delimiter (usually a comma) is ignored inside sub-expressions. You must have +-- already read the start token of the list, e.g. open parentheses. It will eat the end token +-- and return the list of TLs, plus the end token. Based on similar code in Penlight's +-- `pl.lexer` module. +-- @param tok the token stream +-- @param endt the end token (default ')') +-- @param delim the delimiter (default ',') +-- @return list of token lists +-- @return end token in form {type,value} +function Getter.list(tok,endtoken,delim) + endtoken = endtoken or ')' + delim = delim or ',' + local parm_values = LTL() + local level = 1 -- used to count ( and ) + local tl = TL() + local is_end + if type(endtoken) == 'function' then + is_end = endtoken + elseif endtoken == '\n' then + is_end = function(t,val) + return t == 'space' and val:find '\n' + end + else + is_end = function (t) + return t == endtoken + end + end + local token,value = tok() + if is_end(token,value) then return parm_values end + if token == 'space' then + token,value = tok() + end + while true do + if not token then return nil,'unexpected end of list' end -- end of stream is an error! + if is_end(token,value) and level == 1 then + append(parm_values,tl) + break + elseif token == '(' then + level = level + 1 + tappend(tl,'(') + elseif token == ')' then + level = level - 1 + if level == 0 then -- finished with parm list + append(parm_values,tl) + break + else + tappend(tl,')') + end + elseif token == '{' then + level = level + 1 + tappend(tl,'{') + elseif token == '}' then + level = level - 1 + tappend(tl,'}') + elseif token == delim and level == 1 then + append(parm_values,tl) -- a new parm + tl = TL() + else + tappend(tl,token,value) + end + token,value=tok() + end + return parm_values,tok_new{token,value} +end + +function Getter.upto_keywords (k1,k2) + return function(t,v) + return t == 'keyword' and (v == k1 or v == k2) + end,'' +end + +local tnext = Getter.next + + +function Getter.upto(tok,k1,k2) + local endt = k1 + if type(k1) == 'string' and k1:match '^%a+$' then + endt = Getter.upto_keywords(k1,k2) + end + local ltl,tok = tok:list(endt,'') + M.assert(ltl ~= nil and #ltl > 0,'failed to grab tokens') + return ltl[1],tok +end + +function Getter.line(tok) + return tok:upto(function(t,v) + return (t=='space' and v:match '\n') or t == 'comment' + end) +end + + +local function prettyprint (t, v) + v = v:gsub ("\n", "\\n") + if t == "string" then + if #v > 16 then v = v:sub(1,12).."..."..v:sub(1,1) end + return t.." "..v + end + if #v > 16 then v = v:sub(1,12).."..." end + if t == "space" or t == "comment" or t == "keyword" then + return t.." '"..v.."'" + elseif t == v then + return "'"..v.."'" + else + return t.." "..v + end +end + +--- get the next identifier token. +-- (will be an error if the token has wrong type) +-- @return identifier name +function Getter.iden(tok) + local t,v = tnext(tok) + M.assert(t == 'iden','expecting identifier, got '..prettyprint(t,v)) + return v +end + +Getter.name = Getter.iden -- backwards compatibility! + +--- get the next number token. +-- (will be an error if the token has wrong type) +-- @return converted number +function Getter.number(tok) + local t,v = tnext(tok) + M.assert(t == 'number','expecting number, got '..prettyprint(t,v)) + return tonumber(v) +end + +--- get a delimited list of identifiers. +-- works like list. +-- @param tok the token stream +-- @param endt the end token (default ')') +-- @param delim the delimiter (default ',') +-- @see list +function Getter.idens(tok,endt,delim) + local ltl,err = tok:list(endt,delim) + if not ltl then error('idens: '..err) end + local names = {} + -- list() will return {{}} for an empty list of tlists + for i = 1,#ltl do + local tl = ltl[i] + local tv = tl[1] + if tv then + if tv[1] == 'space' then tv = tl[2] end + names[i] = tv[2] + end + end + return names, err +end + +Getter.names = Getter.idens -- backwards compatibility! + +--- get the next string token. +-- (will be an error if the token has wrong type) +-- @return string value (without quotes) +function Getter.string(tok) + local t,v = tok:expecting("string") + return v:sub(2,-2) +end + +--- assert that the next token has the given type. This will throw an +-- error if the next non-whitespace token does not match. +-- @param type a token type ('iden','string',etc) +-- @param value a token value (optional) +-- @usage get:expecting '(' +-- @usage get:expecting ('iden','bonzo') +function Getter.expecting (tok,type,value) + local t,v = tnext(tok) + if t ~= type then M.error ("expected "..type.." got "..prettyprint(t,v)) end + if value then + if v ~= value then M.error("expected "..value.." got "..prettyprint(t,v)) end + end + return t,v +end + +--- peek ahead or before in the token stream. +-- @param k positive delta for looking ahead, negative for looking behind. +-- @param dont_skip true if you want to check for whitespace +-- @return the token type +-- @return the token value +-- @return the token offset +-- @function Getter.peek + +--- peek ahead two tokens. +-- @return first token type +-- @return first token value +-- @return second token type +-- @return second token value +-- @function Getter.peek2 + +--- patch the token stream at the end. +-- @param idx index in output table +-- @param text to replace value at that index +-- @function Getter.patch + +--- put out a placeholder for later patching. +-- @param put a putter object +-- @return an index into the output table +-- @function Getter.placeholder + +return Getter diff --git a/Tools/LuaMacro/macro/TokenList.lua b/Tools/LuaMacro/macro/TokenList.lua new file mode 100644 index 0000000..a18ac67 --- /dev/null +++ b/Tools/LuaMacro/macro/TokenList.lua @@ -0,0 +1,201 @@ +--------------- +-- A TokenList class for generating token lists. +-- +-- There are also useful `get_` methods for extracting values from +-- the first token. +-- +-- @module macro.TokenList + +local TokenList = {} +local M = TokenList +TokenList.__index = TokenList + +local append = table.insert + +function TokenList.new (tl) + return setmetatable(tl or {},TokenList) +end + +local TokenListList = {} + +function TokenList.new_list (ltl) + return setmetatable(ltl or {},TokenListList) +end + +TokenListList.__index = function(self,key) + local m = TokenList[key] + return function(self,...) + local res = {} + for i = 1,#self do res[i] = m(self[i],...) end + return TokenList.new_list(res) + end +end + +-- token-getting helpers + + +local function extract (tl) + local tk = tl[1] + if tk[1] == 'space' then + tk = tl[2] + end + return tk +end + +--- get an identifier from front of a token list. +-- @return identifier name +function TokenList.get_iden (tl) + local tk = extract(tl) + M.assert(tk[1]=='iden','expecting identifier') + return tk[2] +end + +--- get an number from front of a token list. +-- @return number +function TokenList.get_number(tl) + local tk = extract(tl) + M.assert(tk[1]=='number','expecting number') + return tonumber(tk[2]) +end + +--- get a string from front of a token list. +-- @return string value (without quotes) +function TokenList.get_string(tl) + local tk = extract(tl) + M.assert(tk[1]=='string') + return tk[2]:sub(2,-2) -- watch out! what about long string literals?? +end + +--- takes a token list and strips spaces and comments. +-- @return new tokenlist +function TokenList.strip_spaces (tl) + local out = TokenList.new() + for _,t in ipairs(tl) do + if t[1] ~= 'comment' and t[1] ~= 'space' then + append(out,t) + end + end + return out +end + +--- pick the n-th token from this tokenlist. +-- Note that it returns the value and type, not the type and value. +-- @param n (1 to #self) +-- @return token value +-- @return token type +function TokenList.pick (tl,n) + local t = tl[n] + return t[2],t[1] +end + +-- token-putting helpers +local comma,space = {',',','},{'space',' '} + +--- append an identifier. +-- @param name the identifier +-- @param no_space true if you don't want a space after the iden +-- @return self +function TokenList.iden(res,name,no_space) + append(res,{'iden',name}) + if not no_space then + append(res,space) + end + return res +end + +TokenList.name = TokenList.iden -- backwards compatibility! + +--- append a string. +-- @param s the string +-- @return self +function TokenList.string(res,s) + append(res,{'string','"'..s..'"'}) + return res +end + +--- append a number. +-- @param val the number +-- @return self +function TokenList.number(res,val) + append(res,{'number',val}) + return res +end + +--- put out a list of identifiers, separated by commas. +-- @param res output token list +-- @param names a list of identifiers +-- @return self +function TokenList.idens(res,names) + for i = 1,#names do + res:iden(names[i],true) + if i ~= #names then append(res,comma) end + end + return res +end + +TokenList.names = TokenList.idens -- backwards compatibility! + +--- put out a token list. +-- @param res output token list +-- @param tl a token list +-- @return self +function TokenList.tokens(res,tl) + for j = 1,#tl do + append(res,tl[j]) + end + return res +end + +--- put out a list of token lists, separated by commas. +-- @param res output token list +-- @param ltl a list of token lists +-- @return self +function TokenList.list(res,ltl) + for i = 1,#ltl do + res:tokens(ltl[i]) + if i ~= #ltl then append(res,comma) end + end + return res +end + +--- put out a space token. +-- @param res output token list +-- @param space a string containing only whitespace (default ' ') +-- @return self +function TokenList.space(res,space) + append(res,{'space',space or ' '}) + return res +end + +--- put out a keyword token. +-- @param res output token list +-- @param keyw a Lua keyword +-- @param no_space true if you don't want a space after the iden +-- @return self +function TokenList.keyword(res,keyw,no_space) + append(res,{'keyword',keyw}) + if not no_space then + append(res,space) + end + return res +end + +--- convert this tokenlist into a string. +function TokenList.__tostring(tl) + local res = {} + for j = 1,#tl do + append(res,tl[j][2]) + end + return table.concat(res) +end + +--- put out a operator token. This is the overloaded call operator +-- for token lists. +-- @param res output token list +-- @param keyw an operator string +function TokenList.__call(res,t,v) + append(res,{t,v or t}) + return res +end + +return TokenList diff --git a/Tools/LuaMacro/macro/all.lua b/Tools/LuaMacro/macro/all.lua new file mode 100644 index 0000000..0d7c098 --- /dev/null +++ b/Tools/LuaMacro/macro/all.lua @@ -0,0 +1,5 @@ +require 'macro.forall'
+require 'macro.lambda'
+require 'macro.try'
+require 'macro.do'
+
diff --git a/Tools/LuaMacro/macro/assert.lua b/Tools/LuaMacro/macro/assert.lua new file mode 100644 index 0000000..b25daaf --- /dev/null +++ b/Tools/LuaMacro/macro/assert.lua @@ -0,0 +1,74 @@ +--- a simple testing framework. +-- Defines a single statment macro assert_ which has the following syntax: +-- +-- - assert_ val1 == val2 +-- - assert_ val1 > val2 +-- - assert_ val1 < val2 +-- - assert_ val1 matches val2 (using string matching) +-- - assert_ val1 throws val2 (ditto, on exception string) +-- +-- The `==` case has some special forms. If `val2` is `(v1,v2,..)` then +-- it's assumed that the expression `val1` returns multiple values. `==` will +-- also do value equality for plain tables. If `val2` is a number given in +-- %f format (such as 3.14) then it will match `vall` up to that specified +-- number of digits. +-- +-- assert_ {one=1,two=2} == {two=2,one=1} +-- assert_ 'hello' matches '^hell' +-- assert_ 2 > 1 +-- assert_ ('hello'):find 'll' == (3,4) +-- assert_ a.x throws 'attempt to index global' +-- @module macro.assert + +local M = require 'macro' +local relop = { + ['=='] = 'eq', + ['<'] = 'lt', + ['>'] = 'gt' +} + +local function numfmt (x) + local int,frac = x:match('(%d+)%.(%d+)') + if not frac then return nil end + return '%'..#x..'.'..#frac..'f', x +end + +--- assert that two values match the desired relation. +-- @macro assert_ +M.define('assert_',function(get,put) + local testx,tok = get:upto(function(t,v) + return relop[t] or (t == 'iden' and (v == 'matches' or v == 'throws')) + end) + local testy,eos = get:line() + local otesty = testy + testx = tostring(testx) + testy = tostring(testy) + local t,v,op = tok[1],tok[2] + if relop[t] then + op = relop[t] + if t == '==' then + if testy:match '^%(.+%)$' then + testx = 'T_.tuple('..testx..')' + testy = 'T_.tuple'..testy + elseif #otesty == 1 and otesty[1][1] == 'number' then + local num = otesty[1][2] + local fmt,num = numfmt(num) + if fmt then -- explicit floating-point literal + testy = '"'..num..'"' + testx = '("'..fmt..'"):format('..testx..')' + op = 'match' + end + end + end + elseif v == 'matches' then + op = 'match' + elseif v == 'throws' then + op = 'match' + testx = 'T_.pcall_no(function() return '..testx..' end)' + end + return ('T_.assert_%s(%s,%s)%s'):format(op,testx,testy,tostring(eos)) +end) + +return function() + return "T_ = require 'macro.lib.test'" +end diff --git a/Tools/LuaMacro/macro/builtin.lua b/Tools/LuaMacro/macro/builtin.lua new file mode 100644 index 0000000..12c8b38 --- /dev/null +++ b/Tools/LuaMacro/macro/builtin.lua @@ -0,0 +1,161 @@ +------- +-- LuaMacro built-in macros. +-- @module macro.builtin + +local M = require 'macro' + +local function macro_def (scoped) + return function (get) + local t,name,parms,openp + local t,name = get:next() + local upto,ret + if t == '(' then + t,name = get:next() + upto = function(t,v) return t == ')' end + else + upto = function(t,v) + return t == 'space' and v:find '\n' + end + -- return space following (returned by copy_tokens) + ret = true + end + -- might be immediately followed by a parm list + t,openp = get() + if openp == '(' then + parms = get:names() + end + -- the actual substitution is up to the end of the line + local args, space = M.copy_tokens(get,upto) + if scoped then + M.define_scoped(name,args,parms) + else + M.set_macro(name,args,parms) + end + return ret and space[2] + end +end + +--- a macro for defining lexically scoped simple macros. +-- def_ may be followed by an arglist, and the substitution is the +-- rest of the line. +-- @usage def_ block (function() _END_CLOSE_ +-- @usage def_ sqr(x) ((x)*(x)) +-- @macro def_ +M.define ('def_',macro_def(true)) + +--- a global version of `def_`. +-- @see def_ +-- @macro define_ +M.define ('define_',macro_def(false)) + +--- set the value of an existing macro. +-- the name and value follows immediately, and the value must be +-- a single token +-- @usage set_ T 'string' +-- @usage set_ F function +-- @macro set_ +M.define('set_',function(get) + local name = get:name() + local t,v = get:next() + M.set_macro(name,{{t,v}}) +end) + +--- undefining identifier macros. +-- @macro undef_ +M.define('undef_',function(get) + M.set_macro(get:name()) +end) + +--- Insert text after current block end. `_END_` is followed by a quoted string +-- and is used to insert that string after the current block closes. +-- @macro _END_ +M.define ('_END_',function(get) + local str = get:string() + M.block_handler(-1,function(get,word) + if word ~= 'end' then return nil,true end + return str + end) +end) + +--- insert an end after the next closing block. +-- @macro _END_END_ +-- @see _END_ +M.define '_END_END_ _END_ " end"' + +--- insert a closing parens after next closing block. +-- @usage def_ begin (function() _END_CLOSE_ +-- fun begin ... end --> fun (function() ... end) +-- @macro _END_CLOSE_ +-- @see _END_ +M.define '_END_CLOSE_ _END_ ")"' + +--- 'stringizing' macro. +-- Will convert its argument into a string. +-- @usage def_ _assert(x) assert(x,_STR_(x)) +-- @macro _STR_ +M.define('_STR_(x)',function(x) + x = tostring(x) + local put = M.Putter() + return put '"':name(x) '"' +end) + +-- macro stack manipulation + + +--- push a value onto a given macro' stack. +-- @macro _PUSH_ +-- @param mac existing macro name +-- @param V a string +M.define('_PUSH_(mac,V)',function(mac,V) + M.push_macro_stack(mac:get_string(),V:get_string()) +end) + +--- pop a value from a macro's stack. +-- @macro _POP_ +-- @param mac existing macro name +-- @return a string +-- @see _PUSH_ +M.define('_POP_',function(get,put) + local val = M.pop_macro_stack(get:string()) + if val then + return put(val) + end +end) + +--- drop the top of a macro's stack. +-- Like `_POP_`, except that it does not return the value +-- @macro _DROP_ +-- @return existing macro name +-- @see _POP_ +M.define('_DROP_',function(get) + M.pop_macro_stack(get:string()) +end) + +--- Load a Lua module immediately. This allows macro definitions to +-- to be loaded before the rest of the file is parsed. +-- If the module returns a function, then this is assumed to be a +-- substitution function, allowing macro modules to insert code +-- at this point. +-- @macro require_ +M.define('require_',function(get,put) + local name = get:string() + local ok,fn = pcall(require,name) + if not ok then + fn = require('macro.'..name) + end + if type(fn) == 'function' then + return fn(get,put) + end +end) + +--- Include the contents of a file. This inserts the file directly +-- into the token stream, and is equivalent to cpp's `#include` directive. +-- @macro include_ +M.define('include_',function(get) + local str = get:string() + local f = M.assert(io.open(str)) + local txt = f:read '*a' + f:close() + M.push_substitution(txt) +end) + diff --git a/Tools/LuaMacro/macro/clexer.lua b/Tools/LuaMacro/macro/clexer.lua new file mode 100644 index 0000000..fd859a8 --- /dev/null +++ b/Tools/LuaMacro/macro/clexer.lua @@ -0,0 +1,169 @@ +--[[--- A C lexical scanner using LPeg. += CREDITS += based on the C lexer in Peter Odding's lua-lxsh +@module macro.clexer +--]] + +local clexer = {} +local lpeg = require 'lpeg' +local P, R, S, C, Cc, Ct = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Ct + +-- create a pattern which captures the lua value [id] and the input matching +-- [patt] in a table +local function token(id, patt) return Ct(Cc(id) * C(patt)) end + +-- private interface +local table_of_tokens +local extra_tokens + +function clexer.add_extra_tokens(extra) + extra_tokens = extra_tokens or {} + for _,t in ipairs(extra) do + table.insert(extra_tokens,t) + end + table_of_tokens = nil -- re-initialize +end + +function clexer.init () + local digit = R('09') + + local upp, low = R'AZ', R'az' + local oct, dec = R'07', R'09' + local hex = dec + R'AF' + R'af' + local letter = upp + low + local alnum = letter + dec + '_' + local endline = S'\r\n\f' + local newline = '\r\n' + endline + local escape = '\\' * ( newline + + S'\\"\'?abfnrtv' + + (#oct * oct^-3) + + ('x' * #hex * hex^-2)) + + + -- range of valid characters after first character of identifier + local idsafe = R('AZ', 'az', '\127\255') + P '_' + + -- operators + local OT = P '==' + if extra_tokens then + for _,ex in ipairs(extra_tokens) do + OT = OT + P(ex) + end + end + local operator = token('operator', OT + P '.' + P'>>=' + '<<=' + '--' + '>>' + '>=' + '/=' + '==' + '<=' + + '+=' + '<<' + '*=' + '++' + '&&' + '|=' + '||' + '!=' + '&=' + '-=' + + '^=' + '%=' + '->' + S',)*%+&(-~/^]{}|.[>!?:=<;') + -- identifiers + local ident = token('iden', idsafe * (idsafe + digit) ^ 0) + + -- keywords + local keyword = token('keyword', (P 'auto' + P 'break' + P 'case' + P'char' + + P 'const' + P 'continue' + P 'default' + + P 'do' + P 'double' + P 'else' + P 'enum' + P 'extern' + P 'float' + + P 'for' + P 'goto' + P 'if' + P 'int' + P 'long' + P 'register' + + P 'return' + P 'short' + P 'signed' + P 'sizeof' + P 'static' + + P 'struct' + P 'switch' + P 'typedef' + P 'union' + P 'void' + + P 'volatile' + P 'while') * -(idsafe + digit)) + + -- numbers + local number_sign = S'+-'^-1 + local number_decimal = digit ^ 1 + local number_hexadecimal = P '0' * S 'xX' * R('09', 'AF', 'af') ^ 1 + local number_float = (digit^1 * P'.' * digit^0 + P'.' * digit^1) * + (S'eE' * number_sign * digit^1)^-1 + local number = token('number', number_hexadecimal + + number_float + + number_decimal) + + + local string = token('string', '"' * ((1 - S'\\\r\n\f"') + escape)^0 * '"') + local char = token('char',"'" * ((1 - S"\\\r\n\f'") + escape) * "'") + + -- comments + local singleline_comment = P '//' * (1 - S '\r\n\f') ^ 0 + local multiline_comment = '/*' * (1 - P'*/')^0 * '*/' + local comment = token('comment', multiline_comment + singleline_comment) + local prepro = token('prepro',P '#' * (1 - S '\r\n\f') ^ 0) + + -- whitespace + local whitespace = token('space', S('\r\n\f\t ')^1) + + -- ordered choice of all tokens and last-resort error which consumes one character + local any_token = whitespace + number + keyword + ident + + string + char + comment + prepro + operator + token('error', 1) + + + table_of_tokens = Ct(any_token ^ 0) +end + +-- increment [line] by the number of line-ends in [text] +local function sync(line, text) + local index, limit = 1, #text + while index <= limit do + local start, stop = text:find('\r\n', index, true) + if not start then + start, stop = text:find('[\r\n\f]', index) + if not start then break end + end + index = stop + 1 + line = line + 1 + end + return line +end +clexer.sync = sync + +clexer.line = 0 + +-- we only need to synchronize the line-counter for these token types +local multiline_tokens = { comment = true, space = true } +clexer.multiline_tokens = multiline_tokens + +function clexer.scan_c_tokenlist(input) + if not table_of_tokens then + clexer.init() + end + assert(type(input) == 'string', 'bad argument #1 (expected string)') + local line = 1 + local tokens = lpeg.match(table_of_tokens, input) + for i, token in pairs(tokens) do + local t = token[1] + if t == 'operator' or t == 'error' then + token[1] = token[2] + end + token[3] = line + if multiline_tokens[t] then + line = sync(line, token[2]) + end + end + return tokens +end + +--- get a token iterator from a source containing Lua code. +-- S is the source - can be a string or a file-like object (i.e. read() returns line) +-- Note that this token iterator includes spaces and comments, and does not convert +-- string and number tokens - so e.g. a string token is quoted and a number token is +-- an unconverted string. +function clexer.scan_c(input,name) + if type(input) ~= 'string' and input.read then + input = input:read('*a') + end + local tokens = clexer.scan_c_tokenlist(input) + local i, n = 1, #tokens + return function(k) + if k ~= nil then + k = i + k + if k < 1 or k > n then return nil end + return tokens[k] + end + local tok = tokens[i] + i = i + 1 + if tok then + clexer.line = tok[3] + clexer.name = name + return tok[1],tok[2] + end + end + +end + +return clexer diff --git a/Tools/LuaMacro/macro/do.lua b/Tools/LuaMacro/macro/do.lua new file mode 100644 index 0000000..45cf84c --- /dev/null +++ b/Tools/LuaMacro/macro/do.lua @@ -0,0 +1,75 @@ +--- An intelligent 'loop-unrolling' macro. +-- `do_` defines a named scoped macro `var` which is the loop iterator. +-- +-- For example, +-- +-- y = 0 +-- do_(i,1,10 +-- y = y + i +-- ) +-- assert(y == 55) +-- +-- `tuple` is an example of how the expansion of a macro can be +-- controlled by its context. Normally a tuple `A` expands to +-- `A_1,A_2,A_3` but inside `do_` it works element-wise: +-- +-- tuple(3) A,B +-- def_ do3(stmt) do_(k,1,3,stmt) +-- do3(A = B/2) +-- +-- This expands as +-- +-- A_1 = B_1/2 +-- A_2 = B_2/2 +-- A_3 = B_3/2 +-- +-- @module macro.do +local M = require 'macro' + +--- Expand a loop inline. +-- @p var the loop variable +-- @p start initial value of `var` +-- @p finish final value of `var` +-- @p stat the statement containing `var` +-- @macro do_ +M.define('do_(v,s,f,stat)',function(var,start,finish,statements) + -- macros with specified formal args have to make their own putter, + -- and convert the actual arguments to the type they expect. + local put = M.Putter() + var,start,finish = var:get_iden(),start:get_number(),finish:get_number() + M.push_macro_stack('do_',var) + -- 'do_' works by setting the variable macro for each value + for i = start, finish do + put:name 'set_':name(var):number(i):space() + put:tokens(statements) + end + put:name 'undef_':name(var) + put:name '_DROP_':string 'do_':space() + return put +end) + +--- an example of conditional expansion. +-- `tuple` takes a list of variable names, like a declaration list except that it +-- must end with a line end. +-- @macro tuple +M.define('tuple',function(get) + get:expecting '(' + local N = get:number() + get:expecting ')' + local names = get:names '\n' + for _,name in ipairs(names) do + M.define(name,function(get,put) + local loop_var = M.value_of_macro_stack 'do_' + if loop_var then + local loop_idx = tonumber(M.get_macro_value(loop_var)) + return put:name (name..'_'..loop_idx) + else + local out = {} + for i = 1,N do + out[i] = name..'_'..i + end + return put:names(out) + end + end) + end +end) diff --git a/Tools/LuaMacro/macro/forall.lua b/Tools/LuaMacro/macro/forall.lua new file mode 100644 index 0000000..8ec5c68 --- /dev/null +++ b/Tools/LuaMacro/macro/forall.lua @@ -0,0 +1,70 @@ +-------------------- +-- `forall` statement. +-- The syntax is `forall VAR SELECT [if CONDN] do` where +-- `SELECT` is either `in TBL` or `= START,FINISH` +-- +-- For example, +-- +-- forall name in {'one','two'} do print(name) end +-- +-- forall obj in get_objects() if obj:alive() then +-- obj:action() +-- end +-- +-- Using `forall`, we also define _list comprehensions_ like +-- `L{s:upper() | s in names if s:match '%S+'}` +-- +-- @module macro.forall + +local M = require 'macro' + +--- extended for statement. +-- @macro forall +M.define('forall',function(get,put) + local var = get:iden() + local t,v = get:next() + local rest,endt = get:list(M.upto_keywords('do','if')) + put:keyword 'for' + if v == 'in' then + put:iden '_' ',' :iden(var):keyword 'in' + put:iden 'ipairs' '(' :list(rest) ')' + elseif v == '=' then + put:iden(var) '=' :list(rest) + else + M.error("expecting in or =") + end + put:keyword 'do' + if endt[2] == 'if' then + rest,endt = get:list(M.upto_keywords('do')) + put:keyword 'if':list(rest):keyword 'then':iden '_END_END_' + end + return put +end) + +--- list comprehension. +-- Syntax is `L{expr | select}` where `select` is as in `forall`, +-- or `L{expr for select}` where `select` is as in the regular `for` statement. +-- @macro L +-- @return a list of values +-- @usage L{2*x | x in {1,2,3}} == {1,4,9} +-- @usage L{2*x|x = 1,3} == {1,4,9} +-- @usage L{{k,v} for k,v in pairs(t)} +-- @see forall +M.define('L',function(get,put) + local t,v = get:next() -- must be '{' + local expr,endt = get:list(function(t,v) + return t == '|' or t == 'keyword' and v == 'for' + end,'') + local select = get:list('}','') + put '(' : keyword 'function' '(' ')' :keyword 'local':iden 'res' '=' '{' '}' + if endt[2] == '|' then + put:iden'forall' + else + put:keyword 'for' + end + put:list(select):space():keyword'do' + put:iden'res' '[' '#' :iden'res' '+' :number(1) ']' '=' :list(expr):space() + put:keyword 'end' :keyword 'return' : iden 'res' :keyword 'end' ')' '(' ')' + put:iden '_POP_':string'L' + return put +end) diff --git a/Tools/LuaMacro/macro/ifelse.lua b/Tools/LuaMacro/macro/ifelse.lua new file mode 100644 index 0000000..3d5a0df --- /dev/null +++ b/Tools/LuaMacro/macro/ifelse.lua @@ -0,0 +1,90 @@ +local M = require 'macro' + +local function eval (expr,was_expr) + expr = tostring(expr) + if was_expr then expr = "return "..expr end + local chunk = M.assert(loadstring(expr)) + local ok, res = pcall(chunk) + if not ok then M.error("error evaluating "..res) end + return res +end + +local function eval_line (get,was_expr) + local args = get:line() + return eval(args,was_expr) +end + +local function grab (get) + local ilevel = 0 + while true do + local t,v = get() + while t ~= '@' do t = get() end + t,v = get() + if v == 'if' then + ilevel = ilevel + 1 + else -- 'end','elseif','else' + if ilevel > 0 and v == 'end' then + ilevel = ilevel - 1 + elseif ilevel == 0 then return '@'..v end + end + end +end + +M.define('@',function(get,put) + local t,v = get() +--~ print('got',t,v) + return put:iden(v..'_') +end) + +local ifstack,push,pop = {},table.insert,table.remove + +local function push_if (res) +--~ print 'push' + push(ifstack, not (res==false or res==nil)) +end + +local function pop_if () +--~ print 'pop' + pop(ifstack) +end + +M.define('if_',function(get) + local res = eval_line(get,true) + push_if(res) + if not res then + return grab(get) + end +end) + +M.define('elseif_',function(get) + local res + if ifstack[#ifstack] then + res = false + else + res = eval_line(get,true) + pop_if() + push_if(res) + end + if not res then + return grab(get) + end +end) + +M.define('else_',function(get) + if #ifstack == 0 then M.error("mismatched else") end + if ifstack[#ifstack] then + return grab(get) + end +end) + +M.define('end_',function(get) + pop_if() +end) + +M.define('let_',function(get) + eval_line(get) +end) + +M.define('eval_(X)',function(X) + return tostring(eval(X,true)) +end) diff --git a/Tools/LuaMacro/macro/lambda.lua b/Tools/LuaMacro/macro/lambda.lua new file mode 100644 index 0000000..677f997 --- /dev/null +++ b/Tools/LuaMacro/macro/lambda.lua @@ -0,0 +1,22 @@ +--- Short anonymous functions (lambdas). +-- This syntax is suited +-- to any naive token-processor because the payload is always inside parens. +-- It is an example of a macro associated with a 'operator' character. +-- +-- Syntax is `\<args>(<expr>)` +-- +-- `\x(x+10)` is short for +-- `function(x) return x+10 end`. There may be a number of formal argumets, +-- e.g. `\x,y(x+y)` or there may be none, e.g. `\(somefun())`. Such functions +-- may return multiple values, e.g `\x(x+1,x-1)`. +-- +-- @module macro.lambda + +local M = require 'macro' + +M.define ('\\',function(get,put) + local args, body = get:idens('('), get:list() + return put:keyword 'function' '(' : idens(args) ')' : + keyword 'return' : list(body) : space() : keyword 'end' +end) + diff --git a/Tools/LuaMacro/macro/lc.lua b/Tools/LuaMacro/macro/lc.lua new file mode 100644 index 0000000..0d2968d --- /dev/null +++ b/Tools/LuaMacro/macro/lc.lua @@ -0,0 +1,343 @@ +-- Simplifying writing C extensions for Lua +-- Adds new module and class constructs; +-- see class1.lc and str.lc for examples. +local M = require 'macro' + +function dollar_subst(s,tbl) + return (s:gsub('%$%((%a+)%)',tbl)) +end + +-- reuse some machinery from the C-skin experiments +local push,pop = table.insert,table.remove +local bstack,btop = {},{} + +local function push_brace_stack (newv) + newv = newv or {} + newv.lev = 0 + push(bstack,btop) + btop = newv +end + +M.define('{',function() + if btop.lev then + btop.lev = btop.lev + 1 + end + return nil,true --> pass-through macro +end) + +M.define('}',function(get,put) + if not btop.lev then + return nil,true + elseif btop.lev == 0 then + local res + if btop.handler then res = btop.handler(get,put) end + if not res then res = put:space() '}' end + btop = pop(bstack) + return res + else + btop.lev = btop.lev - 1 + return nil,true --> pass-through macro + end +end) + +--------- actual implementation begins ------- + +local append = table.insert +local module + +local function register_functions (names,cnames) + local out = {} + for i = 1,#names do + append(out,(' {"%s",l_%s},'):format(names[i],cnames[i])) + end + return table.concat(out,'\n') +end + +local function finalizers (names) + local out = {} + for i = 1,#names do + append(out,names[i].."(L);") + end + return table.concat(out,'\n') +end + +local typedefs + +local preamble = [[ +#include <lua.h> +#include <lauxlib.h> +#include <lualib.h> +#ifdef WIN32 +#define EXPORT __declspec(dllexport) +#else +#define EXPORT +#endif +#if LUA_VERSION_NUM > 501 +#define lua_objlen lua_rawlen +#endif +]] + +local finis = [[ +static const luaL_Reg $(cname)_funs[] = { + $(funs) + {NULL,NULL} +}; + +EXPORT int luaopen_$(cname) (lua_State *L) { +#if LUA_VERSION_NUM > 501 + lua_newtable(L); + luaL_setfuncs (L,$(cname)_funs,0); + lua_pushvalue(L,-1); + lua_setglobal(L,"$(cname)"); +#else + luaL_register(L,"$(cname)",$(cname)_funs); +#endif + $(finalizers) + return 1; +} +]] + +M.define('module',function(get) + local name = get:string() + local cname = name:gsub('%.','_') + get:expecting '{' + local out = preamble .. typedefs + push_brace_stack{ + name = name, cname = cname, + names = {}, cnames = {}, finalizers = {}, + handler = function() + local out = {} + local funs = register_functions(btop.names,btop.cnames) + local final = finalizers(btop.finalizers) + append(out,dollar_subst(finis, { + cname = cname, + name = name, + funs = funs, + finalizers = final + })) + return table.concat(out,'\n') + end } + module = btop + return out +end) + + +M.define('def',function(get) + local fname = get:name() + local cname = (btop.ns and btop.ns..'_' or '')..fname + append(btop.names,fname) + append(btop.cnames,cname) + get:expecting '(' + local args = get:list():strip_spaces() + get:expecting '{' + local t,space = get() + indent = space:gsub('^%s*[\n\r]',''):gsub('%s$','') + local out = {"static int l_"..cname.."(lua_State *L) {"} + if btop.massage_arg then + btop.massage_arg(args) + end + for i,arg in ipairs(args) do + local mac = arg[1][2]..'_init' + if arg[3] and arg[3][1] == '=' then + mac = mac .. 'o' + i = i .. ',' .. arg[4][2] + end + if not arg[2] then M.error("parameter must be TYPE NAME [= VALUE]") end + append(out,indent..mac..'('..arg[2][2]..','..i..');') + end + --append(out,space) + return table.concat(out,'\n')..space +end) + +M.define('constants',function(get,put) + get:expecting '{' + local consts = get:list '}' :strip_spaces() + --for k,v in pairs(btop) do io.stderr:write(k,'=',tostring(v),'\n') end + -- os.exit() + local fname = 'set_'..btop.cname..'_constants' + local out = { 'static void '..fname..'(lua_State *L) {'} + if not btop.finalizers then M.error("not inside a module") end + append(btop.finalizers,fname) + for _,c in ipairs(consts) do + local type,value,name + if #c == 1 then -- a simple int constant: CONST + name = c:pick(1) + type = 'Int' + value = name + else -- Type CONST [ = VALUE ] + type = c:pick(1) + name = c:pick(2) + if #c == 2 then + value = name + else + value = c:pick(4) + end + end + append(out,('%s_set("%s",%s);'):format(type,name,value )) + end + append(out,'}') + return table.concat(out,'\n') +end) + +M.define('assign',function(get) + get:expecting '{' + local asses = get:list '}' :strip_spaces() + local out = {} + for _,c in ipairs(asses) do + append(out,('%s_set("%s",%s);\n'):format(c:pick(1),c:pick(2),c:pick(4)) ) + end + return table.concat(out,'\n') +end) + +local load_lua = [[ +static void load_lua_code (lua_State *L) { + luaL_dostring(L,lua_code_block); +} +]] + +M.define('lua',function(get) + get:expecting '{' + local block = tostring(get:upto '}') + local code_name = 'lua_code_block' + local out = {'static const char *'.. code_name .. ' = ""\\'} + for line in block:gmatch('([^\r\n]+)') do + line = line:gsub('\\','\\\\'):gsub('"','\\"') + append(out,' "'..line..'\\n"\\') + end + append(out,';') + append(out,load_lua) + out = table.concat(out,'\n') + append(module.finalizers,'load_lua_code') + return out +end) + +typedefs = [[ +typedef const char *Str; +typedef const char *StrNil; +typedef int Int; +typedef double Number; +typedef int Boolean; +]] + + +M.define 'Str_init(var,idx) const char *var = luaL_checklstring(L,idx,NULL)' +M.define 'Str_inito(var,idx,val) const char *var = luaL_optlstring(L,idx,val,NULL)' +M.define 'Str_set(name,value) lua_pushstring(L,value); lua_setfield(L,-2,name)' +M.define 'Str_get(var,name) lua_getfield(L,-1,name); var=lua_tostring(L,-1); lua_pop(L,1)' +M.define 'Str_geti(var,idx) lua_rawgeti(L,-1,idx); var=lua_tostring(L,-1); lua_pop(L,1)' + +M.define 'StrNil_init(var,idx) const char *var = lua_tostring(L,idx)' + +M.define 'Int_init(var,idx) int var = luaL_checkinteger(L,idx)' +M.define 'Int_inito(var,idx,val) int var = luaL_optinteger(L,idx,val)' +M.define 'Int_set(name,value) lua_pushinteger(L,value); lua_setfield(L,-2,name)' +M.define 'Int_get(var,name) lua_getfield(L,-1,name); var=lua_tointeger(L,-1); lua_pop(L,1)' +M.define 'Int_geti(var,idx) lua_rawgeti(L,-1,idx); var=lua_tointeger(L,-1); lua_pop(L,1)' + +M.define 'Number_init(var,idx) double var = luaL_checknumber(L,idx)' +M.define 'Number_inito(var,idx,val) double var = luaL_optnumber(L,idx,val)' +M.define 'NUmber_set(name,value) lua_pushnumber(L,value); lua_setfield(L,-2,name)' +M.define 'Number_get(var,name) lua_getfield(L,-1,name); var=lua_tonumber(L,-1); lua_pop(L,1)' +M.define 'Number_geti(var,idx) lua_rawgeti(L,-1,idx); var=lua_tonumber(L,-1); lua_pop(L,1)' + +M.define 'Boolean_init(var,idx) int var = lua_toboolean(L,idx)' +M.define 'Boolean_set(name,value) lua_pushboolean(L,value); lua_setfield(L,-2,name)' +M.define 'Boolean_get(var,name) lua_getfield(L,-1,name); var=lua_toboolean(L,-1); lua_pop(L,1)' +M.define 'Boolean_geti(var,idx) lua_rawgeti(L,-1,idx); var=lua_toboolean(L,-1); lua_pop(L,1)' + +M.define 'Value_init(var,idx) int var = idx' + +M.define('lua_tests',function(get) + get:expecting '{' + local body = get:upto '}' + local f = io.open(M.filename..'.lua','w') + f:write(tostring(body)) + f:close() +end) + +------ class support ---------------------- + +local klass_ctor = "static void $(klass)_ctor(lua_State *L, $(klass) *this, $(fargs))" + +local begin_klass = [[ + +typedef struct { + $(fields) +} $(klass); + +define_ $(klass)_init(var,idx) $(klass) *var = $(klass)_arg(L,idx) + +#define $(klass)_MT "$(klass)" + +$(klass) * $(klass)_arg(lua_State *L,int idx) { + $(klass) *this = ($(klass) *)luaL_checkudata(L,idx,$(klass)_MT); + luaL_argcheck(L, this != NULL, idx, "$(klass) expected"); + return this; +} + +$(ctor); + +static int push_new_$(klass)(lua_State *L,$(fargs)) { + $(klass) *this = ($(klass) *)lua_newuserdata(L,sizeof($(klass))); + luaL_getmetatable(L,$(klass)_MT); + lua_setmetatable(L,-2); + $(klass)_ctor(L,this,$(aargs)); + return 1; +} + +]] + +local end_klass = [[ + +static const struct luaL_Reg $(klass)_methods [] = { + $(methods) + {NULL, NULL} /* sentinel */ +}; + +static void $(klass)_register (lua_State *L) { + luaL_newmetatable(L,$(klass)_MT); +#if LUA_VERSION_NUM > 501 + luaL_setfuncs(L,$(klass)_methods,0); +#else + luaL_register(L,NULL,$(klass)_methods); +#endif + lua_pushvalue(L,-1); + lua_setfield(L,-2,"__index"); + lua_pop(L,1); +} +]] + +M.define('class',function(get) + local name = get:iden() + get:expecting '{' + local fields = get:upto (function(t,v) + return t == 'iden' and v == 'constructor' + end) + fields = tostring(fields):gsub('%s+$','\n') + get:expecting '(' + local out = {} + local args = get:list() + local f_args = args:strip_spaces() + local a_args = f_args:pick(2) + f_args = table.concat(args:__tostring(),',') + a_args = table.concat(a_args,',') + local subst = {klass=name,fields=fields,fargs=f_args,aargs=a_args } + local proto = dollar_subst(klass_ctor,subst) + subst.ctor = proto + append(out,dollar_subst(begin_klass,subst)) + append(out,proto) + local pp = {{'iden',name},{'iden','this'}} + push_brace_stack{ + names = {}, cnames = {}, ns = name, cname = name, + massage_arg = function(args) + table.insert(args,1,pp) + end, + handler = function(get,put) + append(module.finalizers,name.."_register") + local methods = register_functions(btop.names,btop.cnames) + return dollar_subst(end_klass,{methods=methods,klass=name,fargs=f_args,aargs=a_args}) + end + } + return table.concat(out,'\n') +end) + diff --git a/Tools/LuaMacro/macro/lexer.lua b/Tools/LuaMacro/macro/lexer.lua new file mode 100644 index 0000000..58ab53a --- /dev/null +++ b/Tools/LuaMacro/macro/lexer.lua @@ -0,0 +1,179 @@ +--[[--- A Lua lexical scanner using LPeg. += CREDITS +Written by Peter Odding, 2007/04/04 + += THANKS TO +- the Lua authors for a wonderful language; +- Roberto for LPeg; +- caffeine for keeping me awake :) + += LICENSE +Shamelessly ripped from the SQLite[3] project: + + The author disclaims copyright to this source code. In place of a legal + notice, here is a blessing: + + May you do good and not evil. + May you find forgiveness for yourself and forgive others. + May you share freely, never taking more than you give. + +@module macro.lexer +--]] + +local lexer = {} +local lpeg = require 'lpeg' +local P, R, S, C, Cb, Cc, Cg, Cmt, Ct = + lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cb, lpeg.Cc, lpeg.Cg, lpeg.Cmt, lpeg.Ct + +-- create a pattern which captures the lua value [id] and the input matching +-- [patt] in a table +local function token(id, patt) return Ct(Cc(id) * C(patt)) end + +-- private interface +local table_of_tokens +local extra_tokens + +function lexer.add_extra_tokens(extra) + extra_tokens = extra_tokens or {} + for _,t in ipairs(extra) do + table.insert(extra_tokens,t) + end + table_of_tokens = nil -- re-initialize +end + +function lexer.init () + local digit = R('09') + + -- range of valid characters after first character of identifier + --local idsafe = R('AZ', 'az', '\127\255') + P '_' + local idsafe = R('AZ', 'az') + P '_' + R '\206\223' * R '\128\255' + -- operators + local OT = P '==' + if extra_tokens then + for _,ex in ipairs(extra_tokens) do + OT = OT + P(ex) + end + end + local operator = token('operator', OT + P '.' + P '~=' + P '<=' + P '>=' + P '...' + + P '..' + S '+-*/%^#=<>;:,.{}[]()') + -- identifiers + local ident = token('iden', idsafe * (idsafe + digit) ^ 0) + + -- keywords + local keyword = token('keyword', (P 'and' + P 'break' + P 'do' + P 'elseif' + + P 'else' + P 'end' + P 'false' + P 'for' + P 'function' + P 'if' + + P 'in' + P 'local' + P 'nil' + P 'not' + P 'or' + P 'repeat' + P 'return' + + P 'then' + P 'true' + P 'until' + P 'while') * -(idsafe + digit)) + + -- numbers + local number_sign = S'+-'^-1 + local number_decimal = digit ^ 1 + local number_hexadecimal = P '0' * S 'xX' * R('09', 'AF', 'af') ^ 1 + local number_float = (digit^1 * P'.' * digit^0 + P'.' * digit^1) * + (S'eE' * number_sign * digit^1)^-1 + local number = token('number', number_hexadecimal + + number_float + + number_decimal) + + -- callback for [=[ long strings ]=] + -- ps. LPeg is for Lua what regex is for Perl, which makes me smile :) + local equals = P '=' ^ 0 + local open = P '[' * Cg(equals, "init") * P '[' * P '\n' ^ -1 + local close = P ']' * C(equals) * P ']' + local closeeq = Cmt(close * Cb "init", function (s, i, a, b) return a == b end) + local longstring = open * C((P(1) - closeeq)^0) * close --/ 1 + + -- strings + local singlequoted_string = P "'" * ((1 - S "'\r\n\f\\") + (P '\\' * 1)) ^ 0 * "'" + local doublequoted_string = P '"' * ((1 - S '"\r\n\f\\') + (P '\\' * 1)) ^ 0 * '"' + local string = token('string', singlequoted_string + + doublequoted_string + + longstring) + + -- comments + local singleline_comment = P '--' * (1 - S '\r\n\f') ^ 0 + local multiline_comment = P '--' * longstring + local comment = token('comment', multiline_comment + singleline_comment) + + -- whitespace + local whitespace = token('space', S('\r\n\f\t ')^1) + + -- ordered choice of all tokens and last-resort error which consumes one character + local any_token = whitespace + number + keyword + ident + + string + comment + operator + token('error', 1) + + + table_of_tokens = Ct(any_token ^ 0) +end + +-- increment [line] by the number of line-ends in [text] +local function sync(line, text) + local index, limit = 1, #text + while index <= limit do + local start, stop = text:find('\r\n', index, true) + if not start then + start, stop = text:find('[\r\n\f]', index) + if not start then break end + end + index = stop + 1 + line = line + 1 + end + return line +end +lexer.sync = sync + +lexer.line = 0 + +-- we only need to synchronize the line-counter for these token types +local multiline_tokens = { comment = true, string = true, space = true } +lexer.multiline_tokens = multiline_tokens + +function lexer.scan_lua_tokenlist(input) + if not table_of_tokens then + lexer.init() + end + assert(type(input) == 'string', 'bad argument #1 (expected string)') + local line = 1 + local tokens = lpeg.match(table_of_tokens, input) + for i, token in pairs(tokens) do + local t = token[1] + if t == 'operator' or t == 'error' then + token[1] = token[2] + end + token[3] = line + if multiline_tokens[t] then + line = sync(line, token[2]) + end + end + return tokens +end + +--- get a token iterator from a source containing Lua code. +-- Note that this token iterator includes spaces and comments, and does not convert +-- string and number tokens - so e.g. a string token is quoted and a number token is +-- an unconverted string. +-- @param input the source - can be a string or a file-like object (i.e. read() returns line) +-- @param name for the source +function lexer.scan_lua(input,name) + if type(input) ~= 'string' and input.read then + input = input:read('*a') + end + local tokens = lexer.scan_lua_tokenlist(input) + local i, n = 1, #tokens + return function(k) + if k ~= nil then + k = i + k + if k < 1 or k > n then return nil end + return tokens[k] + end + local tok = tokens[i] + i = i + 1 + if tok then + lexer.line = tok[3] + lexer.name = name + return tok[1],tok[2] + end + end +end + +return lexer diff --git a/Tools/LuaMacro/macro/lib/class.lua b/Tools/LuaMacro/macro/lib/class.lua new file mode 100644 index 0000000..f762f36 --- /dev/null +++ b/Tools/LuaMacro/macro/lib/class.lua @@ -0,0 +1,35 @@ +---- +-- a basic class mechanism. +-- Used for some of the demonstrations; the `class` macro in the `module` +-- package uses it. It provides a single function which returns a new 'class'. +-- The resulting object can be called to generate an instance of the class. +-- You may provide a base class for single inheritance; in this case, the functions +-- of the base class will be copied into the new class' metatable (so-called 'fat metatable') +-- +-- Example: +-- +-- local class = require 'macro.lib.class' +-- A = class() +-- function A._init(name) self.name = name end +-- a = A("hello") +-- assert(a.name == "hello") +-- +-- @module macro.lib.class + +return function (base) + -- OOP with single inheritance + local klass,cmt = {},{} + if base then -- 'fat metatable' inheritance + for k,v in pairs(base) do klass[k] = v end + end + klass.__index = klass + -- provide a callable constructor that invokes user-supplied ctor + function cmt:__call(...) + local obj = setmetatable({},klass) + if klass._init then klass._init(obj,...) + elseif base and base._init then base._init(base,...) end + return obj + end + setmetatable(klass,cmt) + return klass +end diff --git a/Tools/LuaMacro/macro/lib/test.lua b/Tools/LuaMacro/macro/lib/test.lua new file mode 100644 index 0000000..5fff39e --- /dev/null +++ b/Tools/LuaMacro/macro/lib/test.lua @@ -0,0 +1,144 @@ +--- `assert_` macro library support. +-- This module may of course be used on its own; `assert_` merely provides +-- some syntactical sugar for its functionality. It is based on Penlight's +-- `pl.test` module. +-- @module macro.libs.test + +local test = {} + +local _eq,_tostring + +-- very much like tablex.deepcompare from Penlight +function _eq (v1,v2) + if type(v1) ~= type(v2) then return false end + -- if the value isn't a table, or it has defined the equality operator.. + local mt = getmetatable(v1) + if (mt and mt.__eq) or type(v1) ~= 'table' then + return v1 == v2 + end + -- both values are plain tables + if v1 == v2 then return true end -- they were the same table... + for k1,x1 in pairs(v1) do + local x2 = v2[k1] + if x2 == nil or not _eq(x1,x2) then return false end + end + for k2,x2 in pairs(v2) do + local x1 = v1[k2] + if x1 == nil or not _eq(x1,x2) then return false end + end + return true +end + +local function keyv (k) + if type(k) ~= 'string' then + k = '['..k..']' + end + return k +end + +function _tostring (val) + local mt = getmetatable(val) + if (mt and mt.__tostring) or type(val) ~= 'table' then + if type(val) == 'string' then + return '"'..tostring(val)..'"' + else + return tostring(val) + end + end + -- dump the table; doesn't need to be pretty! + local res = {} + local function put(s) res[#res+1] = s end + put '{' + for k,v in pairs(val) do + put(keyv(k)..'=') + put(_tostring(v)) + put ',' + end + table.remove(res) -- remove last ',' + put '}' + return table.concat(res) +end + +local function _lt (v1,v2) return v1 < v2 end +local function _gt (v1,v2) return v1 > v2 end +local function _match (v1,v2) return v1:match(v2) end + +local function _assert (v1,v2,cmp,msg) + if not cmp(v1,v2) then + print('first:',_tostring(v1)) + print(msg) + print('second:',_tostring(v2)) + error('assertion failed',3) + end +end + +--- assert if parameters are not equal. If the values are tables, +-- they will be compared by value. +-- @param v1 given value +-- @param v2 test value +function test.assert_eq (v1,v2) + _assert(v1,v2,_eq,"is not equal to"); +end + +--- assert if first parameter is not less than second. +-- @param v1 given value +-- @param v2 test value +function test.assert_lt (v1,v2) + _assert(v1,v2,_lt,"is not less than") +end + +--- assert if first parameter is not greater than second. +-- @param v1 given value +-- @param v2 test value +function test.assert_gt (v1,v2) + _assert(v1,v2,_gt,"is not greater than") +end + +--- assert if first parameter string does not match the second. +-- The condition is `v1:match(v2)`. +-- @param v1 given value +-- @param v2 test value +function test.assert_match (v1,v2) + _assert(v1,v2,_match,"does not match") +end + +-- return the error message from a function that raises an error. +-- Will raise an error if the function did not raise an error. +-- @param fun the function +-- @param ... any arguments to the function +-- @return the error message +function test.pcall_no(fun,...) + local ok,err = pcall(fun,...) + if ok then error('expression did not throw error',3) end + return err +end + +local tuple = {} + +function tuple.__eq (a,b) + if a.n ~= b.n then return false end + for i=1, a.n do + if not _eq(a[i],b[i]) then return false end + end + return true +end + +function tuple.__tostring (self) + local ts = {} + for i = 1,self.n do + ts[i] = _tostring(self[i]) + end + return '('..table.concat(ts,',')..')' +end + +--- create a tuple capturing multiple return values. +-- Equality between tuples means that all of their values are equal; +-- values may be `nil` +-- @param ... any values +-- @return a tuple object +function test.tuple(...) + return setmetatable({n=select('#',...),...},tuple) +end + +return test + diff --git a/Tools/LuaMacro/macro/module.lua b/Tools/LuaMacro/macro/module.lua new file mode 100644 index 0000000..a8e1b8d --- /dev/null +++ b/Tools/LuaMacro/macro/module.lua @@ -0,0 +1,132 @@ +--[[--- +Easy no-fuss modules. + +Any function inside the module will be exported, unless it is explicitly +local. The functions are declared up front using patching, leading to efficient +calls between module functions. + + require_ 'module' + + function one () + return two() + end + + function two () + return 42 + end + +Classes can also be declared inside modules: + + require_ 'module' + + class A + function set(self,val) @val = val end + function get(self) return @val end + end + +Within class definitions, the macro `@` expands to either `self.` or `self:` depending +on context, and provides a Ruby-like shortcut. + +If you give these modules names with `m.lua` extension like `mod.m.lua`, then you can +simply use `require()` to use them with LuaMacro. + +@module macro.module +]] +local M = require 'macro' + +local locals, locals_with_value = {}, {} +local ref + +local function module_add_new_local (name) + locals[#locals+1] = name +end + +local function module_add_new_local_with_value (name,value) + locals_with_value[name] = value +end + + +local function was_local_function (get) + local tl,keyw = get:peek(-1) + return tl=='keyword' and keyw=='local' +end + +-- exclude explicitly local functions and anonymous functions. +M.keyword_handler('function',function(get) + local tn,name = get:peek(1) + local was_local = was_local_function(get) + if not was_local and tn == 'iden' then + module_add_new_local(name) + end +end) + +-- when the module is closed, this will patch the locals and +-- output the module table. +M.keyword_handler('END',function(get) + local concat = table.concat + local patch = '' + if next(locals_with_value) then + local lnames,lvalues = {},{} + local i = 1 + for k,v in pairs(locals_with_value) do + lnames[i] = k + lvalues[i] = v + i = i + 1 + end + patch = patch..'local '..concat(lnames,',')..'='..concat(lvalues,',')..'; ' + end + if #locals > 0 then + patch = patch .. 'local '..concat(locals,',') + end + get:patch(ref,patch) + local dcl = {} + for i,name in ipairs(locals) do + dcl[i] = name..'='..name + end + dcl = table.concat(dcl,', ') + return 'return {' .. dcl .. '}' +end) + +local no_class_require + +-- the meaning of @f is either 'self.f' for fields, or 'self:f' for methods. +local function at_handler (get,put) + local tn,name,tp = get:peek2(1) + M.assert(tn == 'iden','identifier must follow @') + return put:iden ('self',true) (tp=='(' and ':' or '.') +end + +local function method_handler (get,put) + local tn,name,tp = get:peek2() + if not was_local_function(get) and tn == 'iden' and tp == '(' then + return put ' ' :iden ('_C',true) '.' + end +end + +M.define ('_C_',function() + M.define_scoped('@',at_handler) + if not no_class_require then + module_add_new_local_with_value('_class','require "macro.lib.class"') + no_class_require = true + end + M.scoped_keyword_handler('function',method_handler) +end) + +M.define('class',function(get) + local base = '' + local name = get:iden() + if get:peek(1) == ':' then + get:next() + base = get:iden() + end + module_add_new_local(name) + return ('do local _C = _class(%s); %s = _C; _C_\n'):format(base,name) +end) + +-- the result of the macro is just a placeholder for the locals +return function(get,put) + ref = get:placeholder(put) + return put +end + + diff --git a/Tools/LuaMacro/macro/try.lua b/Tools/LuaMacro/macro/try.lua new file mode 100644 index 0000000..8e49eb0 --- /dev/null +++ b/Tools/LuaMacro/macro/try.lua @@ -0,0 +1,47 @@ +--- A try/except block. +-- This generates syntactical sugar around `pcal`l, and correctly +-- distinguishes between the try block finishing naturally and +-- explicitly using 'return' with no value. This is handled by +-- converting any no value `return` to `return nil`. +-- +-- Apart from the usual creation of a closure, this uses a table +-- to capture all the results. Not likely to win speed contests, +-- but intended to be correct. +-- @module macro.try + +local M = require 'macro' + +local function pack (...) + local args = {...} + args.n = select('#',...) + return args +end + +function pcall_(fn,...) + return pack(pcall(fn,...)) +end + +local function check_return_value(get,put) + local t,v = get:next() + put:space() + if t=='keyword' and (v=='end' or v=='else' or v=='until') then + put:keyword 'nil' + end + return put(t,v) +end + + +M.define('RR_',M.make_scoped_handler('return',check_return_value)) + + +--- A try macro, paired with except. +-- +-- try +-- maybe_something_bad() +-- except (e) +-- print(e) +-- end +-- @macro try +M.define 'try do local r_ = pcall_(function() RR_ ' +M.define 'except(e) end); if r_[1] then if r_.n > 1 then return unpack(r_,2,r_.n) end else local e = r_[2] _END_END_ ' + diff --git a/Tools/LuaMacro/macro/with.lua b/Tools/LuaMacro/macro/with.lua new file mode 100644 index 0000000..de51590 --- /dev/null +++ b/Tools/LuaMacro/macro/with.lua @@ -0,0 +1,31 @@ +--[[-- +A `with` statement. This works more like the Visual Basic statement than the +Pascal one; fields have an explicit period to indicate that they are special. +This makes variable scoping explcit. + + aLongTableName = {} + with aLongTableName do + .a = 1 + .b = {{x=1},{x=2}} + .c = {f = 2} + print(.a,.c.f,.b[1].x) + end + +Fields that follow an identifier or a `}` are passed as-is. + +@module macro.with +]] +local M = require 'macro' + +M.define('with',function(get,put) + M.define_scoped('.',function() + local lt,lv = get:peek(-1,true) -- peek before the period... + if lt ~= 'iden' and lt ~= ']' then + return '_var.' + else + return nil,true -- pass through + end + end) + local expr = get:upto 'do' + return 'do local _var = '..tostring(expr)..'; ' +end) |