Skip to content

Support suspend calls that don't actually suspend#166

Draft
Simn wants to merge 1 commit intomasterfrom
unsuspending-suspend
Draft

Support suspend calls that don't actually suspend#166
Simn wants to merge 1 commit intomasterfrom
unsuspending-suspend

Conversation

@Simn
Copy link
Member

@Simn Simn commented Feb 28, 2026

Our recent changes to how suspend and suspendCancellable work means that they always suspend. This can be verified by using a counting dispatcher:

import haxe.coro.continuations.FunctionContinuation;
import haxe.coro.dispatchers.IDispatchObject;
import haxe.coro.dispatchers.SelfDispatcher;
import haxe.coro.schedulers.IScheduler;
import hxcoro.Coro.*;
import hxcoro.concurrent.AtomicInt;
import hxcoro.run.Setup;
import hxcoro.schedulers.VirtualTimeScheduler;

using hxcoro.run.ContextRun;

class CountingDispatcher extends SelfDispatcher {
	public final counter:AtomicInt;

	public function new(scheduler:IScheduler) {
		super(scheduler);
		counter = new AtomicInt(0);
	}

	override function dispatch(obj:IDispatchObject) {
		counter.add(1);
		super.dispatch(obj);
	}

	public function getCount() {
		return counter.load();
	}
}

function main() {
	final scheduler = new VirtualTimeScheduler();
	final dispatcher = new CountingDispatcher(scheduler);
	final context = Setup.defaultContext.with(dispatcher);
	@:coroutine function run() {
		suspend(cont -> {
			cont.resume(null, null);
		});

		suspendCancellable(cont -> {
			cont.resume(null, null);
			return null;
		});
	}

	var resumed = false;
	final cont = new FunctionContinuation(context, (result, error) -> {
		resumed = true;
	});
	run(cont);

	while (!resumed) {
		scheduler.loop(Once);
	}
	trace(dispatcher.getCount()); // 3 on master, 0 on this branch
}

This branch goes back to returning the continuation if it already is in Returned or Thrown mode after the closure passed to suspend has executed.

This isn't entirely free though because it means we can no longer specify that the suspension functions don't return or throw, which means all call-sites have to check that again. So something like hxcoro_Coro.yield(_hx_continuation); now is back to being this:

				var _hx_tmp = hxcoro_Coro.yield(_hx_continuation);
				switch(_hx_tmp.state) {
				case 0:
					return haxe_coro_SuspensionResult.suspended;
				case 1:
					break;
				case 2:
					_hx_error = _hx_tmp.error;
					break _hx_loop1;
				}
				break;

I'm unsure what's better here. Even though I implemented this change, I wonder if it puts too much focus on the synchronous path, which may or may not be the special case. On the other hand avoiding the asynchronous bubbling in such cases would certainly be more efficient.

So yes, not sure!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant