File: continuations.pod

package info (click to toggle)
nqp 2024.09%2Bdfsg-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,972 kB
  • sloc: java: 28,087; perl: 3,479; ansic: 451; makefile: 202; javascript: 68; sh: 1
file content (161 lines) | stat: -rw-r--r-- 6,874 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
=head1 Continuations in NQP

This document describes the ability to freeze and thaw stack frames
for implementing unusual control flow.

=head2 Interface

I think that the best abstraction to what I'm trying to build here is the
B<delimited continuation> (L<https://en.wikipedia.org/wiki/Delimited_continuation>).

=over 4

=item nqp::continuationreset($tag, { ... })

Executes the argument, marking the stack for a subsequent C<nqp::continuationcontrol>
operation within the dynamic scope.  The C<$tag> is an object which can be used
later by C<nqp::continuationcontrol> to find a specific reset frame.

=item nqp::continuationcontrol($protect, $tag, -> $cont { ... })

Slices off the part of the evaluation stack between the current call frame and
the innermost enclosing C<nqp::continuationreset>.  If C<$tag> is not null,
only resets with the same tag are considered; otherwise the innermost reset
will be taken regardless of tag.  If there is no such reset, or if there is a
non-saveable frame (aka continuation barrier) between the current position and
the matching reset, an error occurs.  The sliced-off part of the stack is
wrapped in an NQP object and passed to the callback function; it is removed
from the stack, so B<if the callback returns without using the continuation,
the effect is to cause C<nqp::continuationreset> to return immediately with the
returned value>.

C<$protect> is an integer.  If it is 1, the reset will be retained on the stack
while the handler is being executed; if it is 0, the reset will be removed.
Other values are undefined.  In no case will the matched reset be included in
the captured continuation object (although an unmatched reset which is skipped
over would be).  Thus, C<control(1, ...)> corresponds to the standard C<control>
operator, while C<control(0, ...)> corresponds to the standard C<control0>
operator.  To simulate C<shift> and C<shift0>, manually add the reset before
invoking the continuation.

=item nqp::continuationinvoke($cont, $inject)

Pastes the saved call frames onto the current stack, such that
C<nqp::continuationcontrol> calls C<$inject> within the restored dynamic scope
and returns its return value.  Control returns to the caller when the callback
to C<nqp::continuationreset> returns, with the same value.  This can be used
multiple times on the same continuation.  (Actually, delimited continuations
are traditionally represented as functions, so this operator is implicit and
unnamed.  But sixmodel makes that slightly tricky.)

=item nqp::continuationclone($cont)

Produces a shallow clone of the passed continuation.  This is presently
necessary in situations where a continuation must be active twice at the same
time.  At present, lexical variables will remain shared but locals will not.
B<Details here are subject greatly to change.>

    # should be 3 * 3 * 10 = 90
    # will infinite loop if the clones are removed
    my $cont := nqp::continuationreset(nqp::null(), {
        3 * nqp::continuationcontrol(0, nqp::null(), -> $k { $k });
    });
    my $val := nqp::continuationinvoke(nqp::continuationclone($cont),
        { nqp::continuationinvoke(nqp::continuationclone($cont), { 10 }) });

=back

By way of example, here is Scheme's call/cc implemented using NQP delimited
continuations:

    # for proper R5RS semantics, run this once wrapping your main function
    sub run_main($f) {
        nqp::continuationreset(nqp::null(), $f);
    }

    sub callcc($f) {
        # first get the current continuation
        nqp::continuationcontrol(1, nqp::null(), -> $dcont {
            my $scheme_cont := -> $val {
                # when the scheme continuation is invoked, we need to *replace*
                # the current continuation with this one
                nqp::continuationcontrol(1, nqp::null(), -> $c {
                    nqp::continuationinvoke($dcont, { $val })
                });
            };
            nqp::continuationinvoke($dcont, { $f($scheme_cont) });
        });
    }

And here is something resembling gather/take:

    my $SENTINEL := [];
    sub yield($value) {
        nqp::continuationcontrol(0, nqp::null(), -> $dcont {
            [$value, { nqp::continuationinvoke($dcont, {0}) }]
        });
    }

    sub start_iter($body) {
        my $state := { $body(); yield($SENTINEL) };
        -> {
            my $pkt := nqp::continuationreset(nqp::null(), $state);
            $state := $pkt[1];
            $pkt[0];
        }
    }

Complete examples may be found in t/jvm/01-continuations.t in the source distribution.

=head1 Conjectures

=head2 Lazy recursive reinstate optimization

Consider the following (Raku):

    my $N = 10000;

    sub flatten($x) {
        multi go(@k) { go($_) for @k }
        multi go($k) { take $k }

        gather go($x);
    }

    my $list = [^$N];
    $list = [$list] for ^$N;
    say flatten($list).raku;

This takes O(N^2) time on the current implementation.  Why?  Because each time
take is invoked, we are N frames deep, so each take does O(N) work, and there
are N calls to take.  We can improve this to O(N) by doing the continuation
operations B<lazily>.  That is, when reinstating a continuation only reinstate
the top frame(s) that will be executed, and skip the work of reinstating the
non-top frames only to resave them later.  The design of this is a bit
handwavey at the moment.

=head2 Multiple callers

There are two sensible ways to define the caller of a call frame.  Either the
frame which caused this frame to exist (henceforth, the static caller) or the
frame which caused this frame to be active (henceforth, the dynamic caller).
They are the same for most frames, but differ in the case of the top frame of a
gather.  The static caller of such a frame is the frame containing C<gather>;
the dynamic caller is the frame corresponding to C<GatherIter.reify>.  We need
both: contextuals use the static caller (TimToady has said so quite
explicitly), while exceptions and control flow ought to use the dynamic caller
(people expect lazy exceptions to show up and backtrace at the point where the
list is used).  So we might need to B<add a dynamicCaller field to CallFrame
and come up with updating logic>.  Niecza does precisely this, and I think
parrot is doing something similar.

=head2 Saner cloning

C<nqp::continuationclone> is bad because it exposes details of what the JVM
implementation can do without warning and what requires warnings.  It would be
better to expliclty declare exactly what you intend to do with the continuation
as a bit flag passed to control and/or invoke, and let the op set figure out
itself when cloning is needed.  Flags could include "I don't intend to call
this at all" (control used as an escape operator), "I intend to call this
exactly once" (coroutines), "I may use this more than once, but only on one
thread", "I want to use this on several threads".