Skip to content

Commit

Permalink
Coroutine support (#27)
Browse files Browse the repository at this point in the history
* Coroutine support

* Bug fixing & specs

* Coroutine#resume return results

* Add sample
  • Loading branch information
veelenga committed Jun 30, 2017
1 parent c3ea93d commit e7aec57
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 16 deletions.
27 changes: 27 additions & 0 deletions examples/coroutine.cr
@@ -0,0 +1,27 @@
require "../src/lua"

lua = Lua.load
co = lua.run(%q{
function foo (a)
print("foo", a)
return coroutine.yield(2*a)
end
return coroutine.create(function (a,b)
print("co-body", a, b)
local r = foo(a+1)
print("co-body", r)
local r, s = coroutine.yield(a+b, a-b)
print("co-body", r, s)
return b, "end"
end)
}).as(Lua::Coroutine)

res = co.resume 1, 10
puts "main #{res}"

res = co.resume "r"
puts "main #{res}"

res = co.resume "x", "y"
puts "main #{res}"
70 changes: 70 additions & 0 deletions spec/lua/object/coroutine_spec.cr
@@ -0,0 +1,70 @@
require "../../spec_helper"

module Lua
describe Coroutine do
describe "#resume" do
it "starts and resumes coroutine" do
lua = Lua.load
f = lua.run %q{
return function()
coroutine.yield()
end
}
co = lua.newthread(f.as Lua::Function)
co.status.should eq CALL::OK
co.resume
co.status.should eq CALL::YIELD
co.resume
co.status.should eq CALL::OK
lua.close
end

it "can yield arguments" do
lua = Lua.load
f = lua.run %q{
return function(x)
return coroutine.yield()
end
}
co = lua.newthread(f.as Lua::Function)
co.resume.should eq nil
co.resume(42).should eq 42.0
lua.close
end

it "resumes coroutine created with coroutine.create" do
lua = Lua.load
t = lua.run %q{
function s(x)
return coroutine.yield(x)
end
return coroutine.create(s)
}

co = t.as(Lua::Coroutine)
co.resume.should eq nil
co.status.should eq CALL::YIELD
co.resume("test").should eq "test"
co.status.should eq CALL::OK
lua.close
end

it "can return an error" do
lua = Lua.load
t = lua.run %q{
function s(x)
return x * 20
end
return coroutine.create(s)
}

co = t.as(Lua::Coroutine)
expect_raises RuntimeError do
co.resume("hello")
end
co.status.should eq CALL::ERRRUN
lua.close
end
end
end
end
2 changes: 1 addition & 1 deletion src/lua/constants.cr
@@ -1,5 +1,5 @@
module Lua
alias LuaType = Nil | Bool | Float64 | String | Table | Function
alias LuaType = Nil | Bool | Float64 | String | Table | Function | Coroutine

enum TYPE
TNONE = -1
Expand Down
20 changes: 11 additions & 9 deletions src/lua/object.cr
Expand Up @@ -8,24 +8,26 @@ module Lua
# Loads Lua object onto the stack from registry, yields it's
# position (stack top) and removes object from the stack again.
# Used internally to ensure the Lua object is always accessible.
protected def preload
protected def preload(stack = @stack)
check_ref_valid! @ref
copy_to_stack
yield @stack.size
copy_to_stack(stack)
yield stack.size
ensure
@stack.remove
stack.remove
end

protected def copy_to_stack
# Loads Lua object onto the stack from registry by reference.
# Raises `RuntimeError` if reference is not valid.
protected def copy_to_stack(stack = @stack)
check_ref_valid! ref
@stack.rawgeti ref.not_nil!
stack.rawgeti ref.not_nil!
end

# Removes a reference to this Lua object. It is not be possible
# to retrieve the object after it is being released.
def release
if !@stack.closed? && (ref = @ref)
@stack.unref(ref)
def release(stack = @stack)
if !stack.closed? && (ref = @ref)
stack.unref(ref)
@ref = nil
end
end
Expand Down
62 changes: 62 additions & 0 deletions src/lua/object/coroutine.cr
@@ -0,0 +1,62 @@
module Lua
class Coroutine < Object
# Creates new Coroutine with it's own stack
# and a function to execute.
#
# ```
# lua = Lua.load
#
# f = lua.run %q{
# return function()
# print("before yield")
#
# coroutine.yield()
#
# print("after yield")
# end
# }
#
# co = lua.newthread(f.as Lua::Function)
# co.resume # before yield
# co.status # => YIELD
# co.resume # after yield
# co.status # => OK
# ```
#
# Stack may return coroutine. In this case we do not need to
# pass a function 'cause coroutine should already have it:
#
# ```
# t = lua.run %q {
# function s(x)
# return coroutine.yield(x) * 10
# end
#
# return coroutine.create(s)
# }
#
# co = t.as(Lua::Coroutine)
# co.resume # => nil
# co.resume(4.2) # => 42.0
# ```
def initialize(stack, @function : Function? = nil)
super(stack, nil)
end

def resume(*args)
if function = @function
function.preload(@stack) { @stack.resume *args }
else
@stack.resume *args
end
end

def status
@stack.status
end

protected def function=(f : Function)
@function = f
end
end
end
15 changes: 11 additions & 4 deletions src/lua/stack.cr
Expand Up @@ -4,10 +4,11 @@ module Lua
class Stack
include StackMixin::Type
include StackMixin::Util
include StackMixin::TableSupport
include StackMixin::Chunk
include StackMixin::Registry
include StackMixin::TableSupport
include StackMixin::ErrorHandling
include StackMixin::CoroutineSupport
include StackMixin::StandardLibraries

getter state
Expand All @@ -21,15 +22,21 @@ module Lua
#
# By default it loads all standard libraries. But that's possible to
# load only a subset of them using `libs` named parameter. If you
# pass nil as `libs` parameter, any of standard libraries will be loaded.
# pass nil as `libs` parameter, none of standard libraries will be loaded.
#
# ```
# stack = Lua::Stack.new
# # ...
# stack.close
# ```
def initialize(libs = :all)
@state = LibLua.l_newstate
initialize LibLua.l_newstate, libs
end

# Initializes new Lua stack running in an existed state.
# Has to be closed to call the corresponding garbage-collection
# metamethods on Lua side.
def initialize(@state : LibLua::State, libs)
check_lua_supported

open_libs(libs)
Expand Down Expand Up @@ -105,8 +112,8 @@ module Lua
when TYPE::TSTRING then String.new LibLua.tolstring(@state, pos, nil)
when TYPE::TTABLE then Table.new self, reference(pos)
when TYPE::TFUNCTION then Function.new self, reference(pos)
when TYPE::TTHREAD then Coroutine.new Stack.new(LibLua.tothread(@state, pos), libs.to_a)
when TYPE::TUSERDATA then nil # TBD
when TYPE::TTHREAD then nil # TBD
when TYPE::TLIGHTUSERDATA then nil # TBD
else
raise Exception.new "unable to map Lua type '#{type_at(pos)}'"
Expand Down
3 changes: 1 addition & 2 deletions src/lua/stack/chunk.cr
Expand Up @@ -38,8 +38,7 @@ module Lua
call = CALL.new LibLua.pcallk(@state, args.size, Lua::MULTRET, error_handler_pos, 0, nil)
raise self.error(call, pop) if call != CALL::OK

elements = (chunk_pos..size).map { pop }
elements.size > 1 ? elements : elements.first?
pick_results chunk_pos
ensure
self.remove if error_handler_pos != 0 # removes the handler
end
Expand Down
26 changes: 26 additions & 0 deletions src/lua/stack/coroutine_support.cr
@@ -0,0 +1,26 @@
module Lua::StackMixin
module CoroutineSupport
# Creates new thread and returns a coroutine that wraps
# that state and function `f`
def newthread(f : Function)
LibLua.newthread(@state)
pop.as(Coroutine).tap { |c| c.function = f }
end

# Starts and resumes a coroutine in the given thread
protected def resume(*args)
thread_pos = size
args.each { |a| self.<< a }

res = CALL.new LibLua.resume(@state, nil, args.size)
raise error(res, pop) if res > CALL::YIELD

pick_results thread_pos
end

# Returns the status of the current thread.
def status
CALL.new LibLua.status(@state)
end
end
end
5 changes: 5 additions & 0 deletions src/lua/stack/util.cr
Expand Up @@ -21,5 +21,10 @@ module Lua
raise RuntimeError.new "Lua #{ver} not supported. Try Lua 5.3 or higher."
end
end

protected def pick_results(start, finish = size)
elements = (start..finish).map { pop }
elements.size > 1 ? elements : elements.first?
end
end
end

0 comments on commit e7aec57

Please sign in to comment.