Skip to content

Instantly share code, notes, and snippets.

@jeremycw
Last active July 12, 2020 09:18
Show Gist options
  • Save jeremycw/da9486d445f2fd80d92228d9bfcc8166 to your computer and use it in GitHub Desktop.
Save jeremycw/da9486d445f2fd80d92228d9bfcc8166 to your computer and use it in GitHub Desktop.
Stackless Coroutine implementation in ruby
#Copyright (c) 2019 Jeremy Williams
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
module StacklessCoroutine
class Interpreter
attr_accessor :state
attr_reader :root
def initialize(subroutines, opts = {})
@namespace = opts[:namespace] || :root
@ctx = opts[:ctx]
@stack = [[:scope, [], { type: :root }]]
@root = @stack.last
@subroutines = subroutines
@yield = opts[:yield] || { id: 0 }
end
def if(method = nil, &blk)
node = [:if, [], { blk: method || blk }]
@stack.last[1] << node
@stack.push(node)
branch = [:scope, [], {}]
node[1] << branch
@stack.push(branch)
self
end
def while(method = nil, &blk)
node = [:while, [], { blk: method || blk }]
@stack.last[1] << node
@stack.push(node)
branch = [:scope, [], {}]
node[1] << branch
@stack.push(branch)
self
end
def else
@stack.pop
node = [:scope, [], {}]
@stack.last[1] << node
@stack.push(node)
self
end
def run(method = nil, &blk)
node = [:run, [], { blk: method || blk }]
@stack.last[1] << node
self
end
def yield(label = nil, &blk)
node = [:yield, [], { blk: blk, id: "#{@namespace}:#{label || @yield[:id] += 1 }"}]
@stack.last[1] << node
self
end
def return
node = [:return, []]
@stack.last[1] << node
self
end
def end
@stack.pop
@stack.pop
self
end
def call(name, namespace = nil)
namespace = namespace.nil? ? @namespace : "#{@namespace}:#{namespace}"
i = Interpreter.new(@subroutines, yield: @yield, namespace: namespace)
@subroutines[name].call(i)
node = i.root
@stack.last[1] << node
self
end
def method_missing(m, *args)
node = [:run, [], { blk: m }]
@stack.last[1] << node
self
end
def invoke(data)
@data = data
@stack = []
traverse
@return
end
private
def exec_statement(blk)
fn = blk.is_a?(Symbol) ? ->(d) { send(blk, d) } : blk
if @ctx
@ctx.instance_exec(@data, &fn)
else
fn.call(@data)
end
end
def traverse
return exec(@root) if @state.nil?
node = @root
loop do
if node.nil?
@stack.pop
node, iter = @stack.last
else
iter = node[1].each
@stack.push([node, iter])
end
#puts "Traverse: #{node.inspect}"
if node[0] == :yield && node[2][:id] == @state
exec(nil)
break
end
node = nxt(iter)
end
end
def exec(node)
loop do
if node.nil?
@stack.pop
@stack.pop while @stack.last && @stack.last[0][0] == :if
while @stack.last && @stack.last[0][0] == :while
if exec_statement(@stack.last[0][2][:blk])
@stack.last[1].rewind
break
else
@stack.pop
end
end
break if @stack.last.nil?
node, iter = @stack.last
else
iter = node[1].each
@stack.push([node, iter])
end
#puts "Exec: #{node[0]}"
case node[0]
when :if
if exec_statement(node[2][:blk])
node = nxt(iter)
else
nxt(iter) #pass over true branch
node = nxt(iter)
end
when :return
@stack.pop while !(@stack.last[0][0] == :scope && @stack.last[0][2][:type] == :root)
@stack.pop
break if @stack.last.nil?
node = nxt(@stack.last[1])
when :scope
node = nxt(iter)
when :while
node = exec_statement(node[2][:blk]) ? nxt(iter) : nil
when :run
exec_statement(node[2][:blk])
node = nil
when :yield
@state = node[2][:id]
@return = !node[2][:blk].nil? ? call(node[2][:blk]) : nil
break
end
end
end
def nxt(iter)
iter.next rescue nil
end
end
module ClassMethods
def coroutine(state_attribute, &blk)
@state_attribute = state_attribute
@flow_blk = blk
end
def subroutine(name, &blk)
@subroutines ||= {}
@subroutines[name] = blk
end
def flow_blk
@flow_blk
end
def subroutines
@subroutines
end
def state_attribute
@state_attribute
end
end
def self.included(base)
base.extend(ClassMethods)
end
def invoke(data)
f = Interpreter.new(self.class.subroutines, ctx: self)
f.state = send(self.class.state_attribute)
self.class.flow_blk.call(f)
ret = f.invoke(data)
send("#{self.class.state_attribute}=", f.state)
ret
end
end
class FooBar
include StacklessCoroutine
attr_accessor :state
coroutine(:state) do |exec|
exec
.while { |data| data.loop == true }
.run { puts "looping" }
.if { |data| data.status == 'ready' }
.run { |data| puts data.status }
.run { puts "yielding" }
.yield
.fizzle
.else
.run { |data| puts data.status }
.run { puts "yielding" }
.yield
.call(:foo)
.call(:foo)
.run { puts "finishing..." }
.end
.end
end
subroutine(:foo) do |exec|
exec
.run { |data| puts "in foo" }
.run { puts "yielding baz" }
.yield
end
def fizzle(data)
puts "fizzle"
end
end
require 'ostruct'
d = OpenStruct.new
d.loop = true
d.status = 'ready'
puts "Continue 1"
foo = FooBar.new
foo.invoke(d)
d.status = 'paused'
puts "Continue 2"
puts foo.invoke(d)
d.loop = false
puts "Continue 3"
foo.invoke(d)
foo.invoke(d)
foo.invoke(d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment