Some ramblings about continuation marks
Chris Hanson
(13 Jun 2023 21:51 UTC)
|
||
(missing)
|
||
Fwd: Some ramblings about continuation marks
Marc Nieper-Wißkirchen
(23 Jun 2023 06:23 UTC)
|
||
Re: Some ramblings about continuation marks Philip McGrath (22 Jun 2023 21:21 UTC)
|
||
Re: Some ramblings about continuation marks
Marc Nieper-Wißkirchen
(23 Jun 2023 06:22 UTC)
|
Hi, > > I've been learning about continuation marks so that I can implement > them in MIT/GNU Scheme, and I've run across some things I'd like to > discuss. There are two things: the first is my understanding of the > underlying model, which seems to be different from the one presented; > the second is some quirks about the interface. > I just read this SRFI for the first time, and I don't consider myself an expert on the semantics of continuation marks, but I happened to hear of your message in a Racket chat, and I mostly wanted to suggest asking on <https://racket.discourse.group>: many people there are more qualified than I am to answer, including John Clements and Matthew Flatt, who invented continuation marks, and Jay McCarthy, who advised the master's thesis setting out the implementation strategy in the SRFI sample code. > > 1. > > Trying to understand the model shown in the code was very confusing, > mostly because of the complicated rules about the FLAG value, which is > supposed to indicate whether the expression being evaluated is in > tail-recursive position. In the process, I came up with an > alternative model in which the flag is unnecessary. > > In the alternative model, the MARKS value is a list of frames, as in > the example code. Each mark frame is the mark set of the > corresponding continuation frame. When a continuation frame is > created, it contains the value of MARKS at that point, after which > MARKS is updated by pushing a new empty mark set at the beginning. > When a continuation frame is invoked, MARKS is restored to the stored > value. > > Now, the model I describe suffers from creating many empty frames when > continuation marks are used sparingly, so I'd apply a simple > optimization. When a continuation frame is created, instead of > changing MARKS, it is left as is. Then, which a continuation mark is > set, the value of MARKS is compared to the one stored in the topmost > continuation frame, and if they're the same, a new mark frame is > created. This avoids creating empty mark frames, and pushes some of > the cost onto the use of marks. > > This seems to me to be a very simple and valid model for continuation > marks, but I'd like some feedback from those who've spent more time > thinking about it than I have. > I have not thought sufficiently deeply about either the SRFI sample implementation or your proposal here, but I don't see anything obviously wrong with the basic idea. One important detail, just to be explicit, is that "when a continuation frame is created" and "when a continuation frame is invoked" must apply to the implicit continuation frames created by each non-tail procedure call, not only to first-class continuations from CALL/CC and friends. (Maybe the sample code uses the FLAG value to track non-tail calls in CPSed code? I'd need to look more closely.) You are right to note that adequite performance requires avoiding costs for the many frames that don't change continuation marks. The state-of-the-art implementation strategy for continuation marks is used in Racket's branch of Chez Scheme (and planned to be upstreamed in a future major version). It works by implementing an internal primitive "continuation attachment" and adding the key--value aspect of marks as a library. Details are in Flatt and Dybvig's PLDI 2020 paper, "Compiler and runtime support for continuation marks" <https://doi.org/10.1145/3395632>: there's a corresponding talk at <https://www.youtube.com/watch?v=PiBpCmWq8yQ>. > > 2. > > As for the interface, I have two nits. While I understand that one > goal of this SRFI is compatibility with Racket, I think that the > original design is flawed. > > First, WITH-CONTINUATION-MARK doesn't really make sense, as it misuses > the WITH- pattern generally used in Scheme. This pattern generally > implies that some change is valid for a particular extent that is > delimited by the WITH- form. But that's not the case for > with-continuation-mark: once the change is made, it persists past the > end of the form until the enclosing continuation frame is invoked. As > such, it would make more sense to have SET-CONTINUATION-MARK! which > more clearly indicates what's happening. As a bonus, the latter > syntax is simpler. > I think this might be a misunderstanding. WITH-CONTINUATION-MARKS only has effect within the dynamic extent of its subform. For example, this is #true: (equal? '((1) (2 1) (1)) (map (lambda (marks) (continuation-mark-set->list marks 'k)) (with-continuation-mark 'k 1 (list (current-continuation-marks) (with-continuation-mark 'k 2 (current-continuation-marks)) (current-continuation-marks))))) Perhaps you are thinking of the fact that the continuation might be captured in the dynamic extent of the subform and later restored outside of the WITH-CONTINUATION-MARKS form. For example, this should print (1): (let ([saved-k #f]) (with-continuation-mark 'x 1 ((call/cc (lambda (k) (set! saved-k k) values)))) (when saved-k (with-continuation-mark 'x 2 (saved-k (lambda () (set! saved-k #f) (write (continuation-mark-set->list (current-continuation-marks) 'x)) (newline)))))) But the same is true of other Scheme WITH- forms, as in this version of the pervious example using WITH-EXCEPTION-HANDLER: #!r6rs (import (rnrs)) (let ([saved-k #f]) (with-exception-handler (lambda (e) 1) (lambda () ((call/cc (lambda (k) (set! saved-k k) values))))) (when saved-k (with-exception-handler (lambda (e) 2) (saved-k (lambda () (set! saved-k #f) (write (list (raise-continuable 'k))) (newline)))))) Note that the SRFI ommits the procedure CONTINUATION-MARKS, which makes this more useful: CONTINUATION-MARKS extracts marks from a reified continuation, so CURRENT-CONTINUATION-MARKS becomes equivalent to: (call/cc continuation-marks) (The SRFI also omits CONTINUATION-MARK-SET->ITERATOR, a variant of CONTINUATION-MARK-SET->LIST* that may have better time complexity, especially if early termination is possible.) > > Second, two different names are used for the set of continuation > marks. The procedure CURRENT-CONTINUATION-MARKS retrieves them, and > CONTINUATION-MARKS? tests for such an object. But when interrogating > the sets, CONTINUATION-MARK-SET->*** are used. So what does the > latter mean? Is CONTINUATION-MARK-SET an alias for > CONTINUATION-MARKS, or is it saying that the parameters specify a > subset of the marks and the procedure is returning those? And if the > latter, it seems that the procedure CONTINUATION-MARK-SET->LIST* > should be called CONTINUATION-MARK-SETS->LISTS. > The names in Racket are confusing. In Racket's branch of Chez Scheme, names containing MARK-SET were changes to just say MARKS, since the value in question is not a set data structure. See Racket commit fc81924cbee7a07fe76aa1c9c2b76d0dfde8d6be or commit 2eedc888b06f6223be15cc800107d468e7dd33ef to <https://github.com/racket/ChezScheme> for more discussion. With the renaming, the procedure names become: - with-continuation-mark - continuation-marks? - current-continuation-marks - continuation-next-marks [Racket's continuation-marks] - continuation-marks-first - continuation-marks->list - continuation-marks->iterator - call-with-immediate-continuation-mark Hope this helps, Philip