Skip to content

Instantly share code, notes, and snippets.

@timbertson
Created June 22, 2010 10:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timbertson/448286 to your computer and use it in GitHub Desktop.
Save timbertson/448286 to your computer and use it in GitHub Desktop.
(function(){
var check_later, debug, delay, return_arg, return_call;
var __slice = Array.prototype.slice, __hasProp = Object.prototype.hasOwnProperty;
require('/home/tim/dev/web/coffee-spec/lib/coffee-spec').autorun(global, 2);
debug = require("sys").debug;
delay = function(func) {
return process.nextTick(func);
};
return_arg = function(x, cb) {
return delay(function() {
return cb(x);
});
};
return_call = function(x, cb) {
return delay(function() {
return cb(x());
});
};
// due to concurrency / timing issues, we need an "asynchronous" ok method.
// this one is awful, but it does seem to work
// note that it supports up to 5 levels of nested "return" callbacks. probably.
check_later = function(fn) {
var iterate, stack, tries;
tries = 0;
// grab a *useful* stack trace here, for use later
// (if the eventual check fails)
stack = null;
try {
throw new Error();
} catch (e) {
stack = e.stack;
}
iterate = function() {
var _try;
tries += 1;
_try = function() {
try {
return fn();
} catch (e) {
if (tries <= 5) {
return iterate();
}
throw new Error(e + " (deferred)\n " + stack + "\n ----");
}
};
return delay(_try);
};
return iterate();
};
describe('deferred', function() {
it('should deal with a single defer', function(pass) {
var single_def;
expect(1);
single_def = function(x, cb) {
var _a;
return_arg(x, function(_a) {
return cb(_a);
});
};
return single_def("a", function(result) {
equal(result, "a");
return pass();
});
});
it('should allow for multiple defers in a single expression', function(pass) {
var callback_called, multi_def;
expect(1);
multi_def = function(x, cb) {
var _a, _b;
return_arg(x, function(_a) {
return return_arg(3, function(_b) {
return cb(2 + _a * _b);
});
});
};
equal(multi_def.length, 2);
callback_called = 0;
multi_def(2, function(result) {
equal(result, 8);
return callback_called += 1;
});
return check_later(function() {
equal(callback_called, 1);
return pass();
});
});
it('should *always* return', function(pass) {
var implicit_return_from_skipped_branch;
expect(1);
implicit_return_from_skipped_branch = function(cb) {
var _a;
return_arg(2, function(_a) {
var x;
x = _a;
while (x > 3) {
// force a pure_expression, so we get no funny business...
break;
}
return cb();
});
};
return implicit_return_from_skipped_branch(function() {
return pass();
});
});
it('should maintain the original `this` in a continuation', function(pass) {
var end_this, messing_with_this;
expect(1);
end_this = null;
messing_with_this = function(cb) {
var _a, _b, initial_this;
_a = this;
initial_this = _a;
return_arg(1, function(_b) {
var x;
x = _b;
ok(_a === initial_this, ("this (" + (_a) + ") != initial_this (" + (initial_this) + ")"));
return pass();
});
};
return messing_with_this.call("this", function() { });
});
it('should not interfere with splats', function(pass) {
var splat_def, splat_in_defer;
expect(1);
splat_def = function(a) {
var _c, rest;
var _a = arguments.length, _b = _a >= 3, cb = arguments[_b ? _a - 1 : 1];
rest = __slice.call(arguments, 1, _a - 1);
return_arg(rest, function(_c) {
return cb(_c);
});
};
splat_in_defer = function() {
var _a;
splat_def(1, 2, 3, 4, function(_a) {
var rest;
rest = _a;
equal(rest[0], 2);
equal(rest[1], 3);
equal(rest[2], 4);
return pass();
});
};
return splat_in_defer(function() { });
});
it('should continue code after a defer', function(pass) {
var deferred_body;
expect(1);
deferred_body = function(x, cb) {
var _a;
x = x + 1;
return_arg(x + 1, function(_a) {
var y, z;
y = _a;
z = y + 1;
return cb(z);
});
};
return deferred_body(1, function(result) {
equal(result, 4);
return pass();
});
});
it('should allow a deferred lambda', function(pass) {
var deferred_lambda;
expect(1);
deferred_lambda = function(cb) {
return (function(val, cb) {
var _a;
return_arg(val, function(_a) {
return cb(_a);
});
})(1, cb);
};
return deferred_lambda(function(_) {
equal(_, 1);
return pass();
});
});
it('should allow defer calls in object literals', function(pass) {
var defer_in_obj;
expect(1);
defer_in_obj = function(cb) {
var _a;
return_arg("val", function(_a) {
return cb({
name: "object",
val: _a
});
});
};
return defer_in_obj(function(_) {
equal(_.name, "object");
equal(_.val, "val");
return pass();
});
});
it('should allow defer in arbitrary expressions', function(pass) {
var defer_in_expr;
expect(1);
defer_in_expr = function(cb) {
var _a;
return_arg(1, function(_a) {
return cb(1 + _a + 1);
});
};
return defer_in_expr(function(_) {
equal(_, 3);
return pass();
});
});
it('should allow control-flow modifying deferred calls in a function call', function(pass) {
var deferred_map_in_call;
expect(1);
deferred_map_in_call = function(cb) {
var _a, _c, _d, _e, _f, _h, _i, _j, _k, _l, _m, first, x;
first = function(lst) {
return lst[0];
};
(function(_b) {
_c = (function() {
_j = []; _l = [3, 2, 1];
for (_k = 0, _m = _l.length; _k < _m; _k++) {
_a = _l[_k];
_j.push(_a);
}
return _j;
})();
_d = [];
(function(_g) {
_h = function() {
if (!(_c.length > 0)) {
return _g(undefined); //break;
}
x = _c.shift();
return_arg(x, function(_o) {
_f = _o;
_d.push(_f);
return _h(undefined); //continue;
return undefined;
});
};
return _h(undefined);
return _g(undefined);
})(function(_n) {
_n;
return _b(_d);
});
})(function(_i) {
return cb(first(_i));
});
};
return deferred_map_in_call(function(_) {
equal(_, 3);
return pass();
});
});
it('should implicitly return a deferred value', function(pass) {
var implicit_return, implicit_return_2;
expect(1);
implicit_return = function(cb) {
var _a;
return_arg(1, function(_a) {
return cb(_a);
});
};
implicit_return(function(result) {
return equal(result, 1);
});
implicit_return_2 = function(cb) {
var _a;
return_arg(1, function(_a) {
var x;
x = _a;
return cb(x + 1);
});
};
return implicit_return_2(function(result) {
equal(result, 2);
return pass();
});
});
it('should nest defers', function(pass) {
var nested_defer;
expect(1);
nested_defer = function(cb) {
var _a;
return_arg("x", function(_a) {
var _b, x;
x = _a;
return_arg(x + "y", function(_b) {
var _c, y;
y = _b;
return_arg(y + "z", function(_c) {
var z;
z = _c;
return cb(z + "_");
});
});
});
};
return nested_defer(function(result) {
equal(result, "xyz_");
return pass();
});
});
it('should support defers in branches', function(pass) {
var branched_defer;
expect(2);
branched_defer = function(input, cb) {
var _b, _c, x, y;
(function(_a) {
_b = input === 1;
if (_b) {
return (function() {
return_arg("true", function(_d) {
x = _d;
return_arg(x + " branch", function(_e) {
y = _e;
return _a(y);
});
});
})();
} else {
return (function() {
y = "false branch";
return _a(y);
})();
}
})(function(_c) {
_c;
return cb(y + " taken");
});
};
return branched_defer(1, function(_) {
equal(_, "true branch taken");
pass();
return branched_defer(0, function(_) {
equal(_, "false branch taken");
return pass();
});
});
});
it('should work in ternary statement clause', function(pass) {
var ternary_defer;
expect(2);
ternary_defer = function(input, cb) {
var _a;
return_arg(input, function(_a) {
return cb(_a === 1 ? "one" : "two");
});
};
return ternary_defer(1, function(_) {
equal(_, "one");
pass();
return ternary_defer(2, function(_) {
equal(_, "two");
return pass();
});
});
});
it('should work in ternary statement result', function(pass) {
var ternary_defer2;
expect(2);
ternary_defer2 = function(input, cb) {
var _b, _c;
(function(_a) {
_b = input === 1;
if (_b) {
return (function() {
return_arg("one", function(_d) {
return _a(_d);
});
})();
} else {
return (function() {
return _a("two");
})();
}
})(function(_c) {
var x;
x = _c;
return cb(x + " result");
});
};
return ternary_defer2(1, function(_) {
equal(_, "one result");
pass();
return ternary_defer2(2, function(_) {
equal(_, "two result");
return pass();
});
});
});
it('should be sliceable', function(pass) {
var defer_slice;
expect(1);
defer_slice = function(cb) {
var _a;
return_arg([1, 2, 3], function(_a) {
return cb((_a).slice(0, 1));
});
};
return defer_slice(function(_) {
equal(_.length, 1);
equal(_[0], 1);
return pass();
});
});
it('should work as a slice index', function(pass) {
var defer_slice2;
expect(1);
defer_slice2 = function(cb) {
var _a;
return_arg(1, function(_a) {
return cb([1, 2, 3].slice(0, _a));
});
};
return defer_slice2(function(_) {
equal(_.length, 1);
equal(_[0], 1);
return pass();
});
});
it('should support early returns', function(pass) {
var defer_early_return;
expect(2);
defer_early_return = function(input, cb) {
var _b, _c, x;
(function(_a) {
_b = input === 1;
if (_b) {
return (function() {
return cb(1);
})();
} else {
return (function() {
return_arg(2, function(_d) {
x = _d;
return _a(x);
});
})();
}
})(function(_c) {
_c;
return cb(x);
});
};
return defer_early_return(1, function(_) {
equal(_, 1);
pass();
return defer_early_return(2, function(_) {
equal(_, 2);
return pass();
});
});
});
// loops!
it('should support defer as subject of a map', function(pass) {
var map_defer_as_subject;
expect(1);
map_defer_as_subject = function(cb) {
var _a;
return_arg([0, 1, 2], function(_a) {
var _b, _c, _d, _e, x;
return cb((function() {
_b = []; _d = _a;
for (_c = 0, _e = _d.length; _c < _e; _c++) {
x = _d[_c];
_b.push(x + 1);
}
return _b;
})());
});
};
return map_defer_as_subject(function(_) {
equal(_.length, 3);
equal(_[0], 1);
equal(_[1], 2);
equal(_[2], 3);
return pass();
});
});
it('should support defer in a while loop', function(pass) {
var defer_in_while_loop;
expect(1);
defer_in_while_loop = function(cb) {
var _b, _c, loops_left, x;
x = 1;
loops_left = 10;
(function(_a) {
_b = function() {
if (!((loops_left > 0))) {
return _a(undefined); //break;
}
loops_left -= 1;
return_arg(1, function(_d) {
x += _d;
return _b(undefined); //continue;
return undefined;
});
};
return _b(undefined);
return _a(undefined);
})(function(_c) {
_c;
return cb(x + 1);
});
};
return defer_in_while_loop(function(_) {
equal(_, 12);
return pass();
});
});
it('should support defer as subject of while loop', function(pass) {
var defer_as_subject_of_while_loop;
expect(1);
defer_as_subject_of_while_loop = function(cb) {
var _b, _c, x;
x = 10;
(function(_a) {
_b = function() {
return_arg(x, function(_d) {
if (!(_d > 0)) {
return _a(undefined); //break;
}
x -= 1;
return _b(undefined); //continue;
return undefined;
});
};
return _b(undefined);
return _a(undefined);
})(function(_c) {
_c;
x + 1;
return cb(x);
});
};
return defer_as_subject_of_while_loop(function(_) {
equal(_, 0);
return pass();
});
});
it('should support break in a while loop', function(pass) {
var break_with_defer;
expect(1);
break_with_defer = function(cb) {
var _b, _c, x;
x = 10;
(function(_a) {
_b = function() {
if (!(x > 0)) {
return _a(undefined); //break;
}
if (x === 5) {
return _a(undefined); //break;
}
return_arg(x, function(_d) {
_d;
x -= 1;
return _b(undefined); //continue;
return undefined;
});
};
return _b(undefined);
return _a(undefined);
})(function(_c) {
_c;
return cb(x);
});
};
return break_with_defer(function(_) {
equal(_, 5);
return pass();
});
});
it('should support continue in a while loop', function(pass) {
var break_with_defer;
expect(1);
break_with_defer = function(cb) {
var _b, _c, x, y;
x = 10;
y = 10;
(function(_a) {
_b = function() {
if (!(y > 0)) {
return _a(undefined); //break;
}
y -= 1;
if (y === 5) {
return _b(undefined); //continue;
}
return_arg(x, function(_d) {
_d;
x -= 1;
return _b(undefined); //continue;
return undefined;
});
};
return _b(undefined);
return _a(undefined);
})(function(_c) {
_c;
return cb(x, y);
});
};
return break_with_defer(function(x, y) {
equal(y, 0);
equal(x, 1);
// should have had one less decrement
return pass();
});
});
it('should support defer in a range loop', function(pass) {
var range_defer;
expect(1);
range_defer = function(cb) {
var _a, _c, _d, _e, _f, _h, _i, _j, sum, x;
sum = 0;
(function(_b) {
_c = (function() {
_j = [];
for (_a = 0; _a < 5; _a += 1) {
_j.push(_a);
}
return _j;
})();
_d = [];
(function(_g) {
_h = function() {
if (!(_c.length > 0)) {
return _g(undefined); //break;
}
x = _c.shift();
return_arg(x + 1, function(_l) {
_f = (function() {
sum = sum + _l;
return sum;
})();
_d.push(_f);
return _h(undefined); //continue;
return undefined;
});
};
return _h(undefined);
return _g(undefined);
})(function(_k) {
_k;
return _b(_d);
});
})(function(_i) {
_i;
return cb(sum * 10);
});
};
return range_defer(function(_) {
equal(_, (1 + 2 + 3 + 4 + 5) * 10);
return pass();
});
});
it('should support defer as a while loop filter', function(pass) {
var filter_defer;
expect(1);
filter_defer = function(cb) {
var _b, _c, even_nums, num, nums;
nums = [1, 2, 3, 4, 5];
even_nums = [];
(function(_a) {
_b = function() {
if (!(nums.length > 0)) {
return _a(undefined); //break;
}
return_arg(2, function(_d) {
if (!((num = nums.shift()) % _d === 0)) {
return _b(undefined); //continue;
}
even_nums.push(num);
return _b(undefined); //continue;
return undefined;
});
};
return _b(undefined);
return _a(undefined);
})(function(_c) {
_c;
return cb(even_nums);
});
};
filter_defer(function(_) {
equal(_.length, 2);
equal(_[0], 2);
equal(_[1], 4);
return pass();
});
return expect(1);
});
it('should support defer as a list filter', function(pass) {
var filter_defer;
expect(1);
filter_defer = function(cb) {
var _a, _c, _d, _e, _f, _h, _i, _j, _k, _l, _m, even_nums, num, nums;
nums = [1, 2, 3, 4, 5];
even_nums = [];
(function(_b) {
_c = (function() {
_j = []; _l = nums;
for (_k = 0, _m = _l.length; _k < _m; _k++) {
_a = _l[_k];
_j.push(_a);
}
return _j;
})();
_d = [];
(function(_g) {
_h = function() {
if (!(_c.length > 0)) {
return _g(undefined); //break;
}
num = _c.shift();
return_call(function() {
return (num % 2) === 0;
}, function(_o) {
if (!(_o)) {
return _h(undefined); //continue;
}
_f = (function() {
return even_nums.push(num);
})();
_d.push(_f);
return _h(undefined); //continue;
return undefined;
});
};
return _h(undefined);
return _g(undefined);
})(function(_n) {
_n;
return _b(_d);
});
})(function(_i) {
_i;
return cb(even_nums);
});
};
filter_defer(function(_) {
equal(_.length, 2);
equal(_[0], 2);
equal(_[1], 4);
return pass();
});
return expect(1);
});
it('should support parenthesised control-flow statements containing defers', function(pass) {
var map_defer;
expect(1);
map_defer = function(cb) {
var _a, _c, _d, _e, _f, _h, _i, _j, _k, _l, _m, nums, x;
nums = [1, 2, 3];
(function(_b) {
_c = (function() {
_j = []; _l = nums;
for (_k = 0, _m = _l.length; _k < _m; _k++) {
_a = _l[_k];
_j.push(_a);
}
return _j;
})();
_d = [];
(function(_g) {
_h = function() {
if (!(_c.length > 0)) {
return _g(undefined); //break;
}
x = _c.shift();
return_arg(x + 1, function(_o) {
_f = _o;
_d.push(_f);
return _h(undefined); //continue;
return undefined;
});
};
return _h(undefined);
return _g(undefined);
})(function(_n) {
_n;
return _b(_d);
});
})(function(_i) {
var x;
x = (_i);
return cb(x);
});
};
return map_defer(function(_) {
equal(_.length, 3);
equal(_[0], 2);
equal(_[1], 3);
equal(_[2], 4);
return pass();
});
});
it('should support a deferred expression in a map result', function(pass) {
var map_defer;
expect(1);
map_defer = function(cb) {
var _a, _c, _d, _e, _f, _h, _i, _j, _k, _l, _m, nums, x;
nums = [1, 2, 3];
(function(_b) {
_c = (function() {
_j = []; _l = nums;
for (_k = 0, _m = _l.length; _k < _m; _k++) {
_a = _l[_k];
_j.push(_a);
}
return _j;
})();
_d = [];
(function(_g) {
_h = function() {
if (!(_c.length > 0)) {
return _g(undefined); //break;
}
x = _c.shift();
return_arg(x + 1, function(_o) {
_f = _o;
_d.push(_f);
return _h(undefined); //continue;
return undefined;
});
};
return _h(undefined);
return _g(undefined);
})(function(_n) {
_n;
return _b(_d);
});
})(function(_i) {
return cb(_i);
});
};
return map_defer(function(_) {
equal(_.length, 3);
equal(_[0], 2);
equal(_[1], 3);
equal(_[2], 4);
return pass();
});
});
it('should support a deferred expression in attribute iteration', function(pass) {
var map_defer;
expect(1);
map_defer = function(cb) {
var _a, _b, _d, _e, _f, _g, _i, _j, _k, _l, _n, k, new_nums, nums, v;
nums = {
one: 1,
two: 2,
three: 3
};
new_nums = {};
(function(_c) {
_d = (function() {
_k = []; _l = nums;
for (_a in _l) { if (__hasProp.call(_l, _a)) {
k = _l[_a];
_k.push([_a, k]);
}}
return _k;
})();
_e = [];
(function(_h) {
_i = function() {
if (!(_d.length > 0)) {
return _h(undefined); //break;
}
_n = _d.shift();
k = _n[0];
v = _n[1];
return_arg(v + 1, function(_o) {
_g = (function() {
new_nums[k] = (_o);
return new_nums[k];
})();
_e.push(_g);
return _i(undefined); //continue;
return undefined;
});
};
return _i(undefined);
return _h(undefined);
})(function(_m) {
_m;
return _c(_e);
});
})(function(_j) {
_j;
return cb(new_nums);
});
};
return map_defer(function(_) {
equal(_.one, 2);
equal(_.two, 3);
equal(_.three, 4);
return pass();
});
});
// deconstruction and switching
it('should deconstruct arrays from deferred', function(pass) {
var deconstruct, multival;
expect(1);
multival = function(cb) {
return cb(1, 2);
};
deconstruct = function(cb) {
var _a;
multival(function(_a) {
var _b, x, y;
_b = arguments;
x = _b[0];
y = _b[1];
return cb(y);
});
};
return deconstruct(function(_) {
equal(_, 2, "expected failure");
return pass();
});
});
it('should deconstruct objects from deferred', function(pass) {
var deconstruct, obj;
expect(1);
obj = function(cb) {
return cb({
foo: 1,
bar: 2
});
};
deconstruct = function(cb) {
var _a;
obj(function(_a) {
var _b, result, secondary_result;
_b = _a;
result = _b.foo;
secondary_result = _b.bar;
return cb({
result: result,
secondary_result: secondary_result
});
});
};
return deconstruct(function(_) {
equal(_.result, 1);
equal(_.secondary_result, 2);
return pass();
});
});
// these ones cause compile errors at the moment :/
it('should switch on deferred', function(pass) {
var switch_on_deferred;
expect(3);
switch_on_deferred = function(input, cb) {
var _a, _c, _d, _g, _h, x;
(function(_b) {
return_arg(input, function(_e) {
_c = (_a = _e) === 1;
if (_c) {
return (function() {
return _b(cb(1));
})();
} else {
return (function() {
(function(_f) {
_g = _a === 2;
if (_g) {
return (function() {
return_arg(1, function(_i) {
x = _i;
return _b(cb(x + 1));
});
})();
} else {
return (function() {
return _b(cb(3));
})();
}
})(function(_h) {
_h;
return _b(undefined);
});
})();
}
return undefined;
});
})(function(_d) {
_d;
return undefined;
});
};
return switch_on_deferred(1, function(_) {
equal(_, 1);
pass();
return switch_on_deferred(2, function(_) {
equal(_, 2);
pass();
return switch_on_deferred(3, function(_) {
equal(_, 3);
return pass();
});
});
});
});
it('should allow defers in switch branches', function(pass) {
var switch_with_deferred_branches;
expect(3);
switch_with_deferred_branches = function(input, cb) {
var _b, _c, _f, _g;
(function(_a) {
return_arg(1, function(_d) {
_b = input === _d;
if (_b) {
return (function() {
return _a(cb(1));
})();
} else {
return (function() {
(function(_e) {
_f = input === 2;
if (_f) {
return (function() {
return_arg(2, function(_h) {
return _a(cb(_h));
});
})();
} else {
return (function() {
return _a(cb(3));
})();
}
})(function(_g) {
_g;
return _a(undefined);
});
})();
}
return undefined;
});
})(function(_c) {
_c;
return undefined;
});
};
return switch_with_deferred_branches(1, function(_) {
equal(_, 1);
pass();
return switch_with_deferred_branches(2, function(_) {
equal(_, 2);
pass();
return switch_with_deferred_branches(3, function(_) {
equal(_, 3);
return pass();
});
});
});
});
it('should not interfere with exception handling', function(pass) {
var error_handling;
expect(1);
error_handling = function(x, cb) {
var _a, _b, _c;
return_arg(1, function(_a) {
return return_arg(2, function(_b) {
return return_arg(3, function(_c) {
try {
_a;
_b;
_c;
throw ("uhoh! " + x);
return undefined;
} catch (err) {
return cb(err);
}
return undefined;
});
});
});
};
return error_handling("error!", function(_) {
equal(_, "uhoh! error!");
return pass();
});
});
it('should work with exception handling with deferred conditionals', function(pass) {
var error_handling2;
expect(3);
error_handling2 = function(x, num, cb) {
var _a, _b, _c, _d;
return_arg(1, function(_a) {
return return_arg(2, function(_b) {
return return_arg(3, function(_c) {
return return_arg(x, function(_d) {
var y;
try {
if (num === _a) {
throw ("" + x + " " + num);
}
if (num === (y = _b)) {
throw ("" + x + " " + num);
}
_c;
throw _d;
return undefined;
} catch (err) {
return cb(err);
}
return undefined;
});
});
});
});
};
return error_handling2("Another error!", 0, function(_) {
equal(_, "Another error!");
pass();
return error_handling2("Another error!", 1, function(_) {
equal(_, "Another error! 1");
pass();
return error_handling2("Another error!", 2, function(_) {
equal(_, "Another error! 2");
return pass();
});
});
});
});
it('should not interfere with `finally`', function(pass) {
var try_finally;
expect(2);
try_finally = function(x, msg, cb) {
var _a, _msg, in_continuable;
_msg = "Finally: ";
in_continuable = false;
return_arg(2, function(_a) {
try {
if (x === _a) {
throw msg;
}
in_continuable = true;
_msg = ("" + (_msg) + "didn't throw");
return _msg;
return undefined;
} catch (msg) {
in_continuable = true;
_msg = ("" + (_msg) + msg);
return _msg;
} finally {
if (in_continuable) {
cb(_msg);
}
}
return undefined;
});
};
return try_finally(1, "threw", function(_) {
equal(_, "Finally: didn't throw");
pass();
return try_finally(2, "threw", function(_) {
equal(_, "Finally: threw");
return pass();
});
});
});
return it('should work inside string interpolation', function(pass) {
var string_interpolation;
expect(1);
string_interpolation = function(str, cb) {
var _a;
return_arg(str, function(_a) {
return cb(("Hello " + (_a)));
});
};
return string_interpolation("John!", function(_) {
equal(_, "Hello John!");
return pass();
});
});
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment