suspensions.txt 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. Notes on Suspensions
  2. --------------------
  3. v1: Meredydd Luff, 23/Sep/2014
  4. Suspensions are continuations, generated on demand by returning an instance
  5. of Sk.misceval.Suspension from a function. They allow Python execution to be
  6. suspended and subsequently resumed, allowing simulation of blocking I/O,
  7. time-slicing to keep web pages responsive, and even multi-threading.
  8. Normally, a suspension is initiated by a callee (Javascript) function,
  9. but there is also a 'debugging' option (passed to Sk.configure()) which
  10. causes the compiler to generate a suspension before each statement.
  11. As each suspension also captures all stack frames and local variables,
  12. this can be used to implement a single-step debugger. An optional
  13. breakpoints() callback (also passed to Sk.configure()) allows the user
  14. to dynamically filter which of these suspensions actually happen.
  15. Suspensions have a 'data' property, which is an object indicating the reason
  16. for the suspension (and under what circumstances it may be resumed).
  17. 'data.type' is a string. The following types are defined by Skulpt:
  18. 'Sk.promise': Resume when the Javascript Promise 'data.promise' resolves
  19. or yields an error.
  20. 'Sk.debug': A suspension caused by the 'debugging' option (see above)
  21. Suspensions also have an 'optional' property. If set to true, this suspension
  22. may be resumed immediately if it is not convenient to wait. 'Sk.debug'
  23. suspensions have 'optional' set, so they are ignored rather than causing
  24. errors in non-suspendable call stacks (see below).
  25. Example: --------------------------------------------------------------------
  26. Skulpt provides utility functions for calling Python code that might suspend,
  27. and returning its result as a Javascript Promise. (For browsers that do not
  28. support Promises natively, Skulpt embeds the "es6-promises" poly-fill.)
  29. Here is some Javascript code that calls a Python function that might suspend,
  30. then logs its return value to the console:
  31. Sk.misceval.callsimAsync(null, pyFunction).then(function success(r) {
  32. console.log("Function returned: " + r.v);
  33. }, function failure(e) {
  34. console.log("Function threw an exception: " + e);
  35. });
  36. You can also pass an object map of custom suspension handlers, which are
  37. called if a specific type of suspension occurs. Suspension handlers return
  38. a promise which is resolved with the return value of susp.resume(), or
  39. rejected with an exception. For example:
  40. var handlers = {};
  41. handlers["Sk.debug"] = function(susp) {
  42. try {
  43. console.log("Suspended! Now resuming...");
  44. // Return an already-resolved promise in this case
  45. return Promise.resolve(susp.resume());
  46. } catch(e) {
  47. return Promise.reject(e);
  48. }
  49. };
  50. Sk.misceval.callsimAsync(handlers, pyFunction).then(...)
  51. Alternatively, you can use functions that return Suspensions directly.
  52. Sk.importMain() is one such example. If you pass 'true' as its third
  53. argument, it will return a Suspension if its code suspends. (If you don't
  54. give it a third argument, it will throw an error if the code tries to
  55. suspend. This is for backward compatibility.)
  56. However, doing this manually is awkward, so Skulpt provides a utility
  57. function:
  58. var p = Sk.misceval.asyncToPromise(function() {
  59. return Sk.importMain("%s", true, true);
  60. });
  61. p.then(function (module) {
  62. console.log("Script completed");
  63. }, function (err) {
  64. console.log("Script aborted with error: " + err);
  65. });
  66. Completeness notes: ---------------------------------------------------------
  67. There are many places that don't currently support suspension that should.
  68. These include:
  69. * Imports. Both the fetching of the imported module source and the running
  70. of that code should be able to suspend. However, Sk.import*() is a maze of
  71. twisty code paths and loops that make continuation transformation
  72. non-trivial.
  73. Implementation notes: -------------------------------------------------------
  74. * Not every Javascript calling context can handle getting a suspension
  75. instead of a return value. There are many awkward cases within the Skulpt
  76. codebase, let alone existing users of the library. Therefore, uses of
  77. Sk.misceval.callsim()/call()/apply() do not support suspensions, and will
  78. throw a SuspensionError if their callee tries to suspend non-optionally.
  79. (Likewise, suspending part-way through a class declaration will produce
  80. an error.)
  81. Other APIs which call into Python, such as import and Sk.abstr.iternext(),
  82. now have an extra parameter, 'canSuspend'. If false or undefined, they
  83. will throw a SuspensionError if their callee suspends non-optionally. If
  84. true, they may return a Suspension instead of a result.
  85. If a Suspension with the 'optional' flag is returned in a non-suspendable
  86. context, its resume() method will be called immediately rather than
  87. causing an error.
  88. * Suspensions save the call stack as they are processed. This provides a
  89. Python stack trace of every suspension. This could be used to provide
  90. stack traces on exceptions, which is currently a missing feature.
  91. * Likewise, suspensions would be a natural way of implementing generators.
  92. The current generator implementation is quite limited (it does not support
  93. varargs or keyword args) and not quite correct (it does not preserve
  94. temporaries between calls - see below), so would benefit from unification.
  95. * Suspensions would also be a good way of implementing timeouts, as well as
  96. keeping the browser responsive during long computations. I have not
  97. changed the existing timeout code, which still throws errors. A
  98. suspension-based timeout should first return optional suspensions (in case
  99. the timeout triggers on a non-suspendable stack), and then, after a grace
  100. period, issue a non-optional suspension that will terminate a
  101. non-suspendable stack.
  102. Reliability and testing notes: ----------------------------------------------
  103. * Deliberate suspensions are tested by the (newly implemented) time.sleep()
  104. function, which is exercised by the tests t544.py and t555.py.
  105. * We test that a wide variety of generated code is robust to being suspended
  106. by running the entire test suite in 'debugging' mode (see above). This
  107. causes suspensions and resumptions at every statement boundary, giving us
  108. good confidence that any feature exercised by the test suite is robust to
  109. suspension.
  110. Of course, the test suite must also be run in normal mode, to verify that
  111. it works when *not* suspending at every statement boundary.
  112. Performance notes:
  113. * Essential overhead in the fast case (ie code that does not suspend) is
  114. kept quite low, at two conditionals per function call (one by the caller,
  115. to check whether a call completed or suspended, and one by the callee,
  116. to check whether this is a normal call or a suspension being resumed).
  117. Given the number of conditionals and nested function calls in
  118. Sk.misceval.call/callsim/apply, this is probably negligible.
  119. * There is additional implementation-dependent overhead (ie overhead that
  120. can be whittled down if it proves too much, and would not require global
  121. changes to implementations strategy to mitigate). I have not attacked
  122. these too aggressively, as the indications are that the performance hit
  123. already isn't that bad (~5% on the test suite on my machine, but of course
  124. we'd need bigger benchmarks to say for sure). Still, here are some
  125. pointers for future improvement:
  126. - Sk.misceval.call/apply and friends are now wrappers around
  127. applyOrSuspend, with an additional check for suspensions (to throw an
  128. error)
  129. - Each function call creates a new block for "after this function
  130. returns" (this is where we resume to if that function call suspends),
  131. and jumps to it via the normal continue-based mechanism. With more
  132. invasive modification to the block generator, we could use switch-case-
  133. fallthrough to remove this penalty for ordinary function returns.
  134. - Any temporary that might be required to persist across *any* suspension
  135. in a scope (ie any function call) is saved on *every* suspension. This
  136. is conservative but correct (unlike existing generator support, which
  137. just breaks if you try something like x = str((yield y)), as the
  138. temporary used to look up 'str' is not preserved). However, this does
  139. impede the compiler's ability to infer variable lifetimes. This
  140. could be mitigated by generating separate save and resume code for
  141. each suspension site, but that again requires intrusive modification
  142. to the block system.