Advertisement
1lann

goroutines

Jul 25th, 2015
365
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.71 KB | None | 0 0
  1. -- Goroutines for ComputerCraft!
  2. -- Made by 1lann (Jason Chu)
  3. -- Last updated: 31st July 2015
  4.  
  5. --[[
  6. Licensed under the MIT License:
  7. The MIT License (MIT)
  8.  
  9. Copyright (c) 2015 1lann (Jason Chu)
  10.  
  11. Permission is hereby granted, free of charge, to any person obtaining a copy
  12. of this software and associated documentation files (the "Software"), to deal
  13. in the Software without restriction, including without limitation the rights
  14. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. copies of the Software, and to permit persons to whom the Software is
  16. furnished to do so, subject to the following conditions:
  17.  
  18. The above copyright notice and this permission notice shall be included in
  19. all copies or substantial portions of the Software.
  20.  
  21. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. THE SOFTWARE.
  28. ]]--
  29.  
  30.  
  31. -- Goroutine manager variables
  32.  
  33. local activeGoroutines = {}
  34. local channels = {}
  35. local termCompatibilityMode = false
  36. local quitDispatcherEvent = "quit_goroutine_dispatcher"
  37. local channelEventHeader = "goroutine_channel_event_"
  38. local waitGroupEvent = "goroutine_wait_group_event"
  39. local goEnv = {}
  40. local nativeTerm = term.current()
  41. local dispatcherRunning = false
  42. local dispatcherQuitFunc = function() end
  43.  
  44. local currentGoroutine = 0
  45.  
  46. -- Goroutine utility functions
  47.  
  48. local function keyOfGoroutineId(id)
  49.     for k, v in pairs(activeGoroutines) do
  50.         if v.id == id then
  51.             return k
  52.         end
  53.     end
  54.  
  55.     return nil
  56. end
  57.  
  58. local function currentKey()
  59.     local key = keyOfGoroutineId(currentGoroutine)
  60.     if key == nil then
  61.         return goEnv.error("Cannot store term context outside of goroutine.", 1)
  62.     end
  63.  
  64.     return key
  65. end
  66.  
  67. -- Stacktracer
  68. -- A very small portion was taken from CoolisTheName007
  69. -- Origin: http://pastebin.com/YWwLUUpk
  70.  
  71. local function stacktrace(depth, isInvoke)
  72.     local trace = {}
  73.     local i = depth + 2
  74.     local first = true
  75.  
  76.     while true do
  77.         i = i + 1
  78.         _, err = pcall(error, "" , i)
  79.         if err:match("^[^:]+") == "bios" or
  80.             #err == 0 then
  81.             break
  82.         end
  83.  
  84.         if first then
  85.             first = false
  86.             if isInvoke then
  87.                 table.insert(trace, "created by " .. err:sub(1, -3))
  88.             else
  89.                 table.insert(trace, "at " .. err:sub(1, -3))
  90.             end
  91.         else
  92.             table.insert(trace, "from " .. err:sub(1, -3))
  93.         end
  94.     end
  95.  
  96.     if currentGoroutine == -1 then
  97.         table.insert(trace, "created by ? (trace unavailable)")
  98.     else
  99.         local goTrace
  100.         for _, v in pairs(activeGoroutines) do
  101.             if v.id == currentGoroutine then
  102.                 goTrace = v.stacktrace
  103.             end
  104.         end
  105.  
  106.         if not goTrace then
  107.             table.insert(trace, "created by ? (trace unavailable)")
  108.         else
  109.             for _, v in pairs(goTrace) do
  110.                 table.insert(trace, v)
  111.             end
  112.         end
  113.     end
  114.  
  115.     return trace
  116. end
  117.  
  118. function goEnv.error(err, depth)
  119.     if not depth then
  120.         depth = 1
  121.     end
  122.  
  123.     if currentGoroutine == -1 then
  124.         return error(err, depth)
  125.     end
  126.  
  127.     local trace = stacktrace(depth)
  128.     table.insert(activeGoroutines[currentKey()].errors, {
  129.         err = err,
  130.         trace = trace,
  131.     })
  132.  
  133.     if #(activeGoroutines[currentKey()].errors) > 10 then
  134.         table.remove(activeGoroutines[currentKey()].errors, 1)
  135.     end
  136.  
  137.     return error(err, depth)
  138. end
  139.  
  140. local function traceError(err)
  141.     local location, msg = err:match("([^:]+:%d+): (.+)")
  142.  
  143.     local recentErrors = activeGoroutines[currentKey()].errors
  144.  
  145.     local tracedTrace = nil
  146.     for _, v in pairs(recentErrors) do
  147.         if v.err == msg then
  148.             tracedTrace = v.trace
  149.             break
  150.         end
  151.     end
  152.  
  153.     local _, y = nativeTerm.getCursorPos()
  154.     nativeTerm.setCursorPos(1, y)
  155.     nativeTerm.setTextColor(colors.red)
  156.  
  157.     if tracedTrace then
  158.         print("goroutine runtime error:")
  159.         print(msg)
  160.         for _, v in pairs(tracedTrace) do
  161.             print("  " .. v)
  162.         end
  163.  
  164.         return
  165.     end
  166.  
  167.     if not msg then
  168.         msg = err
  169.     end
  170.  
  171.     print("goroutine runtime error:")
  172.     print(msg)
  173.  
  174.     if location then
  175.         print("  at " .. location)
  176.     else
  177.         print("  at ? (location unavailable)")
  178.     end
  179.  
  180.     print("  from ? (trace unavailable)")
  181.  
  182.     if currentGoroutine == -1 then
  183.         print("  created by ? (trace unavailable)")
  184.         print("  from goroutine start")
  185.     else
  186.         local goTrace
  187.         for _, v in pairs(activeGoroutines) do
  188.             if v.id == currentGoroutine then
  189.                 goTrace = v.stacktrace
  190.             end
  191.         end
  192.  
  193.         if not goTrace then
  194.             print("  created by ? (trace unavailable)")
  195.             print("  from goroutine start")
  196.         else
  197.             for _, v in pairs(goTrace) do
  198.                 print("  " .. v)
  199.             end
  200.         end
  201.     end
  202. end
  203.  
  204. -- Term wrapper to allow for saving terminal states
  205.  
  206. local emulatedTerm = {}
  207.  
  208. for k, v in pairs(nativeTerm) do
  209.     emulatedTerm[k] = v
  210. end
  211.  
  212. emulatedTerm.setCursorBlink = function(blink)
  213.     activeGoroutines[currentKey()].termState.blink = blink
  214.     return nativeTerm.setCursorBlink(blink)
  215. end
  216.  
  217. emulatedTerm.setBackgroundColor = function(color)
  218.     if type(color) ~= "number" then
  219.         return goEnv.error("Argument to term.setBackgroundColor must be a number")
  220.     end
  221.  
  222.     activeGoroutines[currentKey()].termState.bg_color = color
  223.     return nativeTerm.setBackgroundColor(color)
  224. end
  225.  
  226. emulatedTerm.write = function(text)
  227.     goEnv.emitChannel("term_events", "write")
  228.     return nativeTerm.write(text)
  229. end
  230.  
  231. emulatedTerm.setTextColor = function(color)
  232.     if type(color) ~= "number" then
  233.         return goEnv.error("Argument to term.setTextColor must be a number")
  234.     end
  235.  
  236.     activeGoroutines[currentKey()].termState.txt_color = color
  237.     return nativeTerm.setTextColor(color)
  238. end
  239.  
  240. emulatedTerm.scroll = function(...)
  241.     goEnv.emitChannel("term_events", "scroll")
  242.     return nativeTerm.scroll(...)
  243. end
  244.  
  245. emulatedTerm.clearLine = function(...)
  246.     goEnv.emitChannel("term_events", "clearLine")
  247.     return nativeTerm.clearLine(...)
  248. end
  249.  
  250. emulatedTerm.clear = function(...)
  251.     goEnv.emitChannel("term_events", "clear")
  252.     return nativeTerm.clear(...)
  253. end
  254.  
  255. local function restoreTermState(termState)
  256.     nativeTerm.setTextColor(termState.txt_color)
  257.     nativeTerm.setBackgroundColor(termState.bg_color)
  258.     nativeTerm.setCursorBlink(termState.blink)
  259.     nativeTerm.setCursorPos(unpack(termState.cursor_pos))
  260. end
  261.  
  262. function goEnv.goroutineId()
  263.     return currentGoroutine
  264. end
  265.  
  266. -- Goroutines and channel functions
  267.  
  268. function goEnv.go(...)
  269.     local args = {...}
  270.  
  271.     if type(args[1]) ~= "function" then
  272.         return goEnv.error("First argument to go must be a function.")
  273.     end
  274.  
  275.     local funcArgs = {}
  276.  
  277.     if #args > 1 then
  278.         for i = 2, #args do
  279.             table.append(funcArgs, args[i])
  280.         end
  281.     end
  282.  
  283.     local idsInUse = {}
  284.     for k,v in pairs(activeGoroutines) do
  285.         idsInUse[tostring(v.id)] = true
  286.     end
  287.  
  288.     local newId = -1
  289.     for i = 1, 1000000 do
  290.         if not idsInUse[tostring(i)] then
  291.             newId = i
  292.             break
  293.         end
  294.     end
  295.  
  296.     if newId < 0 then
  297.         return goEnv.error("Reached goroutine limit, cannot spawn new goroutine")
  298.     end
  299.  
  300.     activeGoroutines[currentKey()].termState.cursor_pos =
  301.         {nativeTerm.getCursorPos()}
  302.  
  303.     local parent = activeGoroutines[currentKey()]
  304.     local copyTermState = {}
  305.  
  306.     for k, v in pairs(parent.termState) do
  307.         copyTermState[k] = v
  308.     end
  309.  
  310.     local func = args[1]
  311.  
  312.     local env = {}
  313.  
  314.     local global = getfenv(0)
  315.     local localEnv = getfenv(1)
  316.  
  317.     for k,v in pairs(global) do
  318.         env[k] = v
  319.     end
  320.  
  321.     for k,v in pairs(localEnv) do
  322.         env[k] = v
  323.     end
  324.  
  325.     for k, v in pairs(goEnv) do
  326.         env[k] = v
  327.     end
  328.  
  329.     local newFunc = setfenv(func, env)
  330.  
  331.     table.insert(activeGoroutines, {
  332.         id = newId,
  333.         func = coroutine.create(newFunc),
  334.         arguments = funcArgs,
  335.         stacktrace = stacktrace(1, true),
  336.         termState = copyTermState,
  337.         suspended = false,
  338.         forceResume = false,
  339.         errors = {},
  340.         filter = nil,
  341.         firstRun = true,
  342.     })
  343.  
  344.     return newId
  345. end
  346.  
  347. function goEnv.suspend(goroutineId)
  348.     local key = keyOfGoroutineId(goroutineId)
  349.     if key == nil then
  350.         return goEnv.error("Attempt to suspend non-existent goroutine.")
  351.     end
  352.  
  353.     activeGoroutines[key].suspended = true
  354.     activeGoroutines[key].forceResume = false
  355. end
  356.  
  357. function goEnv.resume(goroutineId)
  358.     local key = keyOfGoroutineId(goroutineId)
  359.     if key == nil then
  360.         return goEnv.error("Attempt to resume non-existent goroutine.")
  361.     end
  362.  
  363.     activeGoroutines[key].suspended = false
  364.     activeGoroutines[key].forceResume = true
  365. end
  366.  
  367. function goEnv.emitChannel(channel, data, wait)
  368.     if type(channel) ~= "string" then
  369.         return goEnv.error("First argument to emitChannel must be a string.")
  370.     end
  371.  
  372.     if data == nil then
  373.         return goEnv.error("Second argument (data) to emitChannel, cannot be nil.")
  374.     end
  375.  
  376.     if channel == "term_events" and termCompatibilityMode and data then
  377.         return
  378.     end
  379.  
  380.     channels[channel] = {data, goEnv.goroutineId()}
  381.  
  382.     os.queueEvent(channelEventHeader .. channel)
  383.  
  384.     if wait then
  385.         while true do
  386.             if channels[channel] == nil then
  387.                 return
  388.             end
  389.  
  390.             coroutine.yield(channelEventHeader .. channel)
  391.         end
  392.     end
  393. end
  394.  
  395. function goEnv.waitChannel(channel, allowPrev, timeout)
  396.     if type(channel) ~= "string" then
  397.         return goEnv.error("First argument to waitChannel must be a string.")
  398.     end
  399.  
  400.     if timeout and type(timeout) ~= "number" then
  401.         return goEnv.error("Third argument to waitChannel must be a number or nil.")
  402.     end
  403.  
  404.     local stillAlive = true
  405.  
  406.     if timeout then
  407.         goEnv.go(function()
  408.             goEnv.sleep(timeout)
  409.             if stillAlive then
  410.                 goEnv.emitChannel(channel, false)
  411.             end
  412.         end)
  413.     end
  414.  
  415.     if not allowPrev then
  416.         channels[channel] = nil
  417.     end
  418.  
  419.     os.queueEvent(channelEventHeader .. channel)
  420.  
  421.     while true do
  422.         if channels[channel] ~= nil then
  423.             stillAlive = false
  424.             local value = channels[channel]
  425.             channels[channel] = nil
  426.             return unpack(value)
  427.         end
  428.  
  429.         coroutine.yield(channelEventHeader .. channel)
  430.     end
  431. end
  432.  
  433. -- Custom overrides
  434.  
  435. function goEnv.sleep(sec)
  436.     local timer = os.startTimer(sec)
  437.     local start = os.clock()
  438.     while true do
  439.         local event, timerId = os.pullEvent()
  440.  
  441.         if event == "timer" and timerId == timer then
  442.             return
  443.         end
  444.  
  445.         if os.clock() - start >= sec then
  446.             return
  447.         end
  448.     end
  449. end
  450.  
  451. -- Wait groups
  452.  
  453. goEnv.WaitGroup = {}
  454. goEnv.WaitGroup.__index = goEnv.WaitGroup
  455.  
  456. function goEnv.WaitGroup.new()
  457.     local self = setmetatable({}, goEnv.WaitGroup)
  458.     self:setZero()
  459.     return self
  460. end
  461.  
  462. function goEnv.WaitGroup:setZero()
  463.     self.incrementer = 0
  464.     os.queueEvent(waitGroupEvent)
  465. end
  466.  
  467. function goEnv.WaitGroup:done()
  468.     if self.incrementer > 0 then
  469.         self.incrementer = self.incrementer - 1
  470.         os.queueEvent(waitGroupEvent)
  471.     end
  472. end
  473.  
  474. function goEnv.WaitGroup:wait()
  475.     while true do
  476.         if self.incrementer == 0 then
  477.             return
  478.         end
  479.  
  480.         coroutine.yield(waitGroupEvent)
  481.     end
  482. end
  483.  
  484. function goEnv.WaitGroup:add(amount)
  485.     self.incrementer = self.incrementer + amount
  486.     if self.incrementer < 0 then
  487.         self.incrementer = 0
  488.     end
  489.     os.queueEvent(waitGroupEvent)
  490. end
  491.  
  492. function goEnv.WaitGroup:value()
  493.     return self.incrementer
  494. end
  495.  
  496. -- Runner
  497.  
  498. local function cleanUp()
  499.     channels = {}
  500.     activeGoroutines = {}
  501.     dispatcherRunning = false
  502.     termCompatibilityMode = false
  503.     term.redirect(nativeTerm)
  504.  
  505.     local ret, err = pcall(dispatcherQuitFunc)
  506.     if not ret then
  507.         local _, y = term.getCursorPos()
  508.         term.setCursorPos(1, y)
  509.         term.setTextColor(colors.red)
  510.  
  511.         print("user dispatcher quit error:")
  512.         print(err)
  513.  
  514.         term.setTextColor(colors.white)
  515.     end
  516. end
  517.  
  518. function runDispatcher(programFunction)
  519.     if dispatcherRunning then
  520.         error("Dispatcher already running.")
  521.     end
  522.     dispatcherRunning = true
  523.  
  524.     term.redirect(emulatedTerm)
  525.  
  526.     local env = {}
  527.     local global = getfenv(0)
  528.     local localEnv = getfenv(1)
  529.  
  530.     for k, v in pairs(global) do
  531.         env[k] = v
  532.     end
  533.  
  534.     for k, v in pairs(localEnv) do
  535.         env[k] = v
  536.     end
  537.  
  538.     for k, v in pairs(goEnv) do
  539.         env[k] = v
  540.     end
  541.  
  542.     local main = setfenv(programFunction, env)
  543.  
  544.     table.insert(activeGoroutines, {
  545.         func = coroutine.create(main),
  546.         arguments = {},
  547.         id = 0,
  548.         termState = {
  549.             txt_color = colors.white,
  550.             bg_color = colors.black,
  551.             blink = false,
  552.             cursor_pos = {nativeTerm.getCursorPos()},
  553.         },
  554.         errors = {},
  555.         stacktrace = {"from dispatcher start"},
  556.         suspended = false,
  557.         forceResume = false,
  558.         filter = nil,
  559.         firstRun = true,
  560.     })
  561.  
  562.     local events = {}
  563.  
  564.     while true do
  565.         for k, v in pairs(activeGoroutines) do
  566.             if coroutine.status(v.func) ~= "dead" and not v.suspended then
  567.                 if (v.filter and events and #events > 0 and
  568.                     events[1] == v.filter) or (not v.filter and
  569.                     ((events and #events > 0 and
  570.                     not events[1]:find("goroutine_channel_event_")) or
  571.                     (not events or #events <= 0))) or
  572.                     (events and #events > 0 and events[1] == "terminate") or
  573.                     v.forceResume then
  574.  
  575.                     if v.forceResume then
  576.                         if v.filter and events[1] ~= v.filter then
  577.                             events = {"goroutine_force_resume"}
  578.                         end
  579.  
  580.                         activeGoroutines[k].forceResume = false
  581.                     end
  582.  
  583.                     activeGoroutines[k].filter = nil
  584.                     local resp, err
  585.  
  586.                     currentGoroutine = v.id
  587.  
  588.                     restoreTermState(v.termState)
  589.  
  590.                     if v.firstRun then
  591.                         resp, err = coroutine.resume(v.func, unpack(v.arguments))
  592.                         activeGoroutines[k].firstRun = false
  593.                     else
  594.                         resp, err = coroutine.resume(v.func, unpack(events))
  595.                     end
  596.  
  597.                     activeGoroutines[k].termState.cursor_pos =
  598.                         {nativeTerm.getCursorPos()}
  599.  
  600.                     if resp then
  601.                         if err == quitDispatcherEvent then
  602.                             cleanUp()
  603.                             return
  604.                         end
  605.  
  606.                         if type(err) == "string" then
  607.                             activeGoroutines[k].filter = err
  608.                         end
  609.                     else
  610.                         traceError(err)
  611.                         cleanUp()
  612.                         return
  613.                     end
  614.  
  615.                     currentGoroutine = -1
  616.                 end
  617.             end
  618.         end
  619.  
  620.         local sweeper = {}
  621.  
  622.         for k,v in pairs(activeGoroutines) do
  623.             if coroutine.status(v.func) ~= "dead" then
  624.                 table.insert(sweeper, v)
  625.             end
  626.         end
  627.  
  628.         activeGoroutines = {}
  629.  
  630.         for k,v in pairs(sweeper) do
  631.             if v.termState.blink then
  632.                 nativeTerm.setCursorBlink(true)
  633.                 nativeTerm.setTextColor(v.termState.txt_color)
  634.                 nativeTerm.setCursorPos(unpack(v.termState.cursor_pos))
  635.             end
  636.             table.insert(activeGoroutines, v)
  637.         end
  638.  
  639.         if #activeGoroutines == 0 then
  640.             cleanUp()
  641.             return
  642.         end
  643.  
  644.         events = {os.pullEventRaw()}
  645.  
  646.         if events and #events > 0 and events[1] == quitDispatcherEvent then
  647.             cleanUp()
  648.             return
  649.         end
  650.     end
  651. end
  652.  
  653. function goEnv.quitDispatcher()
  654.     coroutine.yield(quitDispatcherEvent)
  655. end
  656.  
  657. function termCompatibility()
  658.     term.redirect(nativeTerm)
  659.     termCompatibilityMode = true
  660. end
  661.  
  662. -- You should not manipulate the terminal with the
  663. -- dispatch quit function, as it will be also be
  664. -- called on dirty quits, such as errors.
  665. function onDispatcherQuit(func)
  666.     dispatcherQuitFunc = func
  667. end
  668.  
  669. function quitDispatcher()
  670.     os.queueEvent(quitDispatcherEvent)
  671. end
  672.  
  673. goEnv.onDispatcherQuit = onDispatcherQuit
  674. goEnv.termCompatibility = termCompatibility
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement