Allow passing a value when first starting a fiber.
If the function the fiber is created from takes a parameter, the value passed to the first call() or transfer() gets bound to that parameter. Also, this now correctly handles fibers with functions that take parameters. It used to leave the stack in a busted state. Now, it's a runtime error to create a fiber with a function that takes any more than one parameter.
This commit is contained in:
parent
e043a95e4e
commit
1661f5368f
@ -30,30 +30,36 @@ using the Fiber class's constructor:
|
||||
System.print("This runs in a separate fiber.")
|
||||
}
|
||||
|
||||
Creating a fiber does not immediately run it. It's just a first class bundle of
|
||||
code sitting there waiting to be activated, a bit like
|
||||
a [function](functions.html).
|
||||
It takes a [function][] containing the code the fiber should execute. The
|
||||
function can take zero or one parameter, but no more than that. Creating the
|
||||
fiber does not immediately run it. It just wraps the function and sits there,
|
||||
waiting to be activated.
|
||||
|
||||
[function]: functions.html
|
||||
|
||||
## Invoking fibers
|
||||
|
||||
Once you've created a fiber, you can invoke it, which suspends the current
|
||||
fiber, by calling its `call()` method:
|
||||
Once you've created a fiber, you run it by calling its `call()` method:
|
||||
|
||||
:::wren
|
||||
fiber.call()
|
||||
|
||||
The called fiber executes until it reaches the end of its body or until it
|
||||
passes control to another fiber. If it reaches the end of its body, it's
|
||||
considered *done*:
|
||||
This suspends the current fiber and executes the called one until it reaches the
|
||||
end of its body or until it passes control to yet another fiber. If it reaches
|
||||
the end of its body, it is considered *done*:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new { System.print("Hi") }
|
||||
var fiber = Fiber.new {
|
||||
System.print("It's alive!")
|
||||
}
|
||||
|
||||
System.print(fiber.isDone) //> false
|
||||
fiber.call() //> "Hi"
|
||||
fiber.call() //> It's alive!
|
||||
System.print(fiber.isDone) //> true
|
||||
|
||||
When it finishes, it automatically resumes the fiber that called it. It's a
|
||||
runtime error to try to call a fiber that is already done.
|
||||
When a called fiber finishes, it automatically passes control *back* to the
|
||||
fiber that called it. It's a runtime error to try to call a fiber that is
|
||||
already done.
|
||||
|
||||
## Yielding
|
||||
|
||||
@ -70,16 +76,16 @@ You make a fiber yield by calling the static `yield()` method on Fiber:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
System.print("before yield")
|
||||
System.print("Before yield")
|
||||
Fiber.yield()
|
||||
System.print("resumed")
|
||||
System.print("Resumed")
|
||||
}
|
||||
|
||||
System.print("before call") //> before call
|
||||
fiber.call() //> before yield
|
||||
System.print("calling again") //> calling again
|
||||
fiber.call() //> resumed
|
||||
System.print("all done") //> add done
|
||||
System.print("Before call") //> Before call
|
||||
fiber.call() //> Before yield
|
||||
System.print("Calling again") //> Calling again
|
||||
fiber.call() //> Resumed
|
||||
System.print("All done") //> All done
|
||||
|
||||
Note that even though this program uses *concurrency*, it is still
|
||||
*deterministic*. You can reason precisely about what it's doing and aren't at
|
||||
@ -88,22 +94,30 @@ the mercy of a thread scheduler playing Russian roulette with your code.
|
||||
## Passing values
|
||||
|
||||
Calling and yielding fibers is used for passing control, but it can also pass
|
||||
*data*. When you call a fiber, you can optionally pass a value to it. If the
|
||||
fiber has yielded and is waiting to resume, the value becomes the return value
|
||||
of the `yield()` call:
|
||||
*data*. When you call a fiber, you can optionally pass a value to it.
|
||||
|
||||
If you create a fiber using a function that takes a parameter, you can pass a
|
||||
value to it through `call()`:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
var fiber = Fiber.new {|param|
|
||||
System.print(param)
|
||||
}
|
||||
|
||||
fiber.call("Here you go") //> Here you go
|
||||
|
||||
If the fiber has yielded and is waiting to resume, the value you pass to call
|
||||
becomes the return value of the `yield()` call when it resumes:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {|param|
|
||||
System.print(param)
|
||||
var result = Fiber.yield()
|
||||
System.print(result)
|
||||
}
|
||||
|
||||
fiber.call("discarded")
|
||||
fiber.call("sent")
|
||||
|
||||
This prints "sent". Note that the first value sent to the fiber through call is
|
||||
ignored. That's because the fiber isn't waiting on a `yield()` call, so there's
|
||||
nowhere for the sent value to go.
|
||||
fiber.call("First") //> First
|
||||
fiber.call("Second") //> Second
|
||||
|
||||
Fibers can also pass values *back* when they yield. If you pass an argument to
|
||||
`yield()`, that will become the return value of the `call()` that was used to
|
||||
@ -111,12 +125,13 @@ invoke the fiber:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
Fiber.yield("sent")
|
||||
Fiber.yield("Reply")
|
||||
}
|
||||
|
||||
System.print(fiber.call())
|
||||
System.print(fiber.call()) //> Reply
|
||||
|
||||
This also prints "sent".
|
||||
This is sort of like how a function call may return a value, except that a fiber
|
||||
may return a whole sequence of values, one every time it yields.
|
||||
|
||||
## Full coroutines
|
||||
|
||||
@ -125,9 +140,11 @@ Python and C# that have *generators*. Those let you define a function call that
|
||||
you can suspend and resume. When using the function, it appears like a sequence
|
||||
you can iterate over.
|
||||
|
||||
Wren's fibers can do that, but they can do much more. Like Lua, they are
|
||||
full *coroutines*—they can suspend from anywhere in the callstack. For
|
||||
example:
|
||||
Wren's fibers can do that, but they can do much more. Like Lua, they are full
|
||||
*coroutines*—they can suspend from anywhere in the callstack. The function
|
||||
you use to create a fiber can call a method that calls another method that calls
|
||||
some third method which finally calls yield. When that happens, *all* of those
|
||||
method calls — the entire callstack — gets suspended. For example:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
@ -148,15 +165,21 @@ Fibers have one more trick up their sleeves. When you execute a fiber using
|
||||
lets you build up a chain of fiber calls that will eventually unwind back to
|
||||
the main fiber when all of the called ones yield or finish.
|
||||
|
||||
This is almost always what you want. But if you're doing something really low
|
||||
level, like writing your own scheduler to manage a pool of fibers, you may not
|
||||
want to treat them explicitly like a stack.
|
||||
This is usually what you want. But if you're doing something low level, like
|
||||
writing your own scheduler to manage a pool of fibers, you may not want to treat
|
||||
them explicitly like a stack.
|
||||
|
||||
For rare cases like that, fibers also have a `transfer()` method. This switches
|
||||
execution immediately to the transferred fiber. The previous one is suspended,
|
||||
leaving it in whatever state it was in. You can resume the previous fiber by
|
||||
transferring back to it, or even calling it. If you don't, execution stops when
|
||||
the last transferred fiber returns.
|
||||
execution to the transferred fiber and "forgets" the fiber that was transferred
|
||||
*from*. The previous one is suspended, leaving it in whatever state it was in.
|
||||
You can resume the previous fiber by explicitly transferring back to it, or even
|
||||
calling it. If you don't, execution stops when the last transferred fiber
|
||||
returns.
|
||||
|
||||
Where `call()` and `yield()` are analogous to calling and returning from
|
||||
functions, `transfer()` works more like an unstructured goto. It lets you freely
|
||||
switch control between a number of fibers, all of which act as peers to one
|
||||
another.
|
||||
|
||||
<a class="right" href="error-handling.html">Error Handling →</a>
|
||||
<a href="classes.html">← Classes</a>
|
||||
|
||||
@ -29,6 +29,11 @@ fiber is run. Does not immediately start running the fiber.
|
||||
System.print("I won't get printed")
|
||||
}
|
||||
|
||||
`function` must be a function (an actual [Fn][] instance, not just an object
|
||||
with a `call()` method) and it may only take zero or one parameters.
|
||||
|
||||
[fn]: fn.html
|
||||
|
||||
### Fiber.**suspend**()
|
||||
|
||||
Pauses the current fiber, and stops the interpreter. Control returns to the
|
||||
@ -97,46 +102,36 @@ Similar to `Fiber.yield` but provides a value to return to the parent fiber's
|
||||
|
||||
### **call**()
|
||||
|
||||
Starts or resumes the fiber if it is in a paused state.
|
||||
Starts or resumes the fiber if it is in a paused state. Equivalent to:
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
System.print("Fiber called")
|
||||
Fiber.yield()
|
||||
System.print("Fiber called again")
|
||||
}
|
||||
|
||||
fiber.call() // Start it.
|
||||
fiber.call() // Resume after the yield() call.
|
||||
|
||||
When the called fiber yields, control is transferred back to the fiber that
|
||||
called it.
|
||||
|
||||
If the called fiber is resuming from a yield, the `yield()` method returns
|
||||
`null` in the called fiber.
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
System.print(Fiber.yield())
|
||||
}
|
||||
|
||||
fiber.call()
|
||||
fiber.call() //> null
|
||||
fiber.call(null)
|
||||
|
||||
### **call**(value)
|
||||
|
||||
Invokes the fiber or resumes the fiber if it is in a paused state and sets
|
||||
`value` as the returned value of the fiber's call to `yield`.
|
||||
Start or resumes the fiber if it is in a paused state. If the fiber is being
|
||||
started for the first time, and its function takes a parameter, `value` is
|
||||
passed to it.
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {|param|
|
||||
System.print(param) //> begin
|
||||
}
|
||||
|
||||
fiber.call("begin")
|
||||
|
||||
If the fiber is being resumed, `value` becomes the returned value of the fiber's
|
||||
call to `yield`.
|
||||
|
||||
:::wren
|
||||
var fiber = Fiber.new {
|
||||
System.print(Fiber.yield())
|
||||
System.print(Fiber.yield()) //> resume
|
||||
}
|
||||
|
||||
fiber.call()
|
||||
fiber.call("value") //> value
|
||||
fiber.call("resume")
|
||||
|
||||
### **error***
|
||||
### **error**
|
||||
|
||||
The error message that was passed when aborting the fiber, or `null` if the
|
||||
fiber has not been aborted.
|
||||
@ -154,8 +149,6 @@ fiber has not been aborted.
|
||||
Whether the fiber's main function has completed and the fiber can no longer be
|
||||
run. This returns `false` if the fiber is currently running or has yielded.
|
||||
|
||||
### **transfer**()
|
||||
|
||||
### **try**()
|
||||
Tries to run the fiber. If a runtime error occurs
|
||||
in the called fiber, the error is captured and is returned as a string.
|
||||
@ -170,6 +163,8 @@ in the called fiber, the error is captured and is returned as a string.
|
||||
|
||||
If the called fiber raises an error, it can no longer be used.
|
||||
|
||||
### **transfer**()
|
||||
|
||||
**TODO**
|
||||
|
||||
### **transfer**(value)
|
||||
|
||||
@ -53,14 +53,20 @@ DEF_PRIMITIVE(fiber_new)
|
||||
{
|
||||
if (!validateFn(vm, args[1], "Argument")) return false;
|
||||
|
||||
ObjFiber* newFiber = wrenNewFiber(vm, AS_CLOSURE(args[1]));
|
||||
ObjClosure* closure = AS_CLOSURE(args[1]);
|
||||
if (closure->fn->arity > 1)
|
||||
{
|
||||
RETURN_ERROR("Function cannot take more than one parameter.");
|
||||
}
|
||||
|
||||
ObjFiber* newFiber = wrenNewFiber(vm, closure);
|
||||
|
||||
// The compiler expects the first slot of a function to hold the receiver.
|
||||
// Since a fiber's stack is invoked directly, it doesn't have one, so put it
|
||||
// in here.
|
||||
newFiber->stack[0] = NULL_VAL;
|
||||
newFiber->stackTop++;
|
||||
|
||||
|
||||
RETURN_OBJ(newFiber);
|
||||
}
|
||||
|
||||
@ -106,9 +112,20 @@ static bool runFiber(WrenVM* vm, ObjFiber* fiber, Value* args, bool isCall,
|
||||
// need one slot for the result, so discard the other slot now.
|
||||
if (hasValue) vm->fiber->stackTop--;
|
||||
|
||||
// If the fiber was paused, make yield() or transfer() return the result.
|
||||
if (fiber->stackTop > fiber->stack)
|
||||
if (fiber->numFrames == 1 &&
|
||||
fiber->frames[0].ip == fiber->frames[0].closure->fn->code.data)
|
||||
{
|
||||
// The fiber is being started for the first time. If its function takes a
|
||||
// parameter, bind an argument to it.
|
||||
if (fiber->frames[0].closure->fn->arity == 1)
|
||||
{
|
||||
fiber->stackTop[0] = hasValue ? args[1] : NULL_VAL;
|
||||
fiber->stackTop++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The fiber is being resumed, make yield() or transfer() return the result.
|
||||
fiber->stackTop[-1] = hasValue ? args[1] : NULL_VAL;
|
||||
}
|
||||
|
||||
|
||||
@ -170,7 +170,7 @@ ObjFiber* wrenNewFiber(WrenVM* vm, ObjClosure* closure)
|
||||
|
||||
void wrenResetFiber(WrenVM* vm, ObjFiber* fiber, ObjClosure* closure)
|
||||
{
|
||||
// Push the stack frame for the function.
|
||||
// Reset everything.
|
||||
fiber->stackTop = fiber->stack;
|
||||
fiber->openUpvalues = NULL;
|
||||
fiber->caller = NULL;
|
||||
|
||||
@ -3,5 +3,5 @@ var fiber = Fiber.new {
|
||||
}
|
||||
|
||||
System.print("before") // expect: before
|
||||
fiber.call() // expect: fiber
|
||||
fiber.call() // expect: fiber
|
||||
System.print("after") // expect: after
|
||||
|
||||
7
test/core/fiber/call_to_parameter.wren
Normal file
7
test/core/fiber/call_to_parameter.wren
Normal file
@ -0,0 +1,7 @@
|
||||
var fiber = Fiber.new {|value|
|
||||
System.print(value)
|
||||
}
|
||||
|
||||
System.print("before") // expect: before
|
||||
fiber.call() // expect: null
|
||||
System.print("after") // expect: after
|
||||
@ -2,8 +2,6 @@ var fiber = Fiber.new {
|
||||
System.print("fiber")
|
||||
}
|
||||
|
||||
// The first value passed to the fiber is ignored, since there's no yield call
|
||||
// to return it.
|
||||
System.print("before") // expect: before
|
||||
fiber.call("ignored") // expect: fiber
|
||||
System.print("after") // expect: after
|
||||
System.print("before") // expect: before
|
||||
fiber.call("value") // expect: fiber
|
||||
System.print("after") // expect: after
|
||||
|
||||
7
test/core/fiber/call_with_value_to_parameter.wren
Normal file
7
test/core/fiber/call_with_value_to_parameter.wren
Normal file
@ -0,0 +1,7 @@
|
||||
var fiber = Fiber.new {|value|
|
||||
System.print(value)
|
||||
}
|
||||
|
||||
System.print("before") // expect: before
|
||||
fiber.call("value") // expect: value
|
||||
System.print("after") // expect: after
|
||||
1
test/core/fiber/new_wrong_arity.wren
Normal file
1
test/core/fiber/new_wrong_arity.wren
Normal file
@ -0,0 +1 @@
|
||||
var fiber = Fiber.new {|a, b| null } // expect runtime error: Function cannot take more than one parameter.
|
||||
24
test/core/fiber/transfer_to_parameter.wren
Normal file
24
test/core/fiber/transfer_to_parameter.wren
Normal file
@ -0,0 +1,24 @@
|
||||
var a = Fiber.new {|param|
|
||||
System.print("a %(param)")
|
||||
}
|
||||
|
||||
var b = Fiber.new {|param|
|
||||
System.print("b before %(param)")
|
||||
a.transfer()
|
||||
System.print("b after")
|
||||
}
|
||||
|
||||
var c = Fiber.new {|param|
|
||||
System.print("c before %(param)")
|
||||
b.transfer()
|
||||
System.print("c after")
|
||||
}
|
||||
|
||||
System.print("start") // expect: start
|
||||
|
||||
c.transfer()
|
||||
// expect: c before null
|
||||
// expect: b before null
|
||||
// expect: a null
|
||||
|
||||
// Nothing else gets run since the interpreter stops after a completes.
|
||||
24
test/core/fiber/transfer_with_value_to_parameter.wren
Normal file
24
test/core/fiber/transfer_with_value_to_parameter.wren
Normal file
@ -0,0 +1,24 @@
|
||||
var a = Fiber.new {|param|
|
||||
System.print("a %(param)")
|
||||
}
|
||||
|
||||
var b = Fiber.new {|param|
|
||||
System.print("b before %(param)")
|
||||
a.transfer("from b")
|
||||
System.print("b after")
|
||||
}
|
||||
|
||||
var c = Fiber.new {|param|
|
||||
System.print("c before %(param)")
|
||||
b.transfer("from c")
|
||||
System.print("c after")
|
||||
}
|
||||
|
||||
System.print("start") // expect: start
|
||||
|
||||
c.transfer("from main")
|
||||
// expect: c before from main
|
||||
// expect: b before from c
|
||||
// expect: a from b
|
||||
|
||||
// Nothing else gets run since the interpreter stops after a completes.
|
||||
@ -7,8 +7,8 @@ var fiber = Fiber.new {
|
||||
}
|
||||
|
||||
System.print(fiber.call()) // expect: fiber 1
|
||||
// expect: yield 1
|
||||
// expect: yield 1
|
||||
System.print(fiber.call()) // expect: fiber 2
|
||||
// expect: yield 2
|
||||
// expect: yield 2
|
||||
System.print(fiber.call()) // expect: fiber 3
|
||||
// expect: null
|
||||
// expect: null
|
||||
|
||||
Loading…
Reference in New Issue
Block a user