Tame #1942

Closed
wants to merge 247 commits into
from
Select commit
Jump to file
+14,190 −647
Split
View
1 .gitignore
@@ -7,3 +7,4 @@ test/*.js
examples/beautiful_code/parse.coffee
*.gem
/node_modules
+*~
View
22 Cakefile
@@ -29,7 +29,8 @@ header = """
sources = [
'coffee-script', 'grammar', 'helpers'
- 'lexer', 'nodes', 'rewriter', 'scope'
+ 'lexer', 'nodes', 'rewriter', 'scope',
+ 'tame', 'tamelib', 'icedlib'
].map (filename) -> "src/#{filename}.coffee"
# Run a CoffeeScript through our node/coffee interpreter.
@@ -96,7 +97,7 @@ task 'build:ultraviolet', 'build and install the Ultraviolet syntax highlighter'
task 'build:browser', 'rebuild the merged script for inclusion in the browser', ->
code = ''
- for name in ['helpers', 'rewriter', 'lexer', 'parser', 'scope', 'nodes', 'coffee-script', 'browser']
+ for name in ['helpers', 'rewriter', 'lexer', 'parser', 'scope', 'tame', 'nodes', 'coffee-script', 'browser', 'icedlib' ]
code += """
require['./#{name}'] = new function() {
var exports = this;
@@ -167,6 +168,7 @@ runTests = (CoffeeScript) ->
startTime = Date.now()
currentFile = null
passedTests = 0
+ attemptedTests = 0
failures = []
global[name] = func for name, func of require 'assert'
@@ -177,6 +179,7 @@ runTests = (CoffeeScript) ->
# Our test helper function for delimiting different test cases.
global.test = (description, fn) ->
try
+ ++attemptedTests
fn.test = {description, currentFile}
fn.call(fn)
++passedTests
@@ -185,6 +188,18 @@ runTests = (CoffeeScript) ->
e.source = fn.toString() if fn.toString?
failures.push filename: currentFile, error: e
+ # An async testing primitive
+ global.atest = (description, fn) ->
+ ++attemptedTests
+ fn.test = { description, currentFile }
+ fn.call fn, (ok, e) =>
+ if ok
+ ++passedTests
+ else
+ e.description = description if description?
+ e.source = fn.toString() if fn.toString?
+ failures.push filename : currentFile, error : e
+
# See http://wiki.ecmascript.org/doku.php?id=harmony:egal
egal = (a, b) ->
if a is b
@@ -208,6 +223,7 @@ runTests = (CoffeeScript) ->
process.on 'exit', ->
time = ((Date.now() - startTime) / 1000).toFixed(2)
message = "passed #{passedTests} tests in #{time} seconds#{reset}"
+ log("Only #{passedTests} of #{attemptedTests} came back; some went missing!", red) unless passedTests == attemptedTests
return log(message, green) unless failures.length
log "failed #{failures.length} and #{message}", red
for fail in failures
@@ -244,4 +260,6 @@ task 'test:browser', 'run the test suite against the merged browser script', ->
result = {}
global.testingBrowser = yes
(-> eval source).call result
+ global.tame = result.CoffeeScript.tame
runTests result.CoffeeScript
+
View
12 NAMES
@@ -0,0 +1,12 @@
+TeeScript
+BeeScript
+TacoScript
+ManuScript
+CupScript
+MultiScript
+ToffeeScript
+PipeScript
+AdvancedCoffeeScript (ACS)
+AdvancedCoffeeSubstitute (ACS)
+IcedCoffeeScript (ICS)
+IcedScript (.iced)
View
630 TAME.md
@@ -0,0 +1,630 @@
+What Is IcedCoffeeScript?
+==================
+
+IcedCoffeeScript is a system for handling callbacks in event-based code. There
+were two existing implementations, one in [the sfslite library for
+C++](https://github.com/maxtaco/sfslite), and another in the [tamejs
+translator for JavaScript](https://github.com/maxtaco/tamejs).
+This extension to CoffeeScript is a third implementation. The code
+and translation techniques are derived from experience with JS, but
+with some new Coffee-style flavoring.
+
+This document first presents a "Iced" tutorial (adapted from the JavaScript
+version), and then discusses the specifics of the CoffeeScript implementation.
+
+(Note: we are slowly changing all instances of the word "tame" over to
+"iced" in the repository, and in the general code.)
+
+# Quick Tutorial and Examples
+
+Here is a simple example that prints "hello" 10 times, with 100ms
+delay slots in between:
+
+```coffeescript
+# A basic serial loop
+for i in [0..10]
+ await setTimeout(defer(), 100)
+ console.log "hello"
+```
+
+There is one new language addition here, the `await ... ` block (or
+expression), and also one new primitive function, `defer`. The two of
+them work in concert. A function must "wait" at the close of a
+`await` block until all `defer`rals made in that `await` block are
+fulfilled. The function `defer` returns a callback, and a callee in
+an `await` block can fulfill a deferral by simply calling the callback
+it was given. In the code above, there is only one deferral produced
+in each iteration of the loop, so after it's fulfilled by `setTimer`
+in 100ms, control continues past the `await` block, onto the log line,
+and back to the next iteration of the loop. The code looks and feels
+like threaded code, but is still in the asynchronous idiom (if you
+look at the rewritten code output by the *coffee* compiler).
+
+This next example does the same, while showcasing power of the
+`await..` language addition. In the example below, the two timers
+are fired in parallel, and only when both have fulfilled their deferrals
+(after 100ms), does progress continue...
+
+```coffeescript
+for i in [0..10]
+ await
+ setTimeout defer(), 100
+ setTimeout defer(), 10
+ console.log ("hello");
+```
+
+Now for something more useful. Here is a parallel DNS resolver that
+will exit as soon as the last of your resolutions completes:
+
+```coffeescript
+dns = require("dns");
+
+do_one = (cb, host) ->
+ await dns.resolve host, "A", defer(err, ip)
+ msg = if err then "ERROR! #{err}" else "#{host} -> #{ip}"
+ console.log msg
+ cb()
@TrevorBurnham
TrevorBurnham Dec 17, 2011

I'm pretty sure func() is more widely used than do func, including within the compiler... see e.g. http://coffeescript.org/documentation/docs/lexer.html.

@michaelficarra
michaelficarra Dec 17, 2011

do is still a new syntax for CS, so it's reasonable that it wouldn't be all over the compiler yet. do should be used when you're not directly using the value of a function invocation, like when using a function for its side effects. @jashkenas: do you agree? Though this one's kind of different because it's implicitly part of a return, so either way would be acceptable in my eyes.

@jashkenas
jashkenas Dec 17, 2011

No. I still have faint hopes of being able to axe do eventually... At the moment, it exists as a necessary evil to avoid the ((arg)->)(arg) pattern, and the ability for it to invoke an arbitrary expression is sort of a consequence of consistency, but I don't think it would be considered good style. This isn't a strong proscription -- just a note for patches against the compiler.

@geraldalewis
geraldalewis Dec 17, 2011

do cb

I've been using do exactly as @michaelficarra describes ('not directly using the value of a function invocation'), especially when an invocation appears at the end of a method, and it really clears up my code in my eyes.

+
+do_all = (lst) ->
+ await
+ for h in lst
+ do_one defer(), h
+
+do_all process.argv[2...]
+```
+
+You can run this on the command line like so:
+
+ iced examples/tame/dns.coffee yahoo.com google.com nytimes.com okcupid.com tinyurl.com
+
+And you will get a response:
+
+ yahoo.com -> 72.30.2.43,98.137.149.56,209.191.122.70,67.195.160.76,69.147.125.65
+ google.com -> 74.125.93.105,74.125.93.99,74.125.93.104,74.125.93.147,74.125.93.106,74.125.93.103
+ nytimes.com -> 199.239.136.200
+ okcupid.com -> 66.59.66.6
+ tinyurl.com -> 195.66.135.140,195.66.135.139
+
+If you want to run these DNS resolutions in serial (rather than
+parallel), then the change from above is trivial: just switch the
+order of the `await` and `for` statements above:
+
+```coffeescript
+do_all = (lst) ->
+ for h in lst
+ await
+ do_one defer(), h
+```
+
+### Slightly More Advanced Example
+
+We've shown parallel and serial work flows, what about something in
+between? For instance, we might want to make progress in parallel on
+our DNS lookups, but not smash the server all at once. A compromise is
+windowing, which can be achieved in IcedCoffeeScript conveniently in a
+number of different ways. The [2007 academic paper on
+tame](http://pdos.csail.mit.edu/~max/docs/tame.pdf) suggests a
+technique called a *rendezvous*. A rendezvous is implemented in
+CoffeeScript as a pure CS construct (no rewriting involved), which
+allows a program to continue as soon as the first deferral is
+fulfilled (rather than the last):
+
+```coffeescript
+tameRequire(node) # need full library via require() for rendezvous
+
+do_all = (lst, windowsz) ->
+ rv = new tame.Rendezvous
+ nsent = 0
+ nrecv = 0
+
+ while nrecv < lst.length
+ if nsent - nrecv < windowsz and nsent < n
+ do_one rv.id(nsent).defer(), lst[nsent]
+ nsent++
+ else
+ await rv.wait defer evid
+ console.log "got back lookup nsent=#{evid}"
+ nrecv++
+```
+
+This code maintains two counters: the number of requests sent, and the
+number received. It keeps looping until the last lookup is received.
+Inside the loop, if there is room in the window and there are more to
+send, then send; otherwise, wait and harvest. `Rendezvous.defer`
+makes a deferral much like the `defer` primitive, but it can be
+labeled with an identifier. This way, the waiter can know which
+deferral has fulfilled. In this case we use the variable `nsent` as the
+defer ID --- it's the ID of this deferral in launch order. When we
+harvest the deferral, `rv.wait` fires its callback with the ID of the
+deferral that's harvested.
+
+Note that with windowing, the arrival order might not be the same as
+the issue order. In this example, a slower DNS lookup might arrive
+after faster ones, even if issued before them.
+
+### Composing Serial And Parallel Patterns
+
+In Tame, arbitrary composition of serial and parallel control flows is
+possible with just normal functional decomposition. Therefore, we
+don't allow direct `await` nesting. With inline anonymous CoffeeScript
+functions, you can concisely achieve interesting patterns. The code
+below launches 10 parallel computations, each of which must complete
+two serial actions before finishing:
+
+```coffeescript
+f = (n,cb) ->
+ await
+ for i in [0..n]
+ ((cb) ->
+ await setTimeout defer(), 5 * Math.random()
+ await setTimeout defer(), 4 * Math.random()
+ cb()
+ )(defer())
+ cb()
+```
+
+### autocb
+
+Most of the time, an iced function will call its callback and return
+at the same time. To get this behavior "for free", you can simply
+name this callback `autocb` and it will fire whenever your iced function
+returns. For instance, the above example could be equivalently written as:
+
+```coffeescript
+f = (n,autocb) ->
+ await
+ for i in [0..n]
+ ((autocb) ->
+ setTimeout defer(), 5 * Math.random()
+ setTimeout defer(), 4 * Math.random()
+ )(defer())
+```
+In the first example, recall, you call `cb()` explicitly. In this
+example, because the callback is named `autocb`, it's fired
+automatically when the iced function returns.
+
+If your callback needs to fulfill with a value, then you can pass
+that value via `return`. Consider the following function, that waits
+for a random number of seconds between 0 and 4. After waiting, it
+then fulfills its callback `cb` with the amount of time it waited:
+
+```coffeescript
+rand_wait = (cb) ->
+ time = Math.floor Math.random() * 5
+ if time is 0
+ cb(0)
+ return
+ await setTimeout defer(), time
+ cb(time) # return here, implicitly.....
+```
+
+This function can written equivalently with `autocb` as:
+
+```coffeescript
+rand_wait = (autocb) ->
+ time = Math.floor Math.random() * 5
+ return 0 if time is 0
+ await setTimeout defer(), time
+ return time
+```
+
+Implicitly, `return 0;` is mapped by the CoffeeScript compiler to `autocb(0); return`.
+
+## Language Design Considerations
+
+In sum, the iced additions to CoffeeScript consist of three new keywords:
+
+* **await**, marking off a block or a single statement.
+* **defer**, which is quite similar to a normal function call, but is compiled specially
+to accommodate argument passing.
+* **tameRequire**, which is used to control the "require"ing of the
+iced runtime. By default, the runtime is pasted inline, but with
+`tameRequire(node)`, it is loaded via node's `require`, and with
+`tameRequire(none)`, it is skipped altogether.
+
+Finally, `autocb` isn't a bona-fide keyword, but the compiler searches
+for it in parameters to CoffeeScript functions, and updates the
+behavior of the `Code` block accordingly.
+
+These keywords represent the potential for these iced additions to
+break existing CoffeeScript code --- any preexisting use of these
+keywords as regular function, variable or class names will cause
+headaches.
+
+### The Lowdown on defer
+
+The implementation of `defer` is interesting --- it's trying to
+emulate ``call by reference'' in languages like C++ or Java. Here is an
+example that shows off the four different cases required to make this
+happen:
+
+```coffeescript
+cb = defer x, obj.field, arr[i], rest...
+```
+
+And here is the output from the iced `coffee` compiler:
+
+```javascript
+cb = __tame_deferrals.defer({
+ assign_fn: (function(__slot_1, __slot_2, __slot_3) {
+ return function() {
+ x = arguments[0];
+ __slot_1.field = arguments[1];
+ __slot_2[__slot_3] = arguments[2];
+ return rest = __slice.call(arguments, 3);
+ };
+ })(obj, arr, i)
+ });
+```
+
+The `__tame_deferrals` object is an internal object of type `Deferrals`
+that's collecting all calls to `defer` in the current `await` block.
+The one in question should fulfill with 3 or more values. When it does,
+it will call into the innermost anonymous function to perform the
+appropriate assignments in the original scope. The four cases are:
+
+1. **Simple assignment** --- seen in `x = arguments[0]`. Here, the
+`x` variable is in the scope of the original `defer` call.
+
+1. **Object slot assignment** --- seen in `__slot_1.field = arguments[1]`.
+Here, the reference `obj` must be captured at the time of the `defer` call,
+and `obj.field` is filled in later.
+
+1. **Array cell assignment** --- seen in `__slot_2[__slot_3] = arguments[2]`.
+This of course will work on an array or an object. Here, the reference
+to the array, and the value of the index must be captured when `defer`
+is called, and the cell is assigned later.
+
+1. **Splat assignment** --- seen in `res = __slice.call(arguments,3)`.
+This is much like a simple assignment, but allows a ``splat'' meaning
+assignment of multiple values at once, accessed as an array.
+
+These specifics are also detailed in the code in the `Defer` class,
+file `nodes.coffee`.
+
+### Awaits Can work as Expressions
+
+I don't really like this feature, but people have requested it, so
+here goes a trip down the rabbit hole. It's possible to use `await`
+blocks as expressions. And recursively speaking, it's possible to use
+statements that contain `await` blocks as expressions.
+
+The simple rule is that an `await` block takes on the value of the
+`defer` slot named `_` after its been fulfilled. If there
+are multiple `defer` slots named `_` within the `await` block, then
+the last writer wins. In practice, there's usually only one. Thus:
+
+```coffeescript
+add = (a,b,cb) ->
+ await setTimeout defer(), 10
+ cb(a+b)
+
+x = (await add 3, 4, defer _) + (await add 1, 2, defer _)
+console.log "#{x} == 10"
+```
+
+Of course, things can get arbitrarily compicated and nested, so it
+gets hairy. Consider this:
+
+```coffeescript
+x = await add (await add 1, 2, defer _), (await add 3, 4, defer _), defer _
+```
+
+The rule is that all nested `await` blocks (barf!) are evaluated
+sequentially in DFS order. You will get `10` in the above example
+after three sequential calls to `add`.
+
+I really don't like this feature for two reasons: (1) it's tricky to
+get the implementation right, and I'm sure I haven't tested all of the
+corner cases yet; (2) it's difficult to read and understand what
+happens in which order. I would suggest you save yourself the heartache,
+and just write the above as this:
+
+```coffeescript
+await add 1, 2, defer l
+await add 3, 4, defer r
+await add l, r, defer x
+```
+
+It's just so much clearer what happens in which order, and it's easier
+to parallelize or serialize as you see fit.
+
+## Translation Technique
+
+The IcedCoffeeScript addition uses a similar continuation-passing translation
+to *tamejs*, but it's been refined to generate cleaner code, and to translate
+only when necessary. Here are the general steps involved:
+
+* **1** Run the standard CoffeeScript lexer, rewriter, and parser, with a
+few small additions (for `await` and `defer`), yielding
+a standard CoffeeScript-style abstract syntax tree (AST).
+
+* **2** Apply *iced annotations*:
+
+ * **2.1** Find all `await` nodes in the AST. Mark these nodes and their
+ ancestors with an **A** flag.
+
+ * **2.2** Find all `for`, `while`, `until`, or `loop` nodes marked with
+ **A**. Mark them and their descendants with an **L** flag.
+
+ * **2.3** Find all `continue` or `break` nodes marked with an **L** flag.
+ Mark them and their descendants with a **P** flag.
+
+* **3** ``Rotate'' all those nodes marked with **A** or **P**:
+
+ * **3.1** For each `Block` node _b_ in the `AST` marked **A** or **P**:
+
+ * **3.1.1** Find _b_'s first child _c_ marked with **A** or **P**.
+
+ * **3.1.2** Cut _b_'s list of expressions after _c_, and move those
+ expressions on the right of the cut into a new block, called
+ _d_. This block is _c_'s continuation block and becomes _c_'s
+ child in the AST. This is the actual ``rotation.''
+
+ * **3.1.3** Call the rotation recursively on the child block _d_.
+
+ * **3.1.4** Add an additional code to _c_'s body, which is to call the
+ continuation represented by _d_. For `if` statements this means
+ calling the continuation in both branches; for `switch`
+ statements, this means calling the continuation from all
+ branches; for loops, this means calling `continue` at the end of
+ the loop body; for blocks, this means just calling the
+ continuation as the last statement in the block. See
+ `callContinuation` in `nodes.coffee.`
+
+* **4** Output preamble/boilerplate; for the case of JavaScript output to
+browsers, inline the small class `Deferrals` needed during runtime;
+for node-based server-side JavaScript, a `require` statement suffices
+here. Only do this if the source file has a `defer` statement
+in it.
+
+* **5** Compile as normal. The effect of the above is to mutate the original
+CoffeeScript AST into another valid CoffeeScript AST. This AST is then
+compiled with the normal rules.
+
+
+Translation Example
+------------------
+
+For an example translation, consider the following block of code:
+
+```coffeescript
+
+while x1
+ f1()
+
+while x2
+ if y
+ f2()
+ continue
+ f3()
+ await
+ f4(defer())
+ if z
+ f5()
+ break
+ f6()
+
+while x3
+ f7()
+```
+
+* Here is schematic diagram for this AST:
+
+ <img src="/maxtaco/coffee-script/raw/tame/media/rotate1.png" width=650 />
+
+* After Step 2.1, nodes in blue are marked with **A**. Recall, Step 2.1 traces
+upwards from all `await` blocks.
+
+ <img src="/maxtaco/coffee-script/raw/tame/media/rotate2.png" width=650 />
+
+* After Step 2.2, nodes in purple are marked with **L**. Recall, Step 2.2 floods
+downwards from any any loops marked with **A**.
+
+ <img src="/maxtaco/coffee-script/raw/tame/media/rotate3.png" width=650 />
+
+* After Step 2.3, nodes in yellow are marked with **P**. Recall, Step 2.3
+traces upwards from any jumps marked with **L**.
+
+ <img src="/maxtaco/coffee-script/raw/tame/media/rotate4.png" width=650 />
+
+* The green nodes are those marked with **A** or **P**. They are "marked"
+for rotations in the next step.
+
+ <img src="/maxtaco/coffee-script/raw/tame/media/rotate5.png" width=650 />
+
+* In Step 3, rotate all marked nodes AST nodes. This rotation
+introduces the new orange `block` nodes in the graph, and attaches
+them to pivot nodes as _continuation_ blocks.
+
+ <img src="/maxtaco/coffee-script/raw/tame/media/post-rotate.png" width=650 />
+
+
+* In translated code, the general format of a _pivot_ node is:
+
+```javascript
+(function (k) {
+ // the body
+ k();
+})(function () {
+ // the continuation block.
+}
+```
+
+To see how pivots and continuations are output in our example, look
+at this portion of the AST, introduced after Step 3:
+
+ ![detail](/maxtaco/coffee-script/raw/tame/media/detail.png)
+
+Here is the translated output (slightly hand-edited for clarity):
+
+```javascript
+(function() {
+ // await block f4()
+ (function(k) {
+ var __deferrals = new tame.Deferrals(k);
+ f4(__deferrals.defer({}));
+ __deferrals._fulfill();
+ })(function() {
+ // The continuation block, starting at 'if z'
+ (function(k) {
+ if (z) {
+ f5();
+ (function(k) {
+ // 'break' throws away the current continuation 'k'
+ // and just calls _break()
+ _break();
+ })(function() {
+ // A continuation block, after 'break', up to 'f6()'
+ // This code will never be reached
+ f6();
+ return k();
+ });
+ } else {
+ return k();
+ }
+ })(function() {
+ // end of the loop, call _continue() to start at the top
+ return _continue();
+ });
+ });
+});
+```
+
+## API and Library Documentation
+
+### tame.Rendezvous
+
+The `Rendezvous` is a not a core feature, meaning it's written as a
+straight-ahead CoffeeScript library. It's quite useful for more advanced
+control flows, so we've included it in the main runtime library.
+
+The `Rendezvous` is similar to a blocking condition variable (or a
+"Hoare sytle monitor") in threaded programming.
+
+#### tame.Rendezvous.id(i).defer slots...
+
+Associate a new deferral with the given Rendezvous, whose deferral ID
+is `i`, and whose callbacks slots are supplied as `slots`. Those
+slots can take the two forms of `defer` return as above. As with
+standard `defer`, the return value of the `Rendezvous`'s `defer` is
+fed to a function expecting a callback. As soon as that callback
+fires (and the deferral is fulfilled), the provided slots will be
+filled with the arguments to that callback.
+
+#### tame.Rendezvous.defer slots...
+
+You don't need to explicitly assign an ID to a deferral generated from a
+Rendezvous. If you don't, one will automatically be assigned, in
+ascending order starting from `0`.
+
+#### tame.Rendezvous.wait cb
+
+Wait until the next deferral on this rendezvous is fulfilled. When it
+is, callback `cb` with the ID of the fulfilled deferral. If an
+unclaimed deferral fulfilled before `wait` was called, then `cb` is fired
+immediately.
+
+Though `wait` would work with any hand-rolled JS function expecting
+a callback, it's meant to work particularly well with *tamejs*'s
+`await` function.
+
+#### Example
+
+Here is an example that shows off the different inputs and
+outputs of a `Rendezvous`. It does two parallel DNS lookups,
+and reports only when the first returns:
+
+```coffeescript
+hosts = [ "okcupid.com", "google.com" ];
+ips = errs = []
+rv = new tame.Rendezvous ();
+for h,i in hosts
+ dns.resolve hosts[i], rv.id(i).defer errs[i], ips[i]
+
+await rv.wait defer which
+console.log "#{hosts[which]} -> #{ips[which]}"
+```
+
+### connectors
+
+A *connector* is a function that takes as input
+a callback, and outputs another callback. The best example
+is a `timeout`, given here:
+
+#### tame.timeout(cb, time, res = [])
+
+Timeout an arbitrary async operation.
+
+Given a callback `cb`, a time to wait `time`, and an array to output a
+result `res`, return another callback. This connector will set up a
+race between the callback returned to the caller, and the timer that
+fires after `time` milliseconds. If the callback returned to the
+caller fires first, then fill `res[0] = true;`. If the timer won
+(i.e., if there was a timeout), then fill `res[0] = false;`.
+
+In the following example, we timeout a DNS lookup after 100ms:
+
+```coffeescript
+{timeout} = require 'tamelib'
+info = [];
+host = "pirateWarezSite.ru";
+await dns.lookup host, timeout(defer(err, ip), 100, info)
+if not info[0]
+ console.log "#{host}: timed out!"
+else if (err)
+ console.log "#{host}: error: #{err}"
+else
+ console.log "#{host} -> #{ip}"
+```
+
+### The Pipeliner library
+
+There's another way to do the windowed DNS lookups we saw earlier ---
+you can use the control flow library called `Pipeliner`, which
+manages the common pattern of having "m calls total, with only
+n of them in flight at once, where m > n."
+
+The Pipeliner class is available in the `tamelib` library:
+
+```coffeescript
+{Pipeliner} = require 'tamelib'
+pipeliner = new Pipeliner w,s
+```
+
+Using the pipeliner, we can rewrite our earlier windowed DNS lookups
+as follows:
+
+```coffescript
+do_all = (lst, windowsz) ->
+ pipeliner = new Pipeliner windowsz
+ for x in list
+ await pipeliner.waitInQueue defer()
+ do_one pipeliner.defer(), x
+ await pipeliner.flush defer()
+```
+
+The API is as follows:
+
+#### new Pipeliner w, s
+
+Create a new Pipeliner controller, with a window of at most `w` calls
+out at once, and waiting `s` seconds before launching each call. The
+default values are `w = 10` and `s = 0`.
+
+#### Pipeliner.waitInQueue c
+
+Wait in a queue until there's room in the window to launch a new call.
+The callback `c` will be fulfilled when there is room.
+
+#### Pipeliner.defer args...
+
+Create a new `defer`al for this pipeline, and pass it to whatever
+function is doing the actual work. When the work completes, fulfill
+this `defer`al --- that will update the accounting in the pipeliner
+class, allowing queued actions to proceed.
+
+#### Pipeliner.flush c
+
+Wait for the pipeline to clear out. Fulfills the callback `c`
+when the last action in the pipeline is done.
View
8 clean
@@ -0,0 +1,8 @@
+#!/bin/sh -x
+
+rm lib/coffee-script/*.js
+git checkout lib
+./bin/cake build
+#./bin/cake build:parser
+./bin/cake build
+
View
2 documentation/coffee/interpolation.coffee
@@ -1,7 +1,7 @@
author = "Wittgenstein"
quote = "A picture is a fact. -- #{ author }"
-sentence = "#{ 22 / 7 } is a decent approximation of π"
+sentence = "#{ 22 / 7 } is a decent approximation of pi"
View
2 documentation/coffee/tame_basics_1.coffee
@@ -0,0 +1,2 @@
+await setTimeout defer(), 1000
+alert "back after a 1s delay"
View
8 documentation/coffee/tame_basics_2.coffee
@@ -0,0 +1,8 @@
+window.slowAlert = (w,s,cb) ->
+ await setTimeout defer(), w
+ alert s
+ cb()
+await
+ slowAlert 500, "hello", defer()
+ slowAlert 1000, "friend", defer()
+await slowAlert 500, "back after a delay", defer()
View
6 documentation/coffee/tame_defer.coffee
@@ -0,0 +1,6 @@
+dns = require 'dns' # Use node.js's DNS resolution system
+for host in [ 'yahoo.com', 'google.com', 'nytimes.com']
+ await dns.resolve host, "A", defer err, ip
+ if err then console.log "Error for #{host}: #{err}"
+ else console.log "Resolved #{host} -> #{ip}"
+
View
5 documentation/coffee/tame_expr_1.coffee
@@ -0,0 +1,5 @@
+window.add = (a,b,cb) ->
+ await setTimeout defer(), 10
+ cb(a+b)
+x = (await add 3, 4, defer _) + (await add 1, 2, defer _)
+alert "#{x} is 10"
View
1 documentation/coffee/tame_expr_2.coffee
@@ -0,0 +1 @@
+x = await add (await add 1, 2, defer _), (await add 3, 4, defer _), defer _
View
3 documentation/coffee/tame_expr_3.coffee
@@ -0,0 +1,3 @@
+await add 1, 2, defer l
+await add 3, 4, defer r
+await add l, r, defer x
View
1 documentation/coffee/tame_inc.coffee
@@ -0,0 +1 @@
+tameRequire(window)
View
2 documentation/coffee/tame_loops_1.coffee
@@ -0,0 +1,2 @@
+for i in [0..3]
+ await slowAlert 200, "loop iteration #{i}", defer()
View
3 documentation/coffee/tame_loops_2.coffee
@@ -0,0 +1,3 @@
+await
+ for i in [0..3]
+ slowAlert 200, "loop iteration #{i}", defer()
View
5 documentation/coffee/tame_loops_3.coffee
@@ -0,0 +1,5 @@
+for i in [0..2]
+ await
+ slowAlert 100, "fast alert #{i}", defer()
+ slowAlert 200, "slow alert #{i}", defer()
+
View
158 documentation/index.html.erb
@@ -21,8 +21,6 @@
"<div class='code'>#{cshtml}#{jshtml}#{script}#{import}#{button}<br class='clear' /></div>"
end
%>
-
-
<!DOCTYPE html>
<html>
<head>
@@ -31,6 +29,9 @@
<link rel="stylesheet" type="text/css" href="documentation/css/docs.css" />
<link rel="stylesheet" type="text/css" href="documentation/css/idle.css" />
<link rel="shortcut icon" href="documentation/images/favicon.ico" />
+ <script>
+ <%= File.read("documentation/js/tame_inc.js") %>
+ </script>
</head>
<body>
@@ -47,6 +48,9 @@
<a href="#installation">Installation</a>
<a href="#usage">Usage</a>
<a href="#language">Language Reference</a>
+ <a href="#tame">Tame Basic Features</a>
+ <a href="#tame_defer">Tame Defer Values</a>
+ <a href="#tame_expr">Tame Expressions</a>
<a href="#literals">Literals: Functions, Objects and Arrays</a>
<a href="#lexical_scope">Lexical Scoping and Variable Safety</a>
<a href="#conditionals">If, Else, Unless, and Conditional Assignment</a>
@@ -115,26 +119,32 @@
<span class="bookmark" id="top"></span>
<p>
- <b>CoffeeScript is a little language that compiles into JavaScript.</b> Underneath
- all those awkward braces and semicolons, JavaScript has always had
- a gorgeous object model at its heart. CoffeeScript is an attempt to expose
- the good parts of JavaScript in a simple way.
+ CoffeeScript is a little language that compiles into JavaScript.
+ </p>
+
+ <p>
+ But mind you, this is a <b>fork</b> of CoffeeScript,
+ called <i>AdvancedCoffeeSubstitute (ACS)</i>.
+ <b>ACS is a drop-in replacement for CoffeeScript
+ designed to simplify and parallelize asynchronous control flow, both on the
+ browser, and in the server.</b>
</p>
<p>
- The golden rule of CoffeeScript is: <i>"It's just JavaScript"</i>. The code
- compiles one-to-one into the equivalent JS, and there is
- no interpretation at runtime. You can use any existing JavaScript library
- seamlessly from CoffeeScript (and vice-versa). The compiled output is
- readable and pretty-printed, passes through
+ The golden rule of CoffeeScript was: <i>"It's just JavaScript"</i>. ACS
+ is doing something deeper. It's selectively running code through a
+ <i>continuation-passing style conversion</i>. Original-style
+ CoffeeScript will compile as before, but code using the new ACS
+ features will be less recognizable. Still, the compiled output
+ is readable and pretty-printed, passes through
<a href="http://www.javascriptlint.com/">JavaScript Lint</a>
without warnings, will work in every JavaScript implementation, and tends
to run as fast or faster than the equivalent handwritten JavaScript.
</p>
<p>
<b>Latest Version:</b>
- <a href="http://github.com/jashkenas/coffee-script/tarball/1.2.0">1.2.0</a>
+ <a href="http://github.com/maxtaco/coffee-script/tarball/1.2.0">1.2.0</a>
</p>
<h2>
@@ -361,7 +371,8 @@ Expressions
<p>
<i>
This reference is structured so that it can be read from top to bottom,
- if you like. Later sections use ideas and syntax previously introduced.
+ if you like. <b>Early sections focus on new/forked ACS features</b>.
+ Later sections use ideas and syntax previously introduced.
Familiarity with JavaScript is assumed.
In all of the following examples, the source CoffeeScript is provided on
the left, and the direct compilation into JavaScript is on the right.
@@ -393,6 +404,125 @@ Expressions
</p>
<p>
+ <span id ="tame" class = "bookmark"></span>
+ <b class="header">Tame Basics</b>
+Here is simple example that waits for 1 second, and then calls <tt>alert</tt>:
+ </p>
+ <p>
+ <%= code_for('tame_basics_1', true) %>
+ </p>
+
+ <p>
+ There is one new language addition here, the <tt>await { ... }</tt> block,
+ and also one new primitive
+ function, <tt>defer</tt>. The two of them work in concert. A
+ function must "wait" at the close of an await block until all
+ deferrals made in that await block are fulfilled. The
+ function <tt>defer</tt> returns a callback, and a callee in an
+ await block can fulfill a deferral by simply calling the callback
+ it was given. In the code above, there is only one deferral
+ produced, so after it's fulfilled by `setTimer` in 1000ms, control
+ continues past the `await` block, onto the alert. The code looks
+ and feels like threaded code, but is still in the asynchronous
+ idiom (if you look at the rewritten code output by
+ the <tt>coffee</tt> compiler).
+ </p>
+
+ <p>
+ The next example shows some of the power of the tame primitives.
+ In the following examples, there are two operations that fire in
+ parallel, and a third that fires after the first two complete:
+ </p>
+
+ <p>
+ <%= code_for('tame_basics_2', true) %>
+ </p>
+
+ <p>
+ As one might expect, the tame constructs interoperate with standard
+ coffee control structures. The following program will call
+ <tt>slowAlert</tt> three times sequentially:
+ </p>
+
+ <p>
+ <%= code_for('tame_loops_1', true) %>
+ </p>
+
+ <p>
+ To fire those asynchronous calls in parallel, simply exchange
+ the <tt>for</tt> and <tt>await</tt> blocks:
+ </p>
+
+ <p>
+ <%= code_for('tame_loops_2', true) %>
+ </p>
+
+ Things start to get interesting when you combine parallel and
+ serial control flow, but the syntax remains the same:
+
+ <p>
+ <%= code_for('tame_loops_3', true) %>
+ </p>
+
+ <p>
+ <span id ="tame_defer" class = "bookmark"></span>
+ <b class="header">Tame Defer Values</b>
+ Many JS and coffee libraries use callbacks passed as parameters
+ to signify when the given operation has completed, and which values
+ were produced. As such, <tt>defer</tt> can take one or more arguments
+ which are filled in after a deferral is fulfilled. Note that the
+ snippet below is <i>node.js</i> code that requires a DNS resolver:
+ <p>
+ <%= code_for('tame_defer') %>
+ </p>
+
+ <p>
+ <span id="tame_expr" class = "bookmark"></span>
+ <b class="header">Tame in an Expression Context</b>
+ It's possible
+ to use <tt>await</tt> blocks as expressions. And recursively
+ speaking, it's possible to use statements that contain `await`
+ blocks as expressions.
+ </p>
+
+ <p>
+ The simple rule is that an <tt>await</tt> block takes on the value of the
+ <tt>defer</tt> slot named <tt>_</tt> after its been fulfilled. If there
+ are multiple defer slots named <tt>_</tt> within the await block, then
+ the last writer wins. In practice, there's usually only one. Thus:
+ </p>
+
+ <p><%= code_for('tame_expr_1', true) %></p>
+
+ <p>
+ Of course, things can get arbitrarily compicated and nested, so it
+ gets hairy. Consider this:
+ </p>
+
+ <p><%= code_for('tame_expr_2', true) %></p>
+
+ <p>
+ The rule is that all nested <tt>await</tt> blocks (barf!) are evaluated
+ sequentially in DFS order. You will get <tt>10</tt> in the above example
+ after three sequential calls to <tt>add</tt>.
+ </p>
+
+ <p>
+ I really don't like this feature for two reasons: (1) it's tricky to
+ get the implementation right, and I'm sure I haven't tested all of the
+ corner cases yet; (2) it's difficult to read and understand what
+ happens in which order. I would suggest you save yourself the heartache,
+ and just write the above as this:
+ </p>
+
+ <p><%= code_for('tame_expr_3', true) %></p>
+
+ <p>
+ It's just so much clearer what happens in which order, and it's easier
+ to parallelize or serialize as you see fit.
+ </p>
+
+ <p>
<span id="literals" class="bookmark"></span>
<b class="header">Functions</b>
Functions are defined by an optional list of parameters in parentheses,
@@ -758,7 +888,7 @@ Expressions
Destructuring assignment can be used with any depth of array and object nesting,
to help pull out deeply nested properties.
</p>
- <%= code_for('object_extraction', 'name + " " + street') %>
+ <%= code_for('object_extraction', 'name + " - " + street') %>
<p>
Destructuring assignment can even be combined with splats.
</p>
View
8 documentation/js/array_comprehensions.js
@@ -1,4 +1,4 @@
-var courses, dish, food, foods, i, _i, _j, _len, _len2, _len3, _ref;
+var courses, dish, food, foods, i, _i, _j, _k, _len, _len2, _len3, _ref;
_ref = ['toast', 'cheese', 'wine'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@@ -8,14 +8,14 @@ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
courses = ['greens', 'caviar', 'truffles', 'roast', 'cake'];
-for (i = 0, _len2 = courses.length; i < _len2; i++) {
+for (i = _j = 0, _len2 = courses.length; _j < _len2; i = ++_j) {
dish = courses[i];
menu(i + 1, dish);
}
foods = ['broccoli', 'spinach', 'chocolate'];
-for (_j = 0, _len3 = foods.length; _j < _len3; _j++) {
- food = foods[_j];
+for (_k = 0, _len3 = foods.length; _k < _len3; _k++) {
+ food = foods[_k];
if (food !== 'chocolate') eat(food);
}
View
10 documentation/js/classes.js
@@ -4,6 +4,8 @@ var Animal, Horse, Snake, sam, tom,
Animal = (function() {
+ Animal.name = 'Animal';
+
function Animal(name) {
this.name = name;
}
@@ -20,8 +22,10 @@ Snake = (function(_super) {
__extends(Snake, _super);
+ Snake.name = 'Snake';
+
function Snake() {
- Snake.__super__.constructor.apply(this, arguments);
+ return Snake.__super__.constructor.apply(this, arguments);
}
Snake.prototype.move = function() {
@@ -37,8 +41,10 @@ Horse = (function(_super) {
__extends(Horse, _super);
+ Horse.name = 'Horse';
+
function Horse() {
- Horse.__super__.constructor.apply(this, arguments);
+ return Horse.__super__.constructor.apply(this, arguments);
}
Horse.prototype.move = function() {
View
2 documentation/js/interpolation.js
@@ -4,4 +4,4 @@ author = "Wittgenstein";
quote = "A picture is a fact. -- " + author;
-sentence = "" + (22 / 7) + " is a decent approximation of π";
+sentence = "" + (22 / 7) + " is a decent approximation of pi";
View
4 documentation/js/range_comprehensions.js
@@ -1,9 +1,9 @@
var countdown, num;
countdown = (function() {
- var _results;
+ var _i, _results;
_results = [];
- for (num = 10; num >= 1; num--) {
+ for (num = _i = 10; _i >= 1; num = --_i) {
_results.push(num);
}
return _results;
View
12 documentation/js/tame_basics_1.js
@@ -0,0 +1,12 @@
+var __tame_deferrals, __tame_k,
+ _this = this;
+
+__tame_k = function() {};
+
+(function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ setTimeout(__tame_deferrals.defer({}), 1000);
+ __tame_deferrals._fulfill();
+})(function() {
+ return alert("back after a 1s delay");
+});
View
28 documentation/js/tame_basics_2.js
@@ -0,0 +1,28 @@
+var __tame_deferrals, __tame_k,
+ _this = this;
+
+__tame_k = function() {};
+
+window.slowAlert = function(w, s, cb) {
+ var __tame_deferrals,
+ _this = this;
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ setTimeout(__tame_deferrals.defer({}), w);
+ __tame_deferrals._fulfill();
+ })(function() {
+ alert(s);
+ return cb();
+ });
+};
+
+(function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ slowAlert(500, "hello", __tame_deferrals.defer({}));
+ slowAlert(1000, "friend", __tame_deferrals.defer({}));
+ __tame_deferrals._fulfill();
+})(function() {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ slowAlert(500, "back after a delay", __tame_deferrals.defer({}));
+ __tame_deferrals._fulfill();
+});
View
39 documentation/js/tame_defer.js
@@ -0,0 +1,39 @@
+var dns, err, host, ip, __tame_deferrals, _i, _len, _next, _ref, _while,
+ _this = this;
+
+__tame_k = function() {};
+
+dns = require('dns');
+
+_ref = ['yahoo.com', 'google.com', 'nytimes.com'];
+_len = _ref.length;
+_i = 0;
+_while = function(__tame_k) {
+ var _break, _continue;
+ _break = __tame_k;
+ _continue = function() {
+ ++_i;
+ return _while(__tame_k);
+ };
+ _next = _continue;
+ if (_i < _len) {
+ host = _ref[_i];
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ dns.resolve(host, "A", __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ err = arguments[0];
+ return ip = arguments[1];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ })(function() {
+ return _next(err ? console.log("Error for " + host + ": " + err) : console.log("Resolved " + host + " -> " + ip));
+ });
+ } else {
+ return _break();
+ }
+};
+_while(__tame_k);
View
47 documentation/js/tame_expr_1.js
@@ -0,0 +1,47 @@
+var x, __tame_deferrals,
+ _this = this;
+
+__tame_k = function() {};
+
+window.add = function(a, b, cb) {
+ var __tame_deferrals,
+ _this = this;
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ setTimeout(__tame_deferrals.defer({}), 10);
+ __tame_deferrals._fulfill();
+ })(function() {
+ return cb(a + b);
+ });
+};
+
+(function(__tame_k) {
+ return (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(3, 4, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return __tame_deferrals.ret = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ })(function(___tame_p__2) {
+ return (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(1, 2, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return __tame_deferrals.ret = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ })(function(___tame_p__1) {
+ return __tame_k(___tame_p__2 + ___tame_p__1);
+ });
+ });
+})(function(___tame_p__0) {
+ x = ___tame_p__0;
+ return alert("" + x + " is 10");
+});;
View
42 documentation/js/tame_expr_2.js
@@ -0,0 +1,42 @@
+var x, __tame_deferrals,
+ _this = this;
+
+__tame_k = function() {};
+
+(function(__tame_k) {
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(1, 2, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return __tame_deferrals.ret = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ })(function(___tame_p__5) {
+ return (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(3, 4, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return __tame_deferrals.ret = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ })(function(___tame_p__4) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(___tame_p__5, ___tame_p__4, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return __tame_deferrals.ret = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ });
+ });
+})(function(___tame_p__3) {
+ return x = ___tame_p__3;
+});;
View
38 documentation/js/tame_expr_3.js
@@ -0,0 +1,38 @@
+var l, r, x, __tame_deferrals,
+ _this = this;
+
+__tame_k = function() {};
+
+(function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(1, 2, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return l = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+})(function() {
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(3, 4, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return r = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ })(function() {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ add(l, r, __tame_deferrals.defer({
+ assign_fn: (function() {
+ return function() {
+ return x = arguments[0];
+ };
+ })()
+ }));
+ __tame_deferrals._fulfill();
+ });
+});
View
35 documentation/js/tame_inc.js
@@ -0,0 +1,35 @@
+var __slice = Array.prototype.slice;
+
+window.tame = {
+ Deferrals: (function() {
+
+ function _Class(_arg) {
+ this.continuation = _arg;
+ this.count = 1;
+ this.ret = null;
+ }
+
+ _Class.prototype._fulfill = function() {
+ if (!--this.count) return this.continuation(this.ret);
+ };
+
+ _Class.prototype.defer = function(defer_params) {
+ var _this = this;
+ ++this.count;
+ return function() {
+ var inner_params, _ref;
+ inner_params = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ if (defer_params != null) {
+ if ((_ref = defer_params.assign_fn) != null) {
+ _ref.apply(null, inner_params);
+ }
+ }
+ return _this._fulfill();
+ };
+ };
+
+ return _Class;
+
+ })()
+}
+window.__tame_k = function() {};
View
25 documentation/js/tame_loops_1.js
@@ -0,0 +1,25 @@
+var i, __tame_deferrals, _i, _next, _while,
+ _this = this;
+
+__tame_k = function() {};
+
+i = 0;
+_while = function(__tame_k) {
+ var _break, _continue;
+ _break = __tame_k;
+ _continue = function() {
+ ++i;
+ return _while(__tame_k);
+ };
+ _next = _continue;
+ if (i <= 3) {
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ slowAlert(200, "loop iteration " + i, __tame_deferrals.defer({}));
+ __tame_deferrals._fulfill();
+ })(_next);
+ } else {
+ return _break();
+ }
+};
+_while(__tame_k);
View
9 documentation/js/tame_loops_2.js
@@ -0,0 +1,9 @@
+var i, __tame_deferrals, __tame_k, _i;
+
+__tame_k = function() {};
+
+__tame_deferrals = new tame.Deferrals(__tame_k);
+for (i = _i = 0; _i <= 3; i = ++_i) {
+ slowAlert(200, "loop iteration " + i, __tame_deferrals.defer({}));
+}
+__tame_deferrals._fulfill();
View
26 documentation/js/tame_loops_3.js
@@ -0,0 +1,26 @@
+var i, __tame_deferrals, _i, _next, _while,
+ _this = this;
+
+__tame_k = function() {};
+
+i = 0;
+_while = function(__tame_k) {
+ var _break, _continue;
+ _break = __tame_k;
+ _continue = function() {
+ ++i;
+ return _while(__tame_k);
+ };
+ _next = _continue;
+ if (i <= 2) {
+ (function(__tame_k) {
+ __tame_deferrals = new tame.Deferrals(__tame_k);
+ slowAlert(100, "fast alert " + i, __tame_deferrals.defer({}));
+ slowAlert(200, "slow alert " + i, __tame_deferrals.defer({}));
+ __tame_deferrals._fulfill();
+ })(_next);
+ } else {
+ return _break();
+ }
+};
+_while(__tame_k);
View
17 examples/tame/dns.coffee
@@ -0,0 +1,17 @@
+
+dns = require("dns");
+
+do_one = (cb, host) ->
+ await dns.resolve host, "A", defer(err, ip)
+ if err
+ console.log "ERROR! " + err
+ else
+ console.log host + " -> " + ip
+ cb()
+
+do_all = (lst) ->
+ await
+ for h in lst
+ do_one defer(), h
+
+do_all process.argv.slice(2)
View
17 examples/tame/dns_serial.coffee
@@ -0,0 +1,17 @@
+
+dns = require("dns");
+
+do_one = (cb, host) ->
+ await dns.resolve host, "A", defer(err, ip)
+ if err
+ console.log "ERROR! " + err
+ else
+ console.log host + " -> " + ip
+ cb()
+
+do_all = (lst) ->
+ for h in lst
+ await
+ do_one defer(), h
+
+do_all process.argv.slice(2)
View
4 extras/coffee-script.js
2 additions, 2 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
632 iced.md
@@ -0,0 +1,632 @@
+What Is IcedCoffeeScript?
+==================
+
+IcedCoffeeScript (ICS) is a system for handling callbacks in event-based code.
+There were two existing implementations, one in [the sfslite library for
+C++](https://github.com/maxtaco/sfslite), and another in the [tamejs translator
+for JavaScript](https://github.com/maxtaco/tamejs). This extension to
+CoffeeScript is a third implementation. The code and translation techniques
+are derived from experience with JS, but with some new Coffee-style
+flavoring.
+
+This document first presents a "Iced" tutorial (adapted from the JavaScript
+version), and then discusses the specifics of the CoffeeScript implementation.
+
+(Note: we are slowly changing all instances of the word "tame" in
+the repository and the generated code to "iced". This should have
+little impact on ICS source in the wild, since the word "tame"
+only appears in your code if you're using advanced features.)
+
+# Quick Tutorial and Examples
+
+Here is a simple example that prints "hello" 10 times, with 100ms
+delay slots in between:
+
+```coffeescript
+# A basic serial loop
+for i in [0..10]
+ await setTimeout(defer(), 100)
+ console.log "hello"
+```
+
+There is one new language addition here, the `await ... ` block (or
+expression), and also one new primitive function, `defer`. The two of
+them work in concert. A function must "wait" at the close of a
+`await` block until all `defer`rals made in that `await` block are
+fulfilled. The function `defer` returns a callback, and a callee in
+an `await` block can fulfill a deferral by simply calling the callback
+it was given. In the code above, there is only one deferral produced
+in each iteration of the loop, so after it's fulfilled by `setTimer`
+in 100ms, control continues past the `await` block, onto the log line,
+and back to the next iteration of the loop. The code looks and feels
+like threaded code, but is still in the asynchronous idiom (if you
+look at the rewritten code output by the *coffee* compiler).
+
+This next example does the same, while showcasing power of the
+`await..` language addition. In the example below, the two timers
+are fired in parallel, and only when both have fulfilled their deferrals
+(after 100ms), does progress continue...
+
+```coffeescript
+for i in [0..10]
+ await
+ setTimeout defer(), 100
+ setTimeout defer(), 10
+ console.log ("hello");
+```
+
+Now for something more useful. Here is a parallel DNS resolver that
+will exit as soon as the last of your resolutions completes:
+
+```coffeescript
+dns = require("dns");
+
+do_one = (cb, host) ->
+ await dns.resolve host, "A", defer(err, ip)
+ msg = if err then "ERROR! #{err}" else "#{host} -> #{ip}"
+ console.log msg
+ cb()
+
+do_all = (lst) ->
+ await
+ for h in lst
+ do_one defer(), h
+
+do_all process.argv[2...]
+```
+
+You can run this on the command line like so:
+
+ iced examples/tame/dns.coffee yahoo.com google.com nytimes.com okcupid.com tinyurl.com
+
+And you will get a response:
+
+ yahoo.com -> 72.30.2.43,98.137.149.56,209.191.122.70,67.195.160.76,69.147.125.65
+ google.com -> 74.125.93.105,74.125.93.99,74.125.93.104,74.125.93.147,74.125.93.106,74.125.93.103
+ nytimes.com -> 199.239.136.200
+ okcupid.com -> 66.59.66.6
+ tinyurl.com -> 195.66.135.140,195.66.135.139
+
+If you want to run these DNS resolutions in serial (rather than
+parallel), then the change from above is trivial: just switch the
+order of the `await` and `for` statements above:
+
+```coffeescript
+do_all = (lst) ->
+ for h in lst
+ await
+ do_one defer(), h
+```
+
+### Slightly More Advanced Example
+
+We've shown parallel and serial work flows, what about something in
+between? For instance, we might want to make progress in parallel on
+our DNS lookups, but not smash the server all at once. A compromise is
+windowing, which can be achieved in IcedCoffeeScript conveniently in a
+number of different ways. The [2007 academic paper on
+tame](http://pdos.csail.mit.edu/~max/docs/tame.pdf) suggests a
+technique called a *rendezvous*. A rendezvous is implemented in
+CoffeeScript as a pure CS construct (no rewriting involved), which
+allows a program to continue as soon as the first deferral is
+fulfilled (rather than the last):
+
+```coffeescript
+icedRequire(node) # need full library via require() for rendezvous
+
+do_all = (lst, windowsz) ->
+ rv = new iced.Rendezvous
+ nsent = 0
+ nrecv = 0
+
+ while nrecv < lst.length
+ if nsent - nrecv < windowsz and nsent < n
+ do_one rv.id(nsent).defer(), lst[nsent]
+ nsent++
+ else
+ await rv.wait defer evid
+ console.log "got back lookup nsent=#{evid}"
+ nrecv++
+```
+
+This code maintains two counters: the number of requests sent, and the
+number received. It keeps looping until the last lookup is received.
+Inside the loop, if there is room in the window and there are more to
+send, then send; otherwise, wait and harvest. `Rendezvous.defer`
+makes a deferral much like the `defer` primitive, but it can be
+labeled with an identifier. This way, the waiter can know which
+deferral has fulfilled. In this case we use the variable `nsent` as the
+defer ID --- it's the ID of this deferral in launch order. When we
+harvest the deferral, `rv.wait` fires its callback with the ID of the
+deferral that's harvested.
+
+Note that with windowing, the arrival order might not be the same as
+the issue order. In this example, a slower DNS lookup might arrive
+after faster ones, even if issued before them.
+
+### Composing Serial And Parallel Patterns
+
+In IcedCoffeeScript, arbitrary composition of serial and parallel control flows is
+possible with just normal functional decomposition. Therefore, we
+don't allow direct `await` nesting. With inline anonymous CoffeeScript
+functions, you can concisely achieve interesting patterns. The code
+below launches 10 parallel computations, each of which must complete
+two serial actions before finishing:
+
+```coffeescript
+f = (n,cb) ->
+ await
+ for i in [0..n]
+ ((cb) ->
+ await setTimeout defer(), 5 * Math.random()
+ await setTimeout defer(), 4 * Math.random()
+ cb()
+ )(defer())
+ cb()
+```
+
+### autocb
+
+Most of the time, an iced function will call its callback and return
+at the same time. To get this behavior "for free", you can simply
+name this callback `autocb` and it will fire whenever your iced function
+returns. For instance, the above example could be equivalently written as:
+
+```coffeescript
+f = (n,autocb) ->
+ await
+ for i in [0..n]
+ ((autocb) ->
+ setTimeout defer(), 5 * Math.random()
+ setTimeout defer(), 4 * Math.random()
+ )(defer())
+```
+In the first example, recall, you call `cb()` explicitly. In this
+example, because the callback is named `autocb`, it's fired
+automatically when the iced function returns.
+
+If your callback needs to fulfill with a value, then you can pass
+that value via `return`. Consider the following function, that waits
+for a random number of seconds between 0 and 4. After waiting, it
+then fulfills its callback `cb` with the amount of time it waited:
+
+```coffeescript
+rand_wait = (cb) ->
+ time = Math.floor Math.random() * 5
+ if time is 0
+ cb(0)
+ return
+ await setTimeout defer(), time
+ cb(time) # return here, implicitly.....
+```
+
+This function can written equivalently with `autocb` as:
+
+```coffeescript
+rand_wait = (autocb) ->
+ time = Math.floor Math.random() * 5
+ return 0 if time is 0
+ await setTimeout defer(), time
+ return time
+```
+
+Implicitly, `return 0;` is mapped by the CoffeeScript compiler to `autocb(0); return`.
+
+## Language Design Considerations
+
<