Browse Source

Non-LIFO return error reified.

Amber cannot do non-LIFO non-local returns.
Herby Vojčík 3 years ago
parent
commit
04c7fa7508

+ 1 - 0
CHANGELOG

@@ -2,6 +2,7 @@
 ===================================
 
 * A model in `Promise new: [ :model | ... ]` reified as PromiseExecution.
+* Non-LIFO return represented by own Error subclass NonLifoReturn.
 
 Commits: https://lolg.it/amber/amber/commits/0.29.6.
 

+ 1 - 0
lang/API-CHANGES.txt

@@ -2,6 +2,7 @@
 
 * Add TPromiseModel with unary value / signal passing to 1-arg.
   * Promise class as well as PromiseExecution use it.
+* Add class NonLifoReturn.
 
 + Error class >>
   + messageText:

+ 98 - 0
lang/src/Kernel-Exceptions.js

@@ -881,4 +881,102 @@ return $recv($1)._signal();
 }; }),
 $globals.NonBooleanReceiver.a$cls);
 
+
+$core.addClass("NonLifoReturn", $globals.Error, "Kernel-Exceptions");
+$core.setSlots($globals.NonLifoReturn, ["value"]);
+$core.addMethod(
+$core.method({
+selector: "messageText",
+protocol: "accessing",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "messageText\x0a\x09^ 'Non-LIFO return: ', self value asString",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: [",", "asString", "value"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+return "Non-LIFO return: ".__comma($recv($self._value())._asString());
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"messageText",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.NonLifoReturn);
+
+$core.addMethod(
+$core.method({
+selector: "value",
+protocol: "accessing",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "value\x0a\x09^ value",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: []
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+return $self.value;
+
+}; }),
+$globals.NonLifoReturn);
+
+$core.addMethod(
+$core.method({
+selector: "value:",
+protocol: "accessing",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: ["anObject"],
+source: "value: anObject\x0a\x09value := anObject",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: []
+}, function ($methodClass){ return function (anObject){
+var self=this,$self=this;
+$self.value=anObject;
+return self;
+
+}; }),
+$globals.NonLifoReturn);
+
+
+$core.addMethod(
+$core.method({
+selector: "value:",
+protocol: "instance creation",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: ["anObject"],
+source: "value: anObject\x0a\x09^ super new\x0a\x09\x09value: anObject;\x0a\x09\x09yourself",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["value:", "new", "yourself"]
+}, function ($methodClass){ return function (anObject){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $1;
+$1=[(
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+$ctx1.supercall = true,
+//>>excludeEnd("ctx");
+($methodClass.superclass||$boot.nilAsClass).fn.prototype._new.call($self))
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.supercall = false
+//>>excludeEnd("ctx");
+][0];
+$recv($1)._value_(anObject);
+return $recv($1)._yourself();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"value:",{anObject:anObject})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.NonLifoReturn.a$cls);
+
 });

+ 26 - 0
lang/src/Kernel-Exceptions.st

@@ -242,3 +242,29 @@ signalOn: anObject
 		signal
 ! !
 
+Error subclass: #NonLifoReturn
+	slots: {#value}
+	package: 'Kernel-Exceptions'!
+
+!NonLifoReturn methodsFor: 'accessing'!
+
+messageText
+	^ 'Non-LIFO return: ', self value asString
+!
+
+value
+	^ value
+!
+
+value: anObject
+	value := anObject
+! !
+
+!NonLifoReturn class methodsFor: 'instance creation'!
+
+value: anObject
+	^ super new
+		value: anObject;
+		yourself
+! !
+

+ 31 - 3
lang/src/Kernel-Infrastructure.js

@@ -3087,11 +3087,11 @@ selector: "asSmalltalkException:",
 protocol: "error handling",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: ["anObject"],
-source: "asSmalltalkException: anObject\x0a\x09\x22A JavaScript exception may be thrown.\x0a\x09We then need to convert it back to a Smalltalk object\x22\x0a\x09\x0a\x09^ anObject\x0a\x09\x09ifNil: [ [ self error: 'Error: nil' ] on: Error do: [ :e | e ] ]\x0a\x09\x09ifNotNil: [\x0a\x09\x09\x09(self isError: anObject)\x0a\x09\x09\x09\x09ifTrue: [ anObject ]\x0a\x09\x09\x09\x09ifFalse: [ JavaScriptException on: anObject ] ]",
-referencedClasses: ["Error", "JavaScriptException"],
+source: "asSmalltalkException: anObject\x0a\x09\x22A JavaScript exception may be thrown.\x0a\x09We then need to convert it back to a Smalltalk object\x22\x0a\x09\x0a\x09^ anObject\x0a\x09\x09ifNil: [ [ self error: 'Error: nil' ] on: Error do: [ :e | e ] ]\x0a\x09\x09ifNotNil: [\x0a\x09\x09\x09(self isError: anObject)\x0a\x09\x09\x09\x09ifTrue: [ anObject ]\x0a\x09\x09\x09\x09ifFalse: [\x0a\x09\x09\x09\x09\x09(self isNonLocalReturn: anObject)\x0a\x09\x09\x09\x09\x09\x09ifTrue: [ NonLifoReturn value: anObject first ]\x0a\x09\x09\x09\x09\x09\x09ifFalse: [ JavaScriptException on: anObject ] ] ]",
+referencedClasses: ["Error", "NonLifoReturn", "JavaScriptException"],
 //>>excludeEnd("ide");
 pragmas: [],
-messageSends: ["ifNil:ifNotNil:", "on:do:", "error:", "ifTrue:ifFalse:", "isError:", "on:"]
+messageSends: ["ifNil:ifNotNil:", "on:do:", "error:", "ifTrue:ifFalse:", "isError:", "isNonLocalReturn:", "value:", "first", "on:"]
 }, function ($methodClass){ return function (anObject){
 var self=this,$self=this;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
@@ -3114,9 +3114,13 @@ return e;
 if($core.assert($self._isError_(anObject))){
 return anObject;
 } else {
+if($core.assert($self._isNonLocalReturn_(anObject))){
+return $recv($globals.NonLifoReturn)._value_($recv(anObject)._first());
+} else {
 return $recv($globals.JavaScriptException)._on_(anObject);
 }
 }
+}
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 }, function($ctx1) {$ctx1.fill(self,"asSmalltalkException:",{anObject:anObject})});
 //>>excludeEnd("ctx");
@@ -3481,6 +3485,30 @@ return false;
 }; }),
 $globals.SmalltalkImage);
 
+$core.addMethod(
+$core.method({
+selector: "isNonLocalReturn:",
+protocol: "testing",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: ["anObject"],
+source: "isNonLocalReturn: anObject\x0a\x09<inlineJS: 'return Array.isArray(anObject) && anObject.length === 1'>",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [["inlineJS:", ["return Array.isArray(anObject) && anObject.length === 1"]]],
+messageSends: []
+}, function ($methodClass){ return function (anObject){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+return Array.isArray(anObject) && anObject.length === 1;
+return self;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"isNonLocalReturn:",{anObject:anObject})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SmalltalkImage);
+
 $core.addMethod(
 $core.method({
 selector: "isSmalltalkObject:",

+ 8 - 1
lang/src/Kernel-Infrastructure.st

@@ -942,7 +942,10 @@ asSmalltalkException: anObject
 		ifNotNil: [
 			(self isError: anObject)
 				ifTrue: [ anObject ]
-				ifFalse: [ JavaScriptException on: anObject ] ]
+				ifFalse: [
+					(self isNonLocalReturn: anObject)
+						ifTrue: [ NonLifoReturn value: anObject first ]
+						ifFalse: [ JavaScriptException on: anObject ] ] ]
 !
 
 try: actionBlock ifTrue: aBlock catch: anotherBlock
@@ -1107,6 +1110,10 @@ isError: anObject
 	^ (self isSmalltalkObject: anObject) and: [ anObject isError ]
 !
 
+isNonLocalReturn: anObject
+	<inlineJS: 'return Array.isArray(anObject) && anObject.length === 1'>
+!
+
 isSmalltalkObject: anObject
 	"Consider anObject a Smalltalk object if it has a 'a$cls' property.
 	Note that this may be unaccurate"

+ 13 - 11
lang/src/Kernel-Promises.js

@@ -145,10 +145,10 @@ selector: "new:",
 protocol: "instance creation",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: ["aBlock"],
-source: "new: aBlock\x0a\x22Returns a Promise that is eventually resolved or rejected.\x0aPass a block that is called with one argument, model.\x0aYou should call model value: ... to resolve the promise\x0aand model signal: ... to reject the promise.\x0aIf error happens during run of the block,\x0apromise is rejected with that error as well.\x22\x0a<inlineJS: '\x0a\x09var localReturn = null,\x0a\x09\x09promise = new Promise(function (resolve, reject) {\x0a\x09\x09    var model = $globals.PromiseExecution._resolveBlock_rejectBlock_(resolve, reject);\x0a\x09\x09    try { aBlock._value_(model); }\x0a\x09\x09\x09catch (ex) {\x0a\x09\x09\x09\x09if (Array.isArray(ex) && ex.length === 1) localReturn = ex;\x0a\x09\x09\x09\x09else reject(ex);\x0a\x09\x09\x09}\x0a\x09\x09});\x0a\x09if (localReturn) throw localReturn; else return promise;\x0a'>",
+source: "new: aBlock\x0a\x22Returns a Promise that is eventually resolved or rejected.\x0aPass a block that is called with one argument, model.\x0aYou should call model value: ... to resolve the promise\x0aand model signal: ... to reject the promise.\x0aIf error happens during run of the block,\x0apromise is rejected with that error as well.\x22\x0a<inlineJS: '\x0a\x09var nonLocalReturn = null,\x0a\x09\x09promise = new Promise(function (resolve, reject) {\x0a\x09\x09    var model = $globals.PromiseExecution._resolveBlock_rejectBlock_(resolve, reject);\x0a\x09\x09    try { aBlock._value_(model); }\x0a\x09\x09\x09catch (ex) {\x0a\x09\x09\x09\x09if (Array.isArray(ex) && ex.length === 1) nonLocalReturn = ex;\x0a\x09\x09\x09\x09else reject(ex);\x0a\x09\x09\x09}\x0a\x09\x09});\x0a\x09if (nonLocalReturn) throw nonLocalReturn; else return promise;\x0a'>",
 referencedClasses: [],
 //>>excludeEnd("ide");
-pragmas: [["inlineJS:", ["\x0a\x09var localReturn = null,\x0a\x09\x09promise = new Promise(function (resolve, reject) {\x0a\x09\x09    var model = $globals.PromiseExecution._resolveBlock_rejectBlock_(resolve, reject);\x0a\x09\x09    try { aBlock._value_(model); }\x0a\x09\x09\x09catch (ex) {\x0a\x09\x09\x09\x09if (Array.isArray(ex) && ex.length === 1) localReturn = ex;\x0a\x09\x09\x09\x09else reject(ex);\x0a\x09\x09\x09}\x0a\x09\x09});\x0a\x09if (localReturn) throw localReturn; else return promise;"]]],
+pragmas: [["inlineJS:", ["\x0a\x09var nonLocalReturn = null,\x0a\x09\x09promise = new Promise(function (resolve, reject) {\x0a\x09\x09    var model = $globals.PromiseExecution._resolveBlock_rejectBlock_(resolve, reject);\x0a\x09\x09    try { aBlock._value_(model); }\x0a\x09\x09\x09catch (ex) {\x0a\x09\x09\x09\x09if (Array.isArray(ex) && ex.length === 1) nonLocalReturn = ex;\x0a\x09\x09\x09\x09else reject(ex);\x0a\x09\x09\x09}\x0a\x09\x09});\x0a\x09if (nonLocalReturn) throw nonLocalReturn; else return promise;"]]],
 messageSends: []
 }, function ($methodClass){ return function (aBlock){
 var self=this,$self=this;
@@ -156,16 +156,16 @@ var self=this,$self=this;
 return $core.withContext(function($ctx1) {
 //>>excludeEnd("ctx");
 
-	var localReturn = null,
+	var nonLocalReturn = null,
 		promise = new Promise(function (resolve, reject) {
 		    var model = $globals.PromiseExecution._resolveBlock_rejectBlock_(resolve, reject);
 		    try { aBlock._value_(model); }
 			catch (ex) {
-				if (Array.isArray(ex) && ex.length === 1) localReturn = ex;
+				if (Array.isArray(ex) && ex.length === 1) nonLocalReturn = ex;
 				else reject(ex);
 			}
 		});
-	if (localReturn) throw localReturn; else return promise;;
+	if (nonLocalReturn) throw nonLocalReturn; else return promise;;
 return self;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 }, function($ctx1) {$ctx1.fill(self,"new:",{aBlock:aBlock})});
@@ -230,7 +230,7 @@ selector: "do:",
 protocol: "evaluating",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: ["aBlock"],
-source: "do: aBlock\x0a\x09self value: (self try: aBlock)",
+source: "do: aBlock\x0a\x09\x22Executes a block 'in the context of a promise' and resolves.\x0a\x09That is, if it ends with an error, promise is rejected.\x0a\x09If a block succeeds, promise is resolved with its return value.\x0a\x09Non-local returns are also treated as an error and reified as rejections.\x22\x0a\x09self value: (self try: aBlock)",
 referencedClasses: [],
 //>>excludeEnd("ide");
 pragmas: [],
@@ -298,10 +298,10 @@ selector: "try:",
 protocol: "evaluating",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: ["aBlock"],
-source: "try: aBlock\x0a\x09<inlineJS: '\x0a\x09\x09try {\x0a\x09\x09\x09return aBlock._value();\x0a\x09\x09} catch(error) {\x0a\x09\x09\x09// pass non-local returns undetected\x0a\x09\x09\x09if (Array.isArray(error) && error.length === 1) throw error;\x0a\x09\x09\x09self._signal_(error);\x0a\x09\x09}\x0a\x09'>",
+source: "try: aBlock\x0a\x09\x22Executes a block 'in the context of a promise'.\x0a\x09That is, if it ends with an error, promise is rejected.\x0a\x09Non-local returns are also treated as an error and reified as rejections.\x22\x0a\x09<inlineJS: '\x0a\x09\x09try {\x0a\x09\x09\x09return aBlock._value();\x0a\x09\x09} catch(error) {\x0a\x09\x09\x09$self._signal_(\x0a\x09\x09\x09\x09Array.isArray(error) && error.length === 1 ?\x0a\x09\x09\x09\x09\x09$globals.NonLifoReturn._value_(error[0]) :\x0a\x09\x09\x09\x09\x09error\x0a\x09\x09\x09);\x0a\x09\x09}\x0a\x09'>",
 referencedClasses: [],
 //>>excludeEnd("ide");
-pragmas: [["inlineJS:", ["\x0a\x09\x09try {\x0a\x09\x09\x09return aBlock._value();\x0a\x09\x09} catch(error) {\x0a\x09\x09\x09// pass non-local returns undetected\x0a\x09\x09\x09if (Array.isArray(error) && error.length === 1) throw error;\x0a\x09\x09\x09self._signal_(error);\x0a\x09\x09}\x0a\x09"]]],
+pragmas: [["inlineJS:", ["\x0a\x09\x09try {\x0a\x09\x09\x09return aBlock._value();\x0a\x09\x09} catch(error) {\x0a\x09\x09\x09$self._signal_(\x0a\x09\x09\x09\x09Array.isArray(error) && error.length === 1 ?\x0a\x09\x09\x09\x09\x09$globals.NonLifoReturn._value_(error[0]) :\x0a\x09\x09\x09\x09\x09error\x0a\x09\x09\x09);\x0a\x09\x09}\x0a\x09"]]],
 messageSends: []
 }, function ($methodClass){ return function (aBlock){
 var self=this,$self=this;
@@ -312,9 +312,11 @@ return $core.withContext(function($ctx1) {
 		try {
 			return aBlock._value();
 		} catch(error) {
-			// pass non-local returns undetected
-			if (Array.isArray(error) && error.length === 1) throw error;
-			self._signal_(error);
+			$self._signal_(
+				Array.isArray(error) && error.length === 1 ?
+					$globals.NonLifoReturn._value_(error[0]) :
+					error
+			);
 		}
 	;
 return self;

+ 15 - 6
lang/src/Kernel-Promises.st

@@ -40,16 +40,16 @@ and model signal: ... to reject the promise.
 If error happens during run of the block,
 promise is rejected with that error as well."
 <inlineJS: '
-	var localReturn = null,
+	var nonLocalReturn = null,
 		promise = new Promise(function (resolve, reject) {
 		    var model = $globals.PromiseExecution._resolveBlock_rejectBlock_(resolve, reject);
 		    try { aBlock._value_(model); }
 			catch (ex) {
-				if (Array.isArray(ex) && ex.length === 1) localReturn = ex;
+				if (Array.isArray(ex) && ex.length === 1) nonLocalReturn = ex;
 				else reject(ex);
 			}
 		});
-	if (localReturn) throw localReturn; else return promise;
+	if (nonLocalReturn) throw nonLocalReturn; else return promise;
 '>
 !
 
@@ -77,17 +77,26 @@ resolveBlock: aBlock rejectBlock: anotherBlock
 !PromiseExecution methodsFor: 'evaluating'!
 
 do: aBlock
+	"Executes a block 'in the context of a promise' and resolves.
+	That is, if it ends with an error, promise is rejected.
+	If a block succeeds, promise is resolved with its return value.
+	Non-local returns are also treated as an error and reified as rejections."
 	self value: (self try: aBlock)
 !
 
 try: aBlock
+	"Executes a block 'in the context of a promise'.
+	That is, if it ends with an error, promise is rejected.
+	Non-local returns are also treated as an error and reified as rejections."
 	<inlineJS: '
 		try {
 			return aBlock._value();
 		} catch(error) {
-			// pass non-local returns undetected
-			if (Array.isArray(error) && error.length === 1) throw error;
-			self._signal_(error);
+			$self._signal_(
+				Array.isArray(error) && error.length === 1 ?
+					$globals.NonLifoReturn._value_(error[0]) :
+					error
+			);
 		}
 	'>
 ! !

+ 236 - 0
lang/src/Kernel-Tests.js

@@ -15672,6 +15672,69 @@ $globals.PointTest);
 
 
 $core.addClass("PromiseTest", $globals.TestCase, "Kernel-Tests");
+$core.addMethod(
+$core.method({
+selector: "testPromiseExecutorAsyncDoWithNonLocalReturn",
+protocol: " tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testPromiseExecutorAsyncDoWithNonLocalReturn\x0a\x09self timeout: 40.\x0a\x09^ (Promise new: [ :m | [ m do: [ ^  'Intentional' ] ] fork ])\x0a\x09\x09then: [ self assert: false description: 'Should not have been resolved' ]\x0a\x09\x09on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]",
+referencedClasses: ["Promise", "NonLifoReturn"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "then:on:do:", "new:", "fork", "do:", "assert:description:", "assert:equals:", "value"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $early={};
+try {
+$self._timeout_((40));
+return $recv($recv($globals.Promise)._new_((function(m){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+return $recv(m)._do_((function(){
+throw $early=["Intentional"];
+
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({},$ctx2,2)});
+//>>excludeEnd("ctx");
+}))._fork();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({m:m},$ctx1,1)});
+//>>excludeEnd("ctx");
+})))._then_on_do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_description_(false,"Should not have been resolved");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,4)});
+//>>excludeEnd("ctx");
+}),$globals.NonLifoReturn,(function(nonlifo){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_equals_($recv(nonlifo)._value(),"Intentional");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({nonlifo:nonlifo},$ctx1,5)});
+//>>excludeEnd("ctx");
+}));
+}
+catch(e) {if(e===$early)return e[0]; throw e}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testPromiseExecutorAsyncDoWithNonLocalReturn",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.PromiseTest);
+
 $core.addMethod(
 $core.method({
 selector: "testPromiseExecutorAsyncNegativeDo",
@@ -15938,6 +16001,124 @@ return $self._assert_equals_(result,"timeout");
 }; }),
 $globals.PromiseTest);
 
+$core.addMethod(
+$core.method({
+selector: "testPromiseExecutorAsyncTryWithNonLocalReturn",
+protocol: " tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testPromiseExecutorAsyncTryWithNonLocalReturn\x0a\x09self timeout: 40.\x0a\x09^ (Promise new: [ :m | [ m try: [ ^  'Intentional' ] ] fork ])\x0a\x09\x09then: [ self assert: false description: 'Should not have been resolved' ]\x0a\x09\x09on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]",
+referencedClasses: ["Promise", "NonLifoReturn"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "then:on:do:", "new:", "fork", "try:", "assert:description:", "assert:equals:", "value"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $early={};
+try {
+$self._timeout_((40));
+return $recv($recv($globals.Promise)._new_((function(m){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+return $recv(m)._try_((function(){
+throw $early=["Intentional"];
+
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({},$ctx2,2)});
+//>>excludeEnd("ctx");
+}))._fork();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({m:m},$ctx1,1)});
+//>>excludeEnd("ctx");
+})))._then_on_do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_description_(false,"Should not have been resolved");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,4)});
+//>>excludeEnd("ctx");
+}),$globals.NonLifoReturn,(function(nonlifo){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_equals_($recv(nonlifo)._value(),"Intentional");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({nonlifo:nonlifo},$ctx1,5)});
+//>>excludeEnd("ctx");
+}));
+}
+catch(e) {if(e===$early)return e[0]; throw e}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testPromiseExecutorAsyncTryWithNonLocalReturn",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.PromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "testPromiseExecutorDoWithNonLocalReturn",
+protocol: " tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testPromiseExecutorDoWithNonLocalReturn\x0a\x09self timeout: 20.\x0a\x09^ (Promise new: [ :m | m do: [ ^ 'Intentional' ] ])\x0a\x09\x09then: [ self assert: false description: 'Should not have been resolved' ]\x0a\x09\x09on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]",
+referencedClasses: ["Promise", "NonLifoReturn"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "then:on:do:", "new:", "do:", "assert:description:", "assert:equals:", "value"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $early={};
+try {
+$self._timeout_((20));
+return $recv($recv($globals.Promise)._new_((function(m){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv(m)._do_((function(){
+throw $early=["Intentional"];
+
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({m:m},$ctx1,1)});
+//>>excludeEnd("ctx");
+})))._then_on_do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_description_(false,"Should not have been resolved");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,3)});
+//>>excludeEnd("ctx");
+}),$globals.NonLifoReturn,(function(nonlifo){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_equals_($recv(nonlifo)._value(),"Intentional");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({nonlifo:nonlifo},$ctx1,4)});
+//>>excludeEnd("ctx");
+}));
+}
+catch(e) {if(e===$early)return e[0]; throw e}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testPromiseExecutorDoWithNonLocalReturn",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.PromiseTest);
+
 $core.addMethod(
 $core.method({
 selector: "testPromiseExecutorNegativeDo",
@@ -16180,6 +16361,61 @@ return $self._assert_equals_(result,"timeout");
 }; }),
 $globals.PromiseTest);
 
+$core.addMethod(
+$core.method({
+selector: "testPromiseExecutorTryWithNonLocalReturn",
+protocol: " tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testPromiseExecutorTryWithNonLocalReturn\x0a\x09self timeout: 20.\x0a\x09^ (Promise new: [ :m | m try: [ ^ 'Intentional' ] ])\x0a\x09\x09then: [ self assert: false description: 'Should not have been resolved' ]\x0a\x09\x09on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]",
+referencedClasses: ["Promise", "NonLifoReturn"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "then:on:do:", "new:", "try:", "assert:description:", "assert:equals:", "value"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $early={};
+try {
+$self._timeout_((20));
+return $recv($recv($globals.Promise)._new_((function(m){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv(m)._try_((function(){
+throw $early=["Intentional"];
+
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({m:m},$ctx1,1)});
+//>>excludeEnd("ctx");
+})))._then_on_do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_description_(false,"Should not have been resolved");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,3)});
+//>>excludeEnd("ctx");
+}),$globals.NonLifoReturn,(function(nonlifo){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._assert_equals_($recv(nonlifo)._value(),"Intentional");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({nonlifo:nonlifo},$ctx1,4)});
+//>>excludeEnd("ctx");
+}));
+}
+catch(e) {if(e===$early)return e[0]; throw e}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testPromiseExecutorTryWithNonLocalReturn",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.PromiseTest);
+
 $core.addMethod(
 $core.method({
 selector: "testPromiseNew",

+ 28 - 0
lang/src/Kernel-Tests.st

@@ -2786,6 +2786,13 @@ TestCase subclass: #PromiseTest
 
 !PromiseTest methodsFor: ' tests'!
 
+testPromiseExecutorAsyncDoWithNonLocalReturn
+	self timeout: 40.
+	^ (Promise new: [ :m | [ m do: [ ^  'Intentional' ] ] fork ])
+		then: [ self assert: false description: 'Should not have been resolved' ]
+		on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]
+!
+
 testPromiseExecutorAsyncNegativeDo
 	self timeout: 40.
 	^ (Promise new: [ :m | [ m do: [ self error: 'Intentional' ] ] fork ])
@@ -2815,6 +2822,20 @@ testPromiseExecutorAsyncPositiveTry
 	}) then: [ :result | self assert: result equals: #timeout ].
 !
 
+testPromiseExecutorAsyncTryWithNonLocalReturn
+	self timeout: 40.
+	^ (Promise new: [ :m | [ m try: [ ^  'Intentional' ] ] fork ])
+		then: [ self assert: false description: 'Should not have been resolved' ]
+		on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]
+!
+
+testPromiseExecutorDoWithNonLocalReturn
+	self timeout: 20.
+	^ (Promise new: [ :m | m do: [ ^ 'Intentional' ] ])
+		then: [ self assert: false description: 'Should not have been resolved' ]
+		on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]
+!
+
 testPromiseExecutorNegativeDo
 	self timeout: 40.
 	^ (Promise new: [ :m | [ m do: [ self error: 'Intentional' ] ] fork ])
@@ -2844,6 +2865,13 @@ testPromiseExecutorPositiveTry
 	}) then: [ :result | self assert: result equals: #timeout ].
 !
 
+testPromiseExecutorTryWithNonLocalReturn
+	self timeout: 20.
+	^ (Promise new: [ :m | m try: [ ^ 'Intentional' ] ])
+		then: [ self assert: false description: 'Should not have been resolved' ]
+		on: NonLifoReturn do: [ :nonlifo | self assert: nonlifo value equals: 'Intentional' ]
+!
+
 testPromiseNew
 	self timeout: 20.
 	^ Promise new

+ 257 - 33
lang/src/SUnit-Tests.js

@@ -450,6 +450,40 @@ return self;
 }; }),
 $globals.SUnitAsyncTest);
 
+$core.addMethod(
+$core.method({
+selector: "fakeNonLifoReturn",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeNonLifoReturn\x0a\x09flag := 'bad'.\x0a\x09self timeout: 30.\x0a\x09flag := (self async: [ flag := 'ok'. ^ 'non-lifo' ]) valueWithTimeout: 20",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "valueWithTimeout:", "async:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $early={};
+try {
+$self.flag="bad";
+$self._timeout_((30));
+$self.flag=$recv($self._async_((function(){
+$self.flag="ok";
+throw $early=["non-lifo"];
+
+})))._valueWithTimeout_((20));
+return self;
+}
+catch(e) {if(e===$early)return e[0]; throw e}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeNonLifoReturn",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitAsyncTest);
+
 $core.addMethod(
 $core.method({
 selector: "fakeTimeout",
@@ -773,6 +807,81 @@ return self;
 }; }),
 $globals.SUnitAsyncTest);
 
+$core.addMethod(
+$core.method({
+selector: "testNonLifo",
+protocol: "tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testNonLifo\x0a\x09| suite runner result assertBlock |\x0a\x09suite := #(fakeNonLifoReturn testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09assertBlock := self async: [\x0a\x09\x09self assert: (self selectorSetOf: result errors) equals: #(fakeNonLifoReturn) asSet.\x0a\x09\x09self assert: (self selectorSetOf: result failures) equals: Set new.\x0a\x09\x09\x22TODO check that error is indeed a correct NonLifoReturn\x22\x0a\x09\x09self finished\x0a\x09].\x0a\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: assertBlock ].\x0a\x09runner run",
+referencedClasses: ["TestSuiteRunner", "Set", "ResultAnnouncement"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "async:", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "new", "finished", "on:do:", "announcer", "ifTrue:", "and:", "==", "=", "runs", "total", "run"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+var suite,runner,result,assertBlock;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $1;
+suite=["fakeNonLifoReturn", "testPass"]._collect_((function(each){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv($self._class())._selector_(each);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+runner=$recv($globals.TestSuiteRunner)._on_(suite);
+$self._timeout_((200));
+result=[$recv(runner)._result()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["result"]=1
+//>>excludeEnd("ctx");
+][0];
+assertBlock=$self._async_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+[$self._assert_equals_([$self._selectorSetOf_($recv(result)._errors())
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx2.sendIdx["selectorSetOf:"]=1
+//>>excludeEnd("ctx");
+][0],["fakeNonLifoReturn"]._asSet())
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx2.sendIdx["assert:equals:"]=1
+//>>excludeEnd("ctx");
+][0];
+$self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),$recv($globals.Set)._new());
+return $self._finished();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,2)});
+//>>excludeEnd("ctx");
+}));
+$recv($recv(runner)._announcer())._on_do_($globals.ResultAnnouncement,(function(ann){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+if($core.assert($recv($recv(ann)._result()).__eq_eq(result))){
+$1=$recv($recv(result)._runs()).__eq($recv(result)._total());
+} else {
+$1=false;
+}
+return $recv($1)._ifTrue_(assertBlock);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({ann:ann},$ctx1,3)});
+//>>excludeEnd("ctx");
+}));
+$recv(runner)._run();
+return self;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testNonLifo",{suite:suite,runner:runner,result:result,assertBlock:assertBlock})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitAsyncTest);
+
 $core.addMethod(
 $core.method({
 selector: "testPass",
@@ -1146,6 +1255,40 @@ return $recv($globals.Promise)._delayMilliseconds_((20));
 }; }),
 $globals.SUnitPromiseTest);
 
+$core.addMethod(
+$core.method({
+selector: "fakeNonLifoReturn",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeNonLifoReturn\x0a\x09flag := 'bad'.\x0a\x09self timeout: 30.\x0a\x09flag := Promise delayMilliseconds: 20.\x0a\x09^ flag then: [ flag := 'ok'. ^ 'non-lifo' ]",
+referencedClasses: ["Promise"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "delayMilliseconds:", "then:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $early={};
+try {
+$self.flag="bad";
+$self._timeout_((30));
+$self.flag=$recv($globals.Promise)._delayMilliseconds_((20));
+return $recv($self.flag)._then_((function(){
+$self.flag="ok";
+throw $early=["non-lifo"];
+
+}));
+}
+catch(e) {if(e===$early)return e[0]; throw e}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeNonLifoReturn",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
 $core.addMethod(
 $core.method({
 selector: "fakePromiseWithoutTimeout",
@@ -1321,6 +1464,88 @@ return $recv($globals.Promise)._new();
 }; }),
 $globals.SUnitPromiseTest);
 
+$core.addMethod(
+$core.method({
+selector: "testNonLifo",
+protocol: "tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testNonLifo\x0a\x09| suite runner result |\x0a\x09suite := #(fakeNonLifoReturn testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [ model do: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #(fakeNonLifoReturn) asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: Set new.\x0a\x09\x09\x09\x09\x22TODO check that error is indeed a correct NonLifoReturn\x22\x0a\x09\x09] ] ].\x0a\x09\x09runner run ]",
+referencedClasses: ["TestSuiteRunner", "Promise", "ResultAnnouncement", "Set"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "ifTrue:", "and:", "==", "=", "runs", "total", "do:", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "new", "run"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+var suite,runner,result;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $1;
+suite=["fakeNonLifoReturn", "testPass"]._collect_((function(each){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv($self._class())._selector_(each);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+runner=$recv($globals.TestSuiteRunner)._on_(suite);
+$self._timeout_((200));
+result=[$recv(runner)._result()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["result"]=1
+//>>excludeEnd("ctx");
+][0];
+return $recv($globals.Promise)._new_((function(model){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$recv($recv(runner)._announcer())._on_do_($globals.ResultAnnouncement,(function(ann){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+if($core.assert($recv($recv(ann)._result()).__eq_eq(result))){
+$1=$recv($recv(result)._runs()).__eq($recv(result)._total());
+} else {
+$1=false;
+}
+if($core.assert($1)){
+return $recv(model)._do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx4) {
+//>>excludeEnd("ctx");
+[$self._assert_equals_([$self._selectorSetOf_($recv(result)._errors())
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx4.sendIdx["selectorSetOf:"]=1
+//>>excludeEnd("ctx");
+][0],["fakeNonLifoReturn"]._asSet())
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx4.sendIdx["assert:equals:"]=1
+//>>excludeEnd("ctx");
+][0];
+return $self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),$recv($globals.Set)._new());
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx4) {$ctx4.fillBlock({},$ctx3,6)});
+//>>excludeEnd("ctx");
+}));
+}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({ann:ann},$ctx2,3)});
+//>>excludeEnd("ctx");
+}));
+return $recv(runner)._run();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({model:model},$ctx1,2)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testNonLifo",{suite:suite,runner:runner,result:result})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
 $core.addMethod(
 $core.method({
 selector: "testPass",
@@ -1363,11 +1588,11 @@ selector: "testPromiseErrorsAndFailures",
 protocol: "tests",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: [],
-source: "testPromiseErrorsAndFailures\x0a\x09| suite runner result |\x0a\x09suite := #(fakeError fakeErrorFailingInTearDown fakeFailure testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #(fakeError) asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: #(fakeErrorFailingInTearDown fakeFailure) asSet.\x0a\x09\x09\x09\x09model value: nil ] ].\x0a\x09\x09runner run ]",
+source: "testPromiseErrorsAndFailures\x0a\x09| suite runner result |\x0a\x09suite := #(fakeError fakeErrorFailingInTearDown fakeFailure testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [ model do: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #(fakeError) asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: #(fakeErrorFailingInTearDown fakeFailure) asSet ] ] ].\x0a\x09\x09runner run ]",
 referencedClasses: ["TestSuiteRunner", "Promise", "ResultAnnouncement"],
 //>>excludeEnd("ide");
 pragmas: [],
-messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "ifTrue:", "and:", "==", "=", "runs", "total", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "value:", "run"]
+messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "ifTrue:", "and:", "==", "=", "runs", "total", "do:", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "run"]
 }, function ($methodClass){ return function (){
 var self=this,$self=this;
 var suite,runner,result;
@@ -1405,21 +1630,28 @@ $1=$recv($recv(result)._runs()).__eq($recv(result)._total());
 $1=false;
 }
 if($core.assert($1)){
+return $recv(model)._do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx4) {
+//>>excludeEnd("ctx");
 [$self._assert_equals_([$self._selectorSetOf_($recv(result)._errors())
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["selectorSetOf:"]=1
+,$ctx4.sendIdx["selectorSetOf:"]=1
 //>>excludeEnd("ctx");
 ][0],[["fakeError"]._asSet()
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["asSet"]=1
+,$ctx4.sendIdx["asSet"]=1
 //>>excludeEnd("ctx");
 ][0])
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["assert:equals:"]=1
+,$ctx4.sendIdx["assert:equals:"]=1
 //>>excludeEnd("ctx");
 ][0];
-$self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),["fakeErrorFailingInTearDown", "fakeFailure"]._asSet());
-return $recv(model)._value_(nil);
+return $self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),["fakeErrorFailingInTearDown", "fakeFailure"]._asSet());
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx4) {$ctx4.fillBlock({},$ctx3,6)});
+//>>excludeEnd("ctx");
+}));
 }
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 }, function($ctx3) {$ctx3.fillBlock({ann:ann},$ctx2,3)});
@@ -1442,18 +1674,18 @@ selector: "testTimeouts",
 protocol: "tests",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: [],
-source: "testTimeouts\x0a\x09| suite runner result |\x0a\x09suite := #(fakeTimeout fakeMultipleTimeoutFailing fakeMultipleTimeoutPassing fakeTimeoutSendOnly fakePromiseWithoutTimeout testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09console log: ann; log: ann result runs.\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #() asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: #(fakeMultipleTimeoutFailing fakeTimeout fakeTimeoutSendOnly fakePromiseWithoutTimeout) asSet.\x0a\x09\x09\x09\x09model value: nil ] ].\x0a\x09\x09runner run ]",
+source: "testTimeouts\x0a\x09| suite runner result |\x0a\x09suite := #(fakeTimeout fakeMultipleTimeoutFailing fakeMultipleTimeoutPassing fakeTimeoutSendOnly fakePromiseWithoutTimeout testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [ model do: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #() asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: #(fakeMultipleTimeoutFailing fakeTimeout fakeTimeoutSendOnly fakePromiseWithoutTimeout) asSet ] ] ].\x0a\x09\x09runner run ]",
 referencedClasses: ["TestSuiteRunner", "Promise", "ResultAnnouncement"],
 //>>excludeEnd("ide");
 pragmas: [],
-messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "log:", "runs", "ifTrue:", "and:", "==", "=", "total", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "value:", "run"]
+messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "ifTrue:", "and:", "==", "=", "runs", "total", "do:", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "run"]
 }, function ($methodClass){ return function (){
 var self=this,$self=this;
 var suite,runner,result;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx1) {
 //>>excludeEnd("ctx");
-var $1,$2;
+var $1;
 suite=["fakeTimeout", "fakeMultipleTimeoutFailing", "fakeMultipleTimeoutPassing", "fakeTimeoutSendOnly", "fakePromiseWithoutTimeout", "testPass"]._collect_((function(each){
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx2) {
@@ -1478,42 +1710,34 @@ $recv($recv(runner)._announcer())._on_do_($globals.ResultAnnouncement,(function(
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx3) {
 //>>excludeEnd("ctx");
-$1=console;
-[$recv($1)._log_(ann)
-//>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["log:"]=1
-//>>excludeEnd("ctx");
-][0];
-$recv($1)._log_([$recv([$recv(ann)._result()
-//>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["result"]=2
-//>>excludeEnd("ctx");
-][0])._runs()
-//>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["runs"]=1
-//>>excludeEnd("ctx");
-][0]);
 if($core.assert($recv($recv(ann)._result()).__eq_eq(result))){
-$2=$recv($recv(result)._runs()).__eq($recv(result)._total());
+$1=$recv($recv(result)._runs()).__eq($recv(result)._total());
 } else {
-$2=false;
+$1=false;
 }
-if($core.assert($2)){
+if($core.assert($1)){
+return $recv(model)._do_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx4) {
+//>>excludeEnd("ctx");
 [$self._assert_equals_([$self._selectorSetOf_($recv(result)._errors())
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["selectorSetOf:"]=1
+,$ctx4.sendIdx["selectorSetOf:"]=1
 //>>excludeEnd("ctx");
 ][0],[[]._asSet()
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["asSet"]=1
+,$ctx4.sendIdx["asSet"]=1
 //>>excludeEnd("ctx");
 ][0])
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx3.sendIdx["assert:equals:"]=1
+,$ctx4.sendIdx["assert:equals:"]=1
 //>>excludeEnd("ctx");
 ][0];
-$self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),["fakeMultipleTimeoutFailing", "fakeTimeout", "fakeTimeoutSendOnly", "fakePromiseWithoutTimeout"]._asSet());
-return $recv(model)._value_(nil);
+return $self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),["fakeMultipleTimeoutFailing", "fakeTimeout", "fakeTimeoutSendOnly", "fakePromiseWithoutTimeout"]._asSet());
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx4) {$ctx4.fillBlock({},$ctx3,6)});
+//>>excludeEnd("ctx");
+}));
 }
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 }, function($ctx3) {$ctx3.fillBlock({ann:ann},$ctx2,3)});

+ 50 - 7
lang/src/SUnit-Tests.st

@@ -93,6 +93,12 @@ fakeMultipleTimeoutPassing
 	]) valueWithTimeout: 10
 !
 
+fakeNonLifoReturn
+	flag := 'bad'.
+	self timeout: 30.
+	flag := (self async: [ flag := 'ok'. ^ 'non-lifo' ]) valueWithTimeout: 20
+!
+
 fakeTimeout
 	self timeout: 10.
 	(self async: [ self finished ]) valueWithTimeout: 20
@@ -153,6 +159,23 @@ testIsAsyncReturnsCorrectValues
 	self deny: self isAsync
 !
 
+testNonLifo
+	| suite runner result assertBlock |
+	suite := #(fakeNonLifoReturn testPass) collect: [ :each | self class selector: each ].
+	runner := TestSuiteRunner on: suite.
+	self timeout: 200.
+	result := runner result.
+	assertBlock := self async: [
+		self assert: (self selectorSetOf: result errors) equals: #(fakeNonLifoReturn) asSet.
+		self assert: (self selectorSetOf: result failures) equals: Set new.
+		"TODO check that error is indeed a correct NonLifoReturn"
+		self finished
+	].
+	runner announcer on: ResultAnnouncement do: [ :ann |
+		(ann result == result and: [ result runs = result total ]) ifTrue: assertBlock ].
+	runner run
+!
+
 testPass
 	flag := 'bad'.
 	self timeout: 10.
@@ -221,6 +244,13 @@ fakeMultipleTimeoutPassing
 	^ (Promise delayMilliseconds: 10) then: [ self timeout: 40. Promise delayMilliseconds: 20 ]
 !
 
+fakeNonLifoReturn
+	flag := 'bad'.
+	self timeout: 30.
+	flag := Promise delayMilliseconds: 20.
+	^ flag then: [ flag := 'ok'. ^ 'non-lifo' ]
+!
+
 fakePromiseWithoutTimeout
 	^ Promise delayMilliseconds: 10
 !
@@ -261,6 +291,22 @@ testIsAsyncReturnsCorrectValues
 	^ Promise new
 !
 
+testNonLifo
+	| suite runner result |
+	suite := #(fakeNonLifoReturn testPass) collect: [ :each | self class selector: each ].
+	runner := TestSuiteRunner on: suite.
+	self timeout: 200.
+	result := runner result.
+	^ Promise new: [ :model |
+		runner announcer on: ResultAnnouncement do: [ :ann |
+			(ann result == result and: [ result runs = result total ]) ifTrue: [ model do: [
+				self assert: (self selectorSetOf: result errors) equals: #(fakeNonLifoReturn) asSet.
+				self assert: (self selectorSetOf: result failures) equals: Set new.
+				"TODO check that error is indeed a correct NonLifoReturn"
+		] ] ].
+		runner run ]
+!
+
 testPass
 	flag := 'bad'.
 	self timeout: 10.
@@ -276,10 +322,9 @@ testPromiseErrorsAndFailures
 	result := runner result.
 	^ Promise new: [ :model |
 		runner announcer on: ResultAnnouncement do: [ :ann |
-			(ann result == result and: [ result runs = result total ]) ifTrue: [
+			(ann result == result and: [ result runs = result total ]) ifTrue: [ model do: [
 				self assert: (self selectorSetOf: result errors) equals: #(fakeError) asSet.
-				self assert: (self selectorSetOf: result failures) equals: #(fakeErrorFailingInTearDown fakeFailure) asSet.
-				model value: nil ] ].
+				self assert: (self selectorSetOf: result failures) equals: #(fakeErrorFailingInTearDown fakeFailure) asSet ] ] ].
 		runner run ]
 !
 
@@ -291,11 +336,9 @@ testTimeouts
 	result := runner result.
 	^ Promise new: [ :model |
 		runner announcer on: ResultAnnouncement do: [ :ann |
-			console log: ann; log: ann result runs.
-			(ann result == result and: [ result runs = result total ]) ifTrue: [
+			(ann result == result and: [ result runs = result total ]) ifTrue: [ model do: [
 				self assert: (self selectorSetOf: result errors) equals: #() asSet.
-				self assert: (self selectorSetOf: result failures) equals: #(fakeMultipleTimeoutFailing fakeTimeout fakeTimeoutSendOnly fakePromiseWithoutTimeout) asSet.
-				model value: nil ] ].
+				self assert: (self selectorSetOf: result failures) equals: #(fakeMultipleTimeoutFailing fakeTimeout fakeTimeoutSendOnly fakePromiseWithoutTimeout) asSet ] ] ].
 		runner run ]
 ! !
 

+ 31 - 2
lang/src/SUnit.js

@@ -1355,11 +1355,11 @@ $globals.ReportingTestContext);
 
 $core.addMethod(
 $core.method({
-selector: "withErrorReporting:",
+selector: "withErrorReporting2:",
 protocol: "private",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: ["aBlock"],
-source: "withErrorReporting: aBlock\x0a\x09[ aBlock\x0a\x09\x09on: TestFailure\x0a\x09\x09do: [ :ex | result addFailure: testCase ]\x0a\x09]\x0a\x09\x09on: Error\x0a\x09\x09do: [ :ex | result addError: testCase ]",
+source: "withErrorReporting2: aBlock\x0a\x09[ aBlock\x0a\x09\x09on: TestFailure\x0a\x09\x09do: [ :ex | result addFailure: testCase ]\x0a\x09]\x0a\x09\x09on: Error\x0a\x09\x09do: [ :ex | result addError: testCase ]",
 referencedClasses: ["TestFailure", "Error"],
 //>>excludeEnd("ide");
 pragmas: [],
@@ -1400,6 +1400,35 @@ return $recv($self.result)._addError_($self.testCase);
 ][0];
 return self;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"withErrorReporting2:",{aBlock:aBlock})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.ReportingTestContext);
+
+$core.addMethod(
+$core.method({
+selector: "withErrorReporting:",
+protocol: "private",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: ["aBlock"],
+source: "withErrorReporting: aBlock\x0a\x09<inlineJS: '\x0a\x09try { aBlock._value(); }\x0a\x09catch (ex) {\x0a\x09\x09var smalltalkError = $globals.Smalltalk._asSmalltalkException_(ex);\x0a\x09\x09self._withErrorReporting2_(function () { smalltalkError._pass(); });\x0a\x09}'>",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [["inlineJS:", ["\x0a\x09try { aBlock._value(); }\x0a\x09catch (ex) {\x0a\x09\x09var smalltalkError = $globals.Smalltalk._asSmalltalkException_(ex);\x0a\x09\x09self._withErrorReporting2_(function () { smalltalkError._pass(); });\x0a\x09}"]]],
+messageSends: []
+}, function ($methodClass){ return function (aBlock){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+
+	try { aBlock._value(); }
+	catch (ex) {
+		var smalltalkError = $globals.Smalltalk._asSmalltalkException_(ex);
+		self._withErrorReporting2_(function () { smalltalkError._pass(); });
+	};
+return self;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
 }, function($ctx1) {$ctx1.fill(self,"withErrorReporting:",{aBlock:aBlock})});
 //>>excludeEnd("ctx");
 }; }),

+ 10 - 1
lang/src/SUnit.st

@@ -335,13 +335,22 @@ result: aTestResult
 
 !ReportingTestContext methodsFor: 'private'!
 
-withErrorReporting: aBlock
+withErrorReporting2: aBlock
 	[ aBlock
 		on: TestFailure
 		do: [ :ex | result addFailure: testCase ]
 	]
 		on: Error
 		do: [ :ex | result addError: testCase ]
+!
+
+withErrorReporting: aBlock
+	<inlineJS: '
+	try { aBlock._value(); }
+	catch (ex) {
+		var smalltalkError = $globals.Smalltalk._asSmalltalkException_(ex);
+		self._withErrorReporting2_(function () { smalltalkError._pass(); });
+	}'>
 ! !
 
 !ReportingTestContext methodsFor: 'running'!