You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

transpile.lua 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. --
  2. -- A primitive code transpiler
  3. --
  4. -- Find multiple patterns
  5. local function find_multiple(text, ...)
  6. local n = select('#', ...)
  7. local s, e, pattern
  8. for i = 1, n do
  9. local p = select(i, ...)
  10. local s2, e2 = text:find(p)
  11. if s2 and (not s or s2 < s) then
  12. s, e, pattern = s2, e2 or s2, p
  13. end
  14. end
  15. return s, e, pattern
  16. end
  17. -- Matches
  18. -- These take 2-3 arguments (code, res, char) and should return code and res.
  19. local lua51 = not asyncio or asyncio.lua51
  20. local matches
  21. matches = {
  22. -- Handle multi-line strings and comments
  23. ['%[=*%['] = function(code, res, char)
  24. res = res .. char
  25. if char:sub(1, 2) == '--' then
  26. char = char:sub(4, -2)
  27. else
  28. char = char:sub(2, -2)
  29. end
  30. local s, e = code:find(']' .. char .. ']', nil, true)
  31. if not s or not e then return code, res end
  32. return code:sub(e + 1), res .. code:sub(1, e)
  33. end,
  34. -- Handle regular comments
  35. ['--'] = function(code, res, char)
  36. res = res .. char
  37. local s, e = code:find('\n', nil, true)
  38. if not s or not e then return code, res end
  39. return code:sub(e + 1), res .. code:sub(1, e)
  40. end,
  41. -- Handle quoted text
  42. ['"'] = function(code, res, char)
  43. res = res .. char
  44. -- Handle backslashes
  45. repeat
  46. local s, e, pattern = find_multiple(code, '\\', char)
  47. if pattern then
  48. res = res .. code:sub(1, e + 1)
  49. code = code:sub(e + 2)
  50. end
  51. until not pattern or pattern == char
  52. return code, res
  53. end,
  54. -- Handle "async function" statements
  55. ['async function'] = function(code, res, char)
  56. if not code:find('end', nil, true) then return code, res .. char end
  57. local s = code:find(')', nil, true)
  58. if not s then return code, res .. char end
  59. if char:sub(1, 1) ~= 'a' then res = res .. char:sub(1, 1) end
  60. local params = code:match('^%s*%(([^%)]*)%)')
  61. if params then
  62. res = res .. 'asyncio.coroutine(function(' .. params
  63. else
  64. -- Check for method functions
  65. local name, params = code:match('^%s*([%w_%.]+)%s*%(([^%)]*)%)')
  66. if not name or not params then
  67. local n, n2, p = code:match(
  68. '^%s*([%w_%.]+)%:([%w_]+)%s*%(([^%)]*)%)')
  69. if not n or not n2 or not p then
  70. return code, res .. char
  71. end
  72. name = n .. '.' .. n2
  73. if p:find('%S') then
  74. params = 'self, ' .. p
  75. else
  76. params = 'self'
  77. end
  78. end
  79. -- Local functions
  80. if res:sub(-6, -2) == 'local' then res = res .. name .. '; ' end
  81. -- Add the first part
  82. res = res .. name .. ' = asyncio.coroutine(function(' .. params
  83. end
  84. code = code:sub(s)
  85. -- Search for the "end"
  86. if code:sub(#code) == 'd' then code = code .. '\n' end
  87. local lvl = 1
  88. while lvl > 0 do
  89. local s, e, pattern = find_multiple(code, '[\'"\\]', '%-%-%[=*%[',
  90. '%-%-', '%[=*%[', '^async function ', '[%s;%[%(]async function',
  91. '^async for ', '[%s;%[]async for ', '^wait until ',
  92. '[%s;%[]wait until ', '%sdo%s', '[%s%)]then%s',
  93. '[%s%(%)]function[%s%(]', '%send[%s%),]', '^end[%s%),]')
  94. if not s then return code .. ')', res end
  95. -- Add non-matching characters
  96. res = res .. code:sub(1, math.max(s - 1, 0))
  97. local char = code:sub(s, e)
  98. if pattern == '%sdo%s' or pattern == '[%s%(%)]function[%s%(]' or
  99. pattern == '[%s%)]then%s' then
  100. lvl = lvl + 1
  101. res = res .. char
  102. code = code:sub(e + 1)
  103. elseif pattern == '%send[%s%),]' or pattern == '^end[%s%),]' then
  104. lvl = lvl - 1
  105. res = res .. char
  106. code = code:sub(e + 1)
  107. else
  108. -- Call the correct function
  109. local func = matches[char] or matches[pattern]
  110. assert(func, 'No function found for pattern!')
  111. code, res = func(code:sub(e + 1), res, char)
  112. -- Increase the level for async for loops
  113. if lua51 and (pattern == '^async for ' or
  114. pattern == '[%s;%[]async for ') then
  115. lvl = lvl + 1
  116. end
  117. end
  118. end
  119. return code, res:sub(1, -2) .. ')' .. res:sub(-1)
  120. end,
  121. -- Handle "async for" statements
  122. ['async for '] = function(code, res, char)
  123. if char:sub(1, 1) ~= 'a' then res = res .. char:sub(1, 1) end
  124. local var, func, n = code:match(
  125. '^([%w_,%s]+)%s+in%s*([%w_%.]+)%s*%(%s*(%S)')
  126. if not var or not func then return code, res .. 'for ' end
  127. local s = code:find('(', nil, true)
  128. code = code:sub(s + 1)
  129. res = res .. 'for ' .. var .. ' in asyncio.iter(' .. func
  130. if n ~= ')' then res = res .. ', ' end
  131. return code, res
  132. end,
  133. ['wait until '] = function(code, res, char)
  134. if char:sub(1, 1) ~= 'w' then res = res .. char:sub(1, 1) end
  135. return code, res .. 'repeat asyncio.defer() until '
  136. end,
  137. }
  138. -- A workaround for Lua 5.1's iterator limitations.
  139. if lua51 then
  140. local orig = matches['async for ']
  141. matches['async for '] = function(code, res, char)
  142. if char:sub(1, 1) ~= 'a' then res = res .. char:sub(1, 1) end
  143. local var, func, params = code:match(
  144. '^([%w_,%s]+)%s+in%s*([%w_%.]+)%s*%(%s*([^%(%)]*%))')
  145. -- TODO: Handle everything
  146. local s, e = code:find('%)%s*do')
  147. if not var or not func or not params or params:sub(-1) ~= ')' or
  148. not e then
  149. return orig(code, res, 'async for ')
  150. end
  151. code = code:sub(e + 1)
  152. -- Generate code
  153. s = var:find(',', nil, true)
  154. local i, v = '__asyncio_lua51_iter_shim', var:sub(1, (s or 2) - 1)
  155. if params ~= ')' then params = ', ' .. params end
  156. res = res .. 'local ' .. i .. ' = asyncio._iter_start(' .. func ..
  157. params .. '; while true do local ' .. var .. ' = ' .. i ..
  158. '(); if ' .. v .. ' == nil then break end;'
  159. return code, res
  160. end
  161. end
  162. -- Give the functions alternate names
  163. matches['%-%-%[=*%['], matches["'"] = matches['%[=*%['], matches['"']
  164. matches['[%s;%[%(]async function'] = matches['async function']
  165. matches['[%s;%[]async for '] = matches['async for ']
  166. matches['[%s;%[]wait until '] = matches['wait until ']
  167. -- The actual transpiler
  168. local function transpile(code)
  169. assert(type(code) == 'string')
  170. local res = ''
  171. -- Split the code by "tokens"
  172. while true do
  173. -- Search for special characters
  174. local s, e, pattern = find_multiple(code, '[\'"\\]', '%-%-%[=*%[',
  175. '%-%-', '%[=*%[', '^async function', '[%s;%[%(]async function',
  176. '^async for ', '[%s;%[]async for ', '^wait until ',
  177. '[%s;%[]wait until ')
  178. if not s then break end
  179. -- Add non-matching characters
  180. res = res .. code:sub(1, math.max(s - 1, 0))
  181. -- Call the correct function
  182. local char = code:sub(s, e)
  183. local func = matches[char] or matches[pattern]
  184. assert(func, 'No function found for pattern!')
  185. code, res = func(code:sub(e + 1), res, char)
  186. end
  187. return res .. code
  188. end
  189. -- Handle being called outside of Minetest.
  190. if not asyncio or not minetest then
  191. -- If this is called via dofile(), just return the function.
  192. if not arg or debug.getinfo(3) then
  193. return transpile
  194. end
  195. if not arg[1] or arg[1] == '' then
  196. print('Invalid usage.')
  197. print('Usage: transpile.lua <file>')
  198. os.exit(1)
  199. end
  200. -- Read the file
  201. local f, err, errcode = io.open(arg[1], 'r')
  202. if not f then
  203. print(err)
  204. os.exit(errcode)
  205. end
  206. local code, err, errcode = f:read('*a')
  207. f:close()
  208. if not code then
  209. print(err)
  210. os.exit(errcode)
  211. end
  212. -- Print the output
  213. print(transpile(code))
  214. os.exit(0)
  215. end
  216. -- Lua 5.2+ compatibility
  217. local loadstring = assert(rawget(_G, 'loadstring') or load)
  218. -- Load strings
  219. function asyncio.loadstring(code, ...)
  220. local f, msg = loadstring(transpile(code), ...)
  221. return f and asyncio.coroutine(f), msg
  222. end
  223. -- Load strings from files
  224. function asyncio.loadfile(file)
  225. local f, msg = io.open(file, 'r')
  226. if not f then return nil, msg end
  227. return asyncio.loadstring(f:read('*a'), '@' .. file)
  228. end
  229. -- Execute files
  230. function asyncio.dofile(file)
  231. local func, msg = asyncio.loadfile(file)
  232. if not func then error(msg, 0) end
  233. return func()
  234. end
  235. -- Execute a string
  236. function asyncio.exec(...)
  237. local func, msg = asyncio.loadstring(...)
  238. if not func then error(msg, 2) end
  239. return func()
  240. end
  241. -- Load a file in the current mod
  242. local DIR_DELIM = rawget(_G, 'DIR_DELIM') or '/'
  243. function asyncio.domodfile(file)
  244. local prefix = minetest.get_modpath(minetest.get_current_modname())
  245. return asyncio.dofile(prefix .. DIR_DELIM .. file)
  246. end