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 8.3KB

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