1. 34
  1. 9

    I can’t believe I wrote the Usenet post about Dylan referenced in this article 22 years ago - after all that time are there any mainstream languages using conditions/restarts?

    1. 8

      Smalltalk doesn’t have exceptions as a language-level construct. Instead, blocks (closures/lambdas) can return from the scope in which the block was created (assuming it’s still on the stack). You implement exceptions by pushing a handler block onto the top of a global stack, invoking it at the ‘throw’ point, and having it return. Smalltalk ‘stacks’ are actually lists of activation records where every local is a field of the object and amenable to introspection, so you can also use it to walk up the stack and build resumable or restartable exceptions. Not really a mainstream language though.

      Perhaps more interesting, the language-agnostic parts of the SEH mechanism on Windows fully support resumable and restartable exceptions. There are some comments about this in the public source in the MSVC redistributable things. To my knowledge, no language has ever actually used them. This is much easier with SEH than with Itanium-style unwinding. The Itanium ABI does a two-pass unwind, where one pass finds cleanups and catch handlers, the second pass runs them. This means that the stack is destroyed by the time that you get to the catch block: each cleanup runs in the stack frame that it’s unwinding through, so implicitly destroys anything that has been unwound through by trampling over its stack. In contrast, the SEH mechanism invokes ‘funclets’, new functions that run on top of the stack with a pointer to the stack pointer for the frame that they’re cleaning up. This means that it’s possible to quite easily shuffle the order in which they are executed and allow a far-off handler to decide, based on the exception object and any other state that it has access to, that it wants to just adjust the object and resume execution from a point in the frame that threw the exception or control the unwinding process further.

      Oh, and one of the comments in the article talks about longjmping out of a signal handler. Never do this. setjmp stores only the callee-save register state: it is a function call and so the caller is responsible for saving any caller-save state. Signals are delivered at arbitrary points, not just at call points, and so you will corrupt some subset of your register state if you do this. setcontext exists specifically to jump out of signal handlers. Similarly, you should not throw out of signal handlers. In theory, DWARF unwind metadata can express everything that you need for this (the FreeBSD signal trampolines now have complete unwind info, so you get back into the calling frame correctly) but both LLVM and GCC assume that exceptions are thrown only at call sites and so it is very likely that the information will be incorrect for the top frame. It will usually work if the top frame doesn’t try to catch the exception, because generally spills and reloads happen in the prolog / epilog and so the unwind state for non-catching functions will correctly restore everything that the parent frame needs.

      1. 2

        Oh, and one of the comments in the article talks about longjmping out of a signal handler. Never do this. setjmp stores only the callee-save register state: it is a function call and so the caller is responsible for saving any caller-save state. Signals are delivered at arbitrary points, not just at call points, and so you will corrupt some subset of your register state if you do this.

        I think you’re wrong here.

        The POSIX standard says this about invoking longjmp() from a signal handler:

        The behavior of async-signal-safe functions, as defined by this section, is as specified by POSIX.1, regardless of invocation from a signal-catching function. This is the only intended meaning of the statement that async-signal-safe functions may be used in signal-catching functions without restriction. … Note that although longjmp() and siglongjmp() are in the list of async-signal-safe functions, there are restrictions on subsequent behavior after the function is called from a signal-catching function. This is because the code executing after longjmp() or siglongjmp() can call any unsafe functions with the same danger as calling those unsafe functions directly from the signal handler. Applications that use longjmp() or siglongjmp() out of signal handlers require rigorous protection in order to be portable.

        It also says this:

        Although longjmp() is an async-signal-safe function, if it is invoked from a signal handler which interrupted a non-async-signal-safe function or equivalent (such as the processing equivalent to exit() performed after a return from the initial call to main()), the behavior of any subsequent call to a non-async-signal-safe function or equivalent is undefined.

        So it seems the extra restriction mentioned before is that longjmp() and siglongjmp() cannot be used from a signal handler that interrupted a non-async-signal-safe function.

        There are other restrictions. The standard says this about them:

        If the most recent invocation of setjmp() with the corresponding jmp_buf occurred in another thread, or if there is no such invocation, or if the function containing the invocation of setjmp() has terminated execution in the interim, or if the invocation of setjmp() was within the scope of an identifier with variably modified type and execution has left that scope in the interim, the behavior is undefined.

        So the function with the setjmp() invocation must be in the same thread and still on the stack (i.e., the invoking function has not returned), and it also has to be in scope (i.e., execution must be in the same C scope as the setjmp() invocation).

        In addition:

        All accessible objects have values, and all other components of the abstract machine have state (for example, floating-point status flags and open files), as of the time longjmp() was called, except that the values of objects of automatic storage duration are unspecified if they meet all the following conditions:

        • They are local to the function containing the corresponding setjmp() invocation.

        • They do not have volatile-qualified type.

        • They are changed between the setjmp() invocation and longjmp() call.

        All three of those conditions have to exist for the value of local variables to be unspecified. If you have a pointer to something in the stack, but in a function above, you’re fine; the pointer doesn’t change, and the contents at the pointer are specified. If you have a pointer to a heap allocation, the contents of the heap allocation are specified. And so on.

        All of this is to say that you are broadly right, that code should usually not longjmp() out of signal handlers. But to say never seems a bit much, if you do everything right. Of course, like crypto, you should only do it if you know what you’re doing.

        Also, I believe that the quotes above mean that if an implementation does not save caller-save registers, it is wrong. The reason is that the compiler could have lifted one local into a caller-save register. If that local does not change between the initial setjmp() and the longjmp() back to it, that caller-save register should have the same value, and if it doesn’t, I argue that the implementation does not follow the standard, that the implementation is wrong, not the application, especially since it is legal for an implementation to use a macro. In fact, the standard has several restrictions on how setjmp() can be invoked to make it easier to implement as a macro:

        An application shall ensure that an invocation of setjmp() appears in one of the following contexts only:

        • The entire controlling expression of a selection or iteration statement

        • One operand of a relational or equality operator with the other operand an integral constant expression, with the resulting expression being the entire controlling expression of a selection or iteration statement

        • The operand of a unary ‘!’ operator with the resulting expression being the entire controlling expression of a selection or iteration

        • The entire expression of an expression statement (possibly cast to void)

        If the invocation appears in any other context, the behavior is undefined.

        Source: I implemented a longjmp() out of a signal handler in my bc and followed all of the relevant standard restrictions I quoted above. It was hard, yes, and I had to have signal “locks” for the signal handler to tell if execution was in a non-async-signal-safe function (in which case, it sets a flag, and returns normally, and when the signal lock is removed, the jump happens then).

        1. 3

          Also, I believe that the quotes above mean that if an implementation does not save caller-save registers, it is wrong.

          If that is the case, then there are no correct implementations. For example, the glibc version saves only 8 registers on x86-64. Similarly, the FreeBSD version stores 8 integer registers. This means that they are not storing all of the integer register set, let alone any floating-point or vector state. This is in contrast to ucontext, which does store the entire state. The sig prefixed versions also store the signal mask and restore it.

          It looks as if ucontext is finally gone from the latest version of POSIX, but the previous version said this:

          When a signal handler is executed, the current user context is saved and a new context is created. If the thread leaves the signal handler via longjmp(), then it is unspecified whether the context at the time of the corresponding setjmp() call is restored and thus whether future calls to getcontext() provide an accurate representation of the current context, since the context restored by longjmp() does not necessarily contain all the information that setcontext() requires. Signal handlers should use siglongjmp() or setcontext() instead.

          Explicitly: longjmp does not store the entire context, any temporary registers may not be stored.

          Also, I believe that the quotes above mean that if an implementation does not save caller-save registers, it is wrong. The reason is that the compiler could have lifted one local into a caller-save register. If that local does not change between the initial setjmp() and the longjmp() back to it, that caller-save register should have the same value, and if it doesn’t, I argue that the implementation does not follow the standard, that the implementation is wrong, not the application, especially since it is legal for an implementation to use a macro.

          No, this is why setjmp explicitly requires that all local mutable state that you access after the return is held in volatile variables: to force the compiler to explicitly save them to the stack (or elsewhere) and reload them. If they are local, not volatile, and have changed, then accessing them is UB. Any subset of these is fine:

          • If they are not volatile and are local, but have not changed, then they must be stored either on the stack or in a callee-save register because otherwise the function call (for setjmp) may clobber them and so that’s fine.
          • If they are local and have not changed, but are not volatile, then they will be stored in the same location (register or stack slot) in the use point as before the setjmp call (which must be something that’s preserved across calls).
          • If they are not local, then any local modifications must have been written back to the global before setjmp and any access after setjmp returns must reload.

          Source: I implemented a longjmp() out of a signal handler in my bc and followed all of the relevant standard restrictions I quoted above. It was hard, yes, and I had to have signal “locks” for the signal handler to tell if execution was in a non-async-signal-safe function (in which case, it sets a flag, and returns normally, and when the signal lock is removed, the jump happens then).

          I hope that you mean siglongjmp, not longjmp, because otherwise your signal mask will be left in an undefined state (in particular, because you’re not resuming via sigreturn, the signal that you’re currently handling will be masked, which may cause very surprising behaviour and incorrect results if you rely on the signals for correctness). It’s not UB, it’s well specified, it’s just impossible to reason about in the general case.

          Note that your signal-lock approach works only if you have an exhaustive list of async signal unsafe functions. This means that it cannot work in a program that links libraries other than libc. Jumping out of signal handlers and doing stack unwinding also requires that you don’t use pthread_cleanup, GCC’s __attribute__((cleanup)), or C++ RAII in your program. I honestly can’t conceive of a situation where I’d consider that a good tradeoff.

          1. 0

            EDIT: I am dumb. I realized that under the ABI’s currently in use, treating caller save registers normally would actually be correct. So there’s no special code, and my bc will work correctly under all compilers. I’m leaving this comment up as a lesson to me and for posterity.

            I hope that you mean siglongjmp, not longjmp, because otherwise your signal mask will be left in an undefined state (in particular, because you’re not resuming via sigreturn, the signal that you’re currently handling will be masked, which may cause very surprising behaviour and incorrect results if you rely on the signals for correctness). It’s not UB, it’s well specified, it’s just impossible to reason about in the general case.

            Yes, I used siglongjmp(). I apologize if this was wrong, but in your original post, it sounded like you thought siglongjmp() should not be used either, so I lumped longjmp() and siglongjmp() together.

            It looks as if ucontext is finally gone from the latest version of POSIX, but the previous version said this:

            When a signal handler is executed, the current user context is saved and a new context is created. If the thread leaves the signal handler via longjmp(), then it is unspecified whether the context at the time of the corresponding setjmp() call is restored and thus whether future calls to getcontext() provide an accurate representation of the current context, since the context restored by longjmp() does not necessarily contain all the information that setcontext() requires. Signal handlers should use siglongjmp() or setcontext() instead.

            Explicitly: longjmp does not store the entire context, any temporary registers may not be stored.

            Yes, that looks like longjmp() does not have to restore the full context according to POSIX. (C99 might still require it, and C99 controls; more on that later.) However, it looks like siglongjmp() does. That paragraph mentions them separately, and it explicitly says that siglongjmp() should be used because of the restrictions on the use of longjmp(), which implies that siglongjmp() does not have those restrictions. And if siglongjmp() does not have those restrictions, then it should restore all of the register context.

            I agree that it does not have to set floating-point context or reset open files, etc., since those are explicitly called out. But it certainly seems to me like siglongjmp() is expected to restore them.

            No, this is why setjmp explicitly requires that all local mutable state that you access after the return is held in volatile variables: to force the compiler to explicitly save them to the stack (or elsewhere) and reload them.

            This sounds completely wrong, so I just went through POSIX and checked: those is no such restriction on the compiler, specifically the c99 utility, nor anywhere else it mentions compilers. I also checked the C99 standard, and it says the exact same thing as POSIX.

            I also searched the C99 standard to see if there were restrictions on handling of objects of automatic storage duration, and there were none.

            So there could be, and are, several C99 compilers that follow the standard, such as tcc, cproc, icc, chibicc, and others, and yet, if someone compiles my bc with one of those compilers on FreeBSD, my bc could fail to work properly.

            But even with Clang, does it either treat setjmp() or sigsetjmp() specially or refuse to promote local variables to caller-save registers? Can you point me to the code in Clang that does either one?

            Is a failure in my bc the fault of the compiler? I argue that the C standard does not have any restriction that puts the compiler at fault.

            Is a failure in my bc my fault? If I had used longjmp() instead of siglongjmp() in the signal handler, yes, it would be my fault. But I did not, and I followed all of the other restrictions on applications, including not relying on any floating-point state.

            Thus, I argue that since neither C99 nor POSIX has a restriction on compilers that require special handling of setjmp(), and since POSIX has no restrictions on applications that I did not follow, neither I nor compilers can be at fault.

            In addition, because POSIX says about setjmp():

            The functionality described on this reference page is aligned with the ISO C standard. Any conflict between the requirements described here and the ISO C standard is unintentional. This volume of POSIX.1-2017 defers to the ISO C standard.

            This means the C99 standard controls, and the C99 standard says:

            The environment of a call to the setjmp macro consists of information sufficient for a call to the longjmp function to return execution to the correct block and invocation of that block, were it called recursively.

            The line “information sufficient” is crucial; I argue that it is a restriction on the implementation, not the compiler or the application.

            Now, we can argue what “sufficient information” means, and I think it might be different to every platform (well, ABI), but if FreeBSD and Linux want to obey the POSIX standard, I think each of them needs to have a conversation about what sigsetjmp() and setjmp() should save. Looking into this for this discussion has made me realize just how little platforms have actually considered what those require.

            So yes, I argue that glibc and FreeBSD are wrong with respect to the POSIX standard. They might decide they are right for what they want.

            Now, why has my bc worked so well on those platforms? Well, the standard compilers might be helping, but it might also be because my bc has been lucky to this point so far. I don’t like that thought.

            So let’s have that conversation, especially on FreeBSD, especially 13.1 just hit, and my bc has begun to be used a lot more. I want my bc to work right, and I’m sure FreeBSD does too.

            Note that your signal-lock approach works only if you have an exhaustive list of async signal unsafe functions. This means that it cannot work in a program that links libraries other than libc.

            This is correct. I do want to have zero dependencies other than libc, so it wasn’t a problem.

            Jumping out of signal handlers and doing stack unwinding also requires that you don’t use pthread_cleanup, GCC’s __attribute__((cleanup)), or C++ RAII in your program. I honestly can’t conceive of a situation where I’d consider that a good tradeoff.

            Because of portability. If I had used any of those, my bc would not be nearly as portable as it is. Making it work on Windows (with the exception of command-line history) was simple. That would not have been the case if I had used any of those, except C++ RAII. But you already know my opinion about C++, so that wasn’t an option either.

            But if you are unsatisfied with that answer, then I have another: in the project where I was thinking about implementing a malloc(), I implemented a heap-based stack with the abiilty to call destructors. With a little clever use of macros, I can apply it to every function in my code, and it is 100% portable! And it also does stack traces!

            So I did give myself something like C++ RAII in C.

            EDIT: I just want to add that this heap-based stack also made it possible for me to implement conditions and restarts in C, to put things back on topic.

            1. 2

              No, this is why setjmp explicitly requires that all local mutable state that you access after the return is held in volatile variables: to force the compiler to explicitly save them to the stack (or elsewhere) and reload them.

              This sounds completely wrong, so I just went through POSIX and checked: those is no such restriction on the compiler, specifically the c99 utility, nor anywhere else it mentions compilers. I also checked the C99 standard, and it says the exact same thing as POSIX.

              It’s implicit from the definition of volatile. The compiler must not elide memory accesses that are present in the C abstract machine when accessing volatile variables. If you read a volatile int then the compiler must emit an int-sized load. It therefore follows that, if you read a volatile variable, call a function, modify the variable, and then read the variable again, the compiler must emit a load, a call, a store, and a load. The C spec has a lot of verbiage about this.

              But even with Clang, does it either treat setjmp() or sigsetjmp() specially or refuse to promote local variables to caller-save registers? Can you point me to the code in Clang that does either one?

              Clang does have a little bit of special handling for setjmp, but the same handling that it has for vfork and similar returns-twice functions. It doesn’t need anything else, because (from the bit of POSIX that I referred to, which mirrors similar text in ISO C) if you modify a non-volatile local in between the first return and the second and then try to access it the results are UB. Specifically, POSIX says:

              All accessible objects have values, and all other components of the abstract machine have state (for example, floating-point status flags and open files), as of the time longjmp() was called, except that the values of objects of automatic storage duration are unspecified if they meet all the following conditions:

              • They are local to the function containing the corresponding setjmp() invocation.

              • They do not have volatile-qualified type.

              • They are changed between the setjmp() invocation and longjmp() call.

              This is confusingly written with a negation of a universally quantified thing, rather than an existentially quantified thing, but applying De Morgan’s laws, we can translate it into a more human-readable form: It is undefined behaviour if you access any local variable that is volatile qualified and is modified between the setjmp and longjmp calls.

              As I said, this works because of other requirements. The compiler must assume (unless it can prove otherwise via escape / alias analysis. The fact that setjmp is either a compiler builtin or an assembly sequence blocks this analysis):

              • Any modification to a global or heap object is visible to code executing in a function call and so must store any changes to such objects back to memory.
              • Any function call may modify any global or heap object and so it must reload any values that it reads from such locations.
              • Loads and stores of volatile variables, including locals, must not be elided, and so any modification of a volatile local must preserve the write to the stack.
              • Local variables must be stored in a location that is not clobbered by a call. If a local variable (even a non-volatile one) is written before the setjmp call, then it must be stored either on the stack, or in a callee-save register, or it will be clobbered by the call.

              These are not special rules for setjmp, they fall out of the C abstract machine and are requirements on any ABI that implements that abstract machine.

              Note: I’m using call a shorthand here. You cannot implement setjmp in C because it needs to do some things that are permitted within the C abstract machine only by setjmp itself. The standard allows it to be a macro so that it can be implemented in inline assembly, rather than as a call to an assembly routine. In this case, the inline assembly will mark all of the registers that are not preserved as clobbered. Inline assembly is non-standard and it’s up to the C implementer to use whatever non-standard extensions they want (or external assembly routines) to implement things like this. On Itanium, setjmp was actually implemented on top of libunwind, with the jump buffer just storing a token that told the unwinder where to jump (this meant that it ran all cleanups on the way up the stack, which was a very nice side effect, and made longjmp safe in exception-safe C++ code as well).

              Oh, and there’s a really stupid reason why setjmp is often a macro: the standard defines it to take the argument by value. In C, if it really is a function, you need something like #define setjmp(env) real_setjmp(&env). GCC’s __builtin_setjmp actually takes a jmp_buf& as an argument and so C++ reference types end up leaking very slightly into C. Yay.

              Note that there probably are some compiler transforms that make even this a bit dubious. The compiler may not elide loads or stores of volatile values, nor reorder them with respect to other accesses to the same value, but it is free to reorder them with respect to other operations. _Atomic introduces restrictions with respect to other accesses, so you might actually need _Atomic(volatile int) for an int that’s guaranteed to have the expected value on return from a siglongjmp out of a signal handler. This does not contradict what the quoted section says: the value that it will have without the _Atomic specifier is still well-defined, it’s just defined to be one of a set of possible values in the state space defined by the C abstract machine (and, in some cases, that’s what you actually want).

              This is particularly true for jumping into the top stack frame. Consider:

              jmp_buf env;
              volatile int x = 0;
              volatile int y = 0;
              volatile int z = 1;
              if (sigsetjmp(env) == 0)
              {
                 int y;
                 x = 1;
                 y = z / y; // SIGFPE delivered here, signal handler calls `siglongjmp`.
              }
              else
              {
                 printf("%d\n", x);
              }
              

              This is permitted to print 0. The compiler is free to reorder the code in the block to:

              z_0 = load z;
              y_0 = load y;
              y_1 = z_0 / y_0; // SIGFPE happens here
              store y y_1
              store x 1
              

              If you used _Atomic(volatile int) instead of volatile int for the three locals then (I think) this would not be permitted because each of these would be a sequentially-consistent operation and so store to x may not be reordered with respect to the loads of z and y. I believe you can also fix it by making each of the locals in a single volatile struct, because then the rule that prevents reordering memory accesses to the same volatile object would kick in.

              Note that this example will, I believe, do what you expect when compiled with clang at the moment because LLVM is fairly conservative with respect to reordering volatile accesses. There are some FIXMEs in the code about this because the conservatism largely dates from a pre-C[++]11 world when _Atomic didn’t exist and so people had to fudge it with volatile and pray.

              In spite of the fact that it is, technically, possible to write correct code that jumps out of a signal handler, my opinion stands. The three hardest parts of the C specification to understand are [sig]setjmp, signals, and volatile. Anything that requires a programmer to understand and use all of these to implement correctly is going to be unmaintainable code. Even if you are smart enough to get it right, the next person to try to modify the code might be someone like me, and I’m definitely not.

          2. 3

            longjmp() and siglongjmp() cannot be used from a signal handler that interrupted a non-async-signal-safe function.

            Okay sure the text says that.

            But that requirement, of knowing what function you have interrupted, is ludicrously hard to arrange except when the signal handler is installed in the same function it is triggered from. Certainly entirely unsuitable for a generic mechanism that might wrap arbitrary user code.

        2. 6

          As a negative result, I believe Rust used to have such a system, but lost it.

            1. 9

              See also the issue where they were removed – that issue & the see also links show why it didn’t work for Rust:

              https://github.com/rust-lang/rust/pull/12039

          1. 3

            They are sometimes mentioned in newer research languages with algebraic effects. So no mainstream languages yet, but at least some people outside of the Lisp/Dylan world are thinking about them!

          2. 5

            I highly recommend the book “The Common Lisp Condition System: Beyond Exception Handling with Control Flow Mechanisms”.

            1. 3

              Interestingly, Limbo (perhaps the most direct predecessor to Go) has something similar.

              Code like this (note: this is not actual Limbo):

              let e = rescue();
              if (e.hasException()) {
                 println("on exception handler");
                 e.rescued(ACTIVE);
              }
              println("after exception handler");
              if (!e.hasException()) {
                throw Exception();
              }
              

              Would print:

              after exception handler
              on exception handler
              after exception handler
              

              You can also rethrow the exception, or suspend the execution of the thread to debug it (and maybe reactivate it? I don’t know).

              1. 3

                One thing I’m not clear on: is it mandatory that all paths out of the handler must end in a throw? It looks like it ought to be.

                1. 6

                  In CL, if a handler does not throw, then the next outer handler is tried. http://clhs.lisp.se/Body/09_a.htm

                  1. 1

                    Ah thanks! So in CL there’s always a top level handler. That page says the default one in CL happens to invoke the debugger.

                    I expect that an implementation that stuck to modern sensibilities would probably throw an exception from the top level handler.

                2. 3

                  IIRC, Bjarne Stroustrup was talking about this in the early 1990s in Boston. He and some others had been given access to some large (for the time) C++ source projects, in the many MLOC. C++ had briefly had a ‘retry’ keyword. It had been used exactly six times in the entire codebase. It was the source of exactly six bugs in the codebase.

                  Retry was dropped from the language.

                  1. 1

                    IIRC Ruby has such a mechanism? (Or am I remembering continuations, which I believe can be used to implement this?)

                    I have mixed feelings about this functionality — on the one hand, I see how powerful it is. On the other hand, it seems to make the flow of control even more tangled than regular exceptions, which are already criticized (with some validity) for creating confusion. I suspect condition systems can lead to some really WTF situations that would be super confusing to debug…

                    1. 2

                      Thankfully common lisp has excellent debugging facilities that make it easy to see what is going on.

                      1. 2

                        IIRC Ruby has such a mechanism? (Or am I remembering continuations, which I believe can be used to implement this?)

                        Ruby used to have continuations but they were severely broken, so they got taken out. I read that they added back continuations later on, but they’re now considered deprecated again.

                        AFAIK you don’t really need continuations to implement this - all you need is a way to “unwind the stack” a la throw if you do want to jump back (which continuations can do, but the original throw would also suffice). The difference here is that the condition-enabled throw would first call a function which may return with a new value to use instead of the value that caused the exception.

                        1. 1

                          I think you’re right. I can imagine doing this in C by having the TRY push a HANDLE callback on a thread-local stack, and then THROW calls this handler, which can either return a result or do the usual longjmp back to a CATCH block.

                      2. 1

                        How does this compare to algebraic effects? I’m not super familiar with each but what I grasp is that they are a generalized version of try/catch & with the ability to resume after the throw.

                        Is that correct & is this the same?

                        1. 2

                          It’s pretty similar. The main difference is that an algebraic effect handler also receives a first-class continuation, so you can do stuff like nondeterminism. (Note however that continuations are not necessary for resumable exceptions - as demonstrated by Common Lisp which doesn’t have continuations.)

                          1. 1

                            Algebraic effects are basically typed delimted continuations. There is a blog post on the internet somewhere about what delimited continuations are in terms of conditions but to be honest the implementation is somewhat leaky (as an abstraction). If you’d like to be “perfect” in your choice of primitives then I think I’d rather have delim. cont. than conditions.

                            1. 1

                              Continuations and conditions are orthogonal. A first-class continuation represents a point in time of a running program. Conditions are used to signal situations from a nested to an outer part of a program. Both are useful, but not directly related. A given language could have either, or both of those two features.

                              1. 2

                                Sure but what I mean is that you can implement one with the other and as a starting point I’d rather have the delim. conts.

                                1. 2

                                  You don’t need continuations to implement conditions (as demonstrated by CL). You cannot implement continuations with conditions (except by writing an interpreter).

                                  1. 1

                                    Yes actually you are more right than me. Sorry for confusing the discussion. Here is that link I mentioned earlier it also has nothing to do with conditions: https://8c6794b6.github.io/posts/Delimited-continuations-with-monadic-functions-in-Common-Lisp.html

                                    I was basically just reciting corrupted memory, after you prompted me to think the tune changed >.<

                                    The thing about delimited continuations that gives me tunnel vision (and that you probably know but now I am getting corrected so it’s good to keep talking and see if I can spot some other nonsense) is that I’d put them as one of the primitives in the “final programming language” - but conditions, while pretty nice to use, aren’t something I’d consider as fundamental.

                                    1. 2

                                      DCs are of course awesome for all kinds of control flow manipulation, but like every language feature, they also have a price. I think you can have a perfectly fine language without continuations, especially if you have some other mechanism for concurrency, such as threads.

                                      Here’s some food for thought: “A language implementation may need to support several control primitives. We have to think what they are and how to reason about their interactions. I feel the design space is not yet well-explored. We need more experience implementing, using and reasoning with various control operators and frameworks.”

                                      https://okmij.org/ftp/continuations/against-callcc.html

                                      1. 1

                                        Yep call/cc is only interesting from an extensional equality perspective (i.e. math) but DCs can be implemented efficiently and okmij also wrote this:

                                        First-class delimited continuations can express any expressible computational effect, including exceptions and mutable state. To be precise, any effect that can be emulated by transforming the whole program (into the state-passing–style, into an Either-returning style, etc.) can be expressed with first-class delimited continuations without the global transformation, see Filinski’s ``Representing Monads.’’

                                        From: https://okmij.org/ftp/continuations/undelimited.html#delim-vs-undelim

                                        I had some enlightenment some months ago where I saw so clearly the “perfect set of primitives” but now I am afraid to try and name the concepts correctly .. but an attempt still follows: with DCs for control flow you just need syntactic closures and lambdas to span the space of expressible computations (lambdas for control is DC, lambdas for syntax and then normal lambdas) but maybe I was having a fever dream :)

                                        1. 2

                                          Re performance - if you implement e.g. state with DCs you will of course always have huge overheads compared to a normal implementation of state.

                                          But even for more control-flowy things like nonlocal exits, which are not as far fetched as state, implementing them in terms of DCs may be wasteful, depending on your implementation: if you just exit, there is no point in capturing a continuation (that’s why e.g. Racket provides a separate ‘abort’ operation that does the same thing as ‘shift’ but doesn’t capture).

                                          Also, many use cases of continuations are coroutine-like, and don’t need the ability to re-enter the same continuation multiple times. So coroutines can also be implemented more efficiently natively than in terms of DCs.

                                          Re syntactic closures: check out the Kernel programming language and vau calculus: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.13.3231

                                          1. 1

                                            Thank you for taking the time to name these examples, very informative!

                                            I have checked out kernel before and it is actually a large inspiration for my datalisp thing.

                                            I believe that we can essentially add infinite precompilation phases / metaprogramming before running / compiling whichever program in whichever language and then just call it ‘the internet’.