Skip to content

Commit 948a2ce

Browse files
committed
Initial documentation for the continuations feature
1 parent 72d5841 commit 948a2ce

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed

docs/continuations.pod

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
=head1 Continuations in NQP
2+
3+
This document describes a somewhat experimental and JVM-only NQP feature: the
4+
ability to freeze and thaw stack frames for implementing unusual control flow.
5+
6+
=head2 Interface
7+
8+
I think that the best abstraction to what I'm trying to build here is the
9+
B<delimited continuation> (L<https://en.wikipedia.org/wiki/Delimited_continuation>).
10+
Those traditionally have three operations:
11+
12+
=over 4
13+
14+
=item nqp::continuationreset($tag, { ... })
15+
16+
Executes the argument, marking the stack for a subsequent C<nqp::continuationshift>
17+
operation within the dynamic scope. The C<$tag> is an object which can be used
18+
later by C<nqp::continuationshift> to find a specific reset frame.
19+
20+
=item nqp::continuationshift($tag, -> $cont { ... })
21+
22+
Slices off the part of the evaluation stack between the current call frame and
23+
the innermost enclosing C<nqp::continuationreset>. If C<$tag> is not null,
24+
only resets with the same tag are considered; otherwise the innermost reset
25+
will be taken regardless of tag. If there is no such reset, or if there is a
26+
non-saveable frame (aka continuation barrier) between the current position and
27+
the matching reset, an error occurs. The sliced-off part of the stack is
28+
wrapped in an NQP object and passed to the callback function; it is removed
29+
from the stack, so B<if the callback returns without using the continuation,
30+
the effect is to cause C<nqp::continuationreset> to return immediately with the
31+
returned value>.
32+
33+
=item nqp::continuationinvoke($cont, $return)
34+
35+
Pastes the saved call frames onto the current stack, such that
36+
C<nqp::continuationshift> returns C<$return>. Control returns to the caller
37+
when the callback to C<nqp::continuationreset> returns, with the same value.
38+
This can be used multiple times on the same continuation. (Actually, delimited
39+
continuations are traditionally represented as functions, so this operator is
40+
implicit and unnamed. But sixmodel makes that slightly tricky.)
41+
42+
=item nqp::continuationclone($cont)
43+
44+
Produces a shallow clone of the passed continuation. This is presently
45+
necessary in situations where a continuation must be active twice at the same
46+
time. At present, lexical variables will remain shared but locals will not.
47+
B<Details here are subject greatly to change.>
48+
49+
# should be 3 * 3 * 10 = 90
50+
# will infinite loop if the clones are removed
51+
my $cont := nqp::continuationreset(-> $ {
52+
3 * (nqp::continuationshift(0, -> $k { $k }))();
53+
});
54+
nqp::continuationinvoke(nqp::continuationclone($cont),
55+
-> { nqp::continuationinvoke(nqp::continuationclone($cont)), -> { 10 }) });
56+
57+
=back
58+
59+
By way of example, here is Scheme's call/cc implemented using NQP delimited
60+
continuations:
61+
62+
# for proper R5RS semantics, run this once wrapping your main function
63+
sub run_main($f) {
64+
nqp::continuationreset(nqp::null(), { $f() });
65+
}
66+
67+
sub callcc($f) {
68+
# first get the current continuation
69+
nqp::continuationshift(nqp::null(), -> $dcont {
70+
my $scheme_cont := -> $val {
71+
# when the scheme continuation is invoked, we need to *replace*
72+
# the current continuation with this one
73+
nqp::continuationshift(nqp::null(), -> $ {
74+
nqp::continuationinvoke($dcont, $val)
75+
});
76+
};
77+
$scheme_cont($f($scheme_cont));
78+
});
79+
}
80+
81+
And here is something resembling gather/take:
82+
83+
sub yield($value) {
84+
nqp::continuationshift(nqp::null(), -> $dcont { [$value, $dcont] });
85+
}
86+
87+
sub start_iter($body) {
88+
my $state := -> $ { $body() };
89+
-> {
90+
my $pkt := nqp::continuationreset(nqp::null(), { $state(NQPMu); });
91+
$state := $pkt[1];
92+
$pkt[0];
93+
}
94+
}
95+
96+
=head1 Conjectures
97+
98+
=head1 Lazy recursive reinstate optimization
99+
100+
Consider the following (Perl 6):
101+
102+
my $N = 10000;
103+
104+
sub flatten($x) {
105+
multi go(@k) { go($_) for @k }
106+
multi go($k) { take $k }
107+
108+
gather go($x);
109+
}
110+
111+
my $list = [^$N];
112+
$list = [$list] for ^$N;
113+
say flatten($list).perl;
114+
115+
This takes O(N^2) time on the current implementation. Why? Because each time
116+
take is invoked, we are N frames deep, so each take does O(N) work, and there
117+
are N calls to take. We can improve this to O(N) by doing the continuation
118+
operations B<lazily>. That is, when reinstating a continuation only reinstate
119+
the top frame(s) that will be executed, and skip the work of reinstating the
120+
non-top frames only to resave them later. The design of this is a bit
121+
handwavey at the moment.
122+
123+
=head1 Multiple callers
124+
125+
There are two sensible ways to define the caller of a call frame. Either the
126+
frame which caused this frame to exist (henceforth, the static caller) or the
127+
frame which caused this frame to be active (henceforth, the dynamic caller).
128+
They are the same for most frames, but differ in the case of the top frame of a
129+
gather. The static caller of such a frame is the frame containing C<gather>;
130+
the dynamic caller is the frame corresponding to C<GatherIter.reify>. We need
131+
both: contextuals use the static caller (TimToady has said so quite
132+
explicitly), while exceptions and control flow ought to use the dynamic caller
133+
(people expect lazy exceptions to show up and backtrace at the point where the
134+
list is used). So we might need to B<add a dynamicCaller field to CallFrame
135+
and come up with updating logic>. Niecza does precisely this, and I think
136+
parrot is doing something similar.
137+

0 commit comments

Comments
 (0)