Skip to content

Conversation

@Simn
Copy link
Member

@Simn Simn commented Feb 6, 2024

Here's a different approach to handling the internal state for coroutines. This is pretty much #10128 but without the additional argument to TFun. The idea is this:

  • Instead of hijacking the typeloader upon encountering the "Coroutine" path, we let it load the abstract. The function type is carried in the type parameter.
  • We add com.basic.tcoro which works like tnull and creates Coroutine<Arguments -> Return>. This is used when typing @:coroutine functions.
  • Any place in the compiler that cares about coroutines calls follow_with_coro, which gives either a Coro(args, ret) or a NotCoro(t). This allows all code paths who are interested in coroutines (which in the entire compiler is only like 10) to check for them, but still deal with the non-coro behavior easily.

This gives us a pretty light diff here, which is always good. There's a chance that I'm missing something important because at the moment we only have the few tests in misc/coroutines.

@skial skial mentioned this pull request Feb 7, 2024
1 task
# Conflicts:
#	src/typing/typer.ml
@Simn Simn mentioned this pull request Feb 8, 2024
@Simn
Copy link
Member Author

Simn commented Feb 14, 2024

I'm making this the main coroutine PR with the following changes:

  • The logic from analyzerCoro.ml in Coroutine Experiments #10128 has been moved and slightly cleaned up to coroToTexpr.ml.
  • The new module coroFromTexpr.ml generates the data structure that ^ works with.
  • That transformation is still very simplistic and basically only works with cases that we're testing. The overall structure should allow us to improve on this easily though.
  • Coroutines are now processed right after a function has been typed. I see no reason to wait until later, we already know that we're dealing with a coroutine anyway.
  • As a consequence, we now run all the normal filters on the transformed expression structure. This will deal with all the problems in Coroutine Experiments #10128 related to capture variables and whatnot.
  • Doing that currently breaks the test in testTryCatchFail. This is related to [broken] Exceptions refactor #11574 and the problem is that the coroutine transformer eliminates the TTry, which causes the exception handler to not wrap the caught values. Running the expression wrapping immediately will fix this.
  • I tried testing this with C++ but ran into some problem with HX_STACKFRAME and '_hx_stateMachine': undeclared identifier. The related dump output looks clean, so this might be an entirely separate problem. Maybe @Aidan63 can give it a try!

Still a million things to figure out, but I'm slowly starting to understand how all this is supposed to work!

@Simn
Copy link
Member Author

Simn commented Feb 14, 2024

Some notes on the transformation:

If we have this:

@:coroutine
@:coroutine.debug
private function mapCalls<TArg, TRet>(args:Array<TArg>, f:Coroutine<TArg->TRet>):Array<TRet> {
	return [for (arg in args) f(arg)];
}

The transformer looks at this:

function(args:Array<mapCalls.TArg>, f:Coroutine<mapCalls.TArg -> mapCalls.TRet>) {
        return {
                var ` = [];
                {
                        var ` = 0;
                        while (` < args.length) {
                                var arg = args[`];
                                ++ `;
                                `.push(f(arg));
                        };
                };
                `;
        };
  }

And generates this:

function(args:Array<mapCalls.TArg>, f:Coroutine<mapCalls.TArg -> mapCalls.TRet>, _hx_continuation:(Array<mapCalls.TRet>, Dynamic) -> Void) {
        var ` = null;
        var ` = 0;
        var _hx_state = 0;
        var _hx_exceptionState = 10;
        var _hx_stateMachine = function(_hx_result:Dynamic, _hx_error:Dynamic) {
                if (_hx_error != null) _hx_state = _hx_exceptionState;
                do (try switch (_hx_state) {
                        case 0: {
                                _hx_state = 1;
                        };
                        case 1: {
                                `;
                                ` = [];
                                _hx_state = 3;
                        };
                        case 2: {
                                _hx_state = -1;
                                _hx_continuation(null, null);
                                return;
                        };
                        case 3: {
                                `;
                                ` = 0;
                                _hx_state = 5;
                        };
                        case 4: {
                                _hx_state = -1;
                                _hx_continuation(`, null);
                                return;
                        };
                        case 5: {
                                if (! (` < args.length)) _hx_state = 7 else _hx_state = 8;
                        };
                        case 6: {
                                _hx_state = 4;
                        };
                        case 7: {
                                _hx_state = 6;
                        };
                        case 8: {
                                var arg;
                                arg = args[`];
                                ++ `;
                                _hx_state = 9;
                                f(arg, _hx_stateMachine)(null, null);
                                return;
                        };
                        case 9: {
                                `.push(cast _hx_result);
                                _hx_state = 5;
                        };
                        case 10: throw _hx_error;
                        default: {
                                _hx_state = 10;
                                throw "Invalid coroutine state";
                        }
                } catch (e:Dynamic) if (_hx_state == 10) {
                        _hx_exceptionState = 10;
                        _hx_continuation(null, e);
                        return;
                } else {
                        _hx_state = _hx_exceptionState;
                        _hx_error = e;
                }) while(true);
        };
        return _hx_stateMachine;
  }

For one, it would be nice to eliminate the forwarding states 0, 6 and 7. Ideally in a way that would still give us the nice state numbering without any holes.

The other thing that looks suspicious to me is that we have do try instead of try do. The only path back into the loop is from the else case in the catch, but it looks to me like we can instead recurse upon our function again after setting _hx_error = e. The function entry checks for if (_hx_error != null) _hx_state = _hx_exceptionState; already, which will give us the exact same state.

Perhaps this could even be optimized for when there's only a single exception state (which I think is the vast majority of coros) because then we can just call the continuation, which we would otherwise do after catching the throw _hx_error in state 10.

Yes I know, premature optimization and all that, but I find this stuff easier to debug if the code looks cleaner!

@skial skial mentioned this pull request Feb 14, 2024
1 task
@Simn
Copy link
Member Author

Simn commented Feb 14, 2024

I got the basics working on the JVM target. This increasingly feels like I have no idea what I'm doing, but that may or may not be normal when implementing something like coroutines.

The generator tests don't work yet because I still don't know how to actually implement Coroutine.suspend properly.

For now, I'm dealing with the Coroutine special cases in genjvm directly. I don't know if we should map this away in a distinct pass instead - seems a bit annoying because the Coroutine type could "leak" to various places. Maybe simply dealing with this in the generators is acceptable.

@Simn
Copy link
Member Author

Simn commented Feb 14, 2024

Got the suspend thing working as well. This feels pretty hacky to do it like that, but I suppose it's not unexpected with these two different signatures going on.

@Aidan63 How did you do this for C++? I managed to include your Coroutine implementation but I can't seem to get the function right.

@Aidan63
Copy link
Contributor

Aidan63 commented Feb 14, 2024

Just gave this a try and it seems to work as it did before for my quick asys bodge, but without needing any of my additions. I added @:native('::hx::Coroutine::suspend') to the extern suspend function to have it call the function in the coroutine header, I just pushed the hxcpp changes to my hxcpp asys branch if you want to confirm things.

I agree that suspend is very confusing, everytime I think I understand it I look at it again and feel like I'm back at square one. It's very odd that it takes two arguments and then does...nothing? with them. Is there eventually going to be some advance functionality to it or is that purely to make it fit the "result and error" function signature.

I'm still seeing C++ errors with non static coroutines, stuff about __this again

image

I'll have to run the coroutine tests instead of just my quick asys bodge to see what else currently breaks.

@Simn
Copy link
Member Author

Simn commented Feb 14, 2024

@:native('::hx::Coroutine::suspend')

Hmm, that's what I tried as well but it generates this which the C++ compiler doesn't appreciate:

 ::Dynamic Coroutine_Impl__obj::_hx_f0xd5f8b494( ::Dynamic f, ::Dynamic cont) {
	return ::::hx::Coroutine::suspend(f,cont);
}

@Aidan63
Copy link
Contributor

Aidan63 commented Feb 14, 2024

That quad colon is suspicious, if @:native is working I wouldn't expect it to also prefix :: (don't know off the top of my head in what cases gencpp prefix ::), I also don't see any mention of the suspend function in the generated Coroutine_Impl__obj on my end.

I just tried compiling misc/coroutine for cpp and get another weird error, haxe::Exception has not been declared.

image

Looking at that header file it does not have a foward declaration of the exception type for some reason.

@Simn
Copy link
Member Author

Simn commented Feb 14, 2024

I can reproduce the Exception problem as well. Don't know yet what that is about, #11574 doesn't help with that either.

Another problem afterwards is this:

@:coroutine function error() {
	throw "nope";
}

function main() {
	error.start((result, error) -> {
		trace(error);
	});
}

This fails with 'result': illegal use of type 'void' from the generated void _hx_run(void result, ::Dynamic error){. I suppose it should infer Dynamic instead of Void in such cases, or maybe haxe.Unit once #11563 is done.

@Aidan63
Copy link
Contributor

Aidan63 commented May 30, 2024

That would work and would be very similar to the current function approach (just in resume instead of a function). The downside to that is that when a suspend function call returns you don't know if it has ran to completion, error, or has been suspended and needs to be resumed at some point. Which is useful to know in many situations.

Having looked at the problem again I think I've made it far more complex than it needs to be. My thinking behind the extra factory classes was that to create a coroutine which is initially suspended needed special support, but thats not the case. You can easily implement it with an extra continuation.

function test_defered() {
	final cont  = new BlockingContinuation(new EventLoopScheduler(Thread.current().events));
	final defer = new Defer(getNumber, cont);

	Assert.equals(0, nextNumber);

	defer.resume(null, null);

	Assert.equals(1, cont.wait());
}

with Defer being.

class Defer implements IContinuation<Any> {
	final lambda : IContinuation<Any>->Any;

	final continuation : IContinuation<Any>;

	public final _hx_context:CoroutineContext;

	public function new(lambda, continuation) {
		this.lambda       = lambda;
		this.continuation = continuation;
		this._hx_context  = continuation._hx_context;
	}

	public function resume(_:Any, _:Exception) {
		try {
			final result = lambda(continuation);
			if (result is Primitive) {
				return;
			}

			continuation.resume(result, null);
		} catch (exn) {
			continuation.resume(null, exn);
		}
	}
}

Calling resume on the defer continuation will start execution of the held coroutine function, so its initially suspended until manually started. So I think my previous comment can be disregarded as a load of rubbish.

@Apprentice-Alchemist
Copy link
Contributor

That would work and would be very similar to the current function approach (just in resume instead of a function). The downside to that is that when a suspend function call returns you don't know if it has ran to completion, error, or has been suspended and needs to be resumed at some point. Which is useful to know in many situations.

But the function call would merely "construct" a coroutine and not start it yet.
To start it you'd need to call resume once which does let you know if it's returned, errored or suspended itself.

@Pign
Copy link

Pign commented Mar 29, 2025

Hi,

I see that there haven't been any new comments for almost a year. Since coroutines would be quiet a big new standard fundamental in the language, I'm a bit worried and wondering how to better handle coroutines/futures/promises in a lib that'd want to be future proof.

Is there any update on this topic?

@TheTechsTech
Copy link

It seems the feature already exists in Ocaml - effect handlers there other libraries created around it effect-handlers-bench and see Effect handlers - wiki , the behavior is the same.

The problem is getting fixed on terminology currently used in this discussion having real differences, and getting in the way.

Doing a quick search, I find Haxe has no direct language support for yield, having a use case for that should be a starter. It's actually the underlying aspect that leads to general async/await for most other languages afterwards. It's a play on generators under the hood.

Implementing yield, you're will find after actually using it for a while, all current issues here and go2hx on implementing Coroutines aka Goroutines, will all pass, things well become apparent. The foundation hasn't been set, which is a process the discussion trying to leap over, there are actual learning stages that hasn't been set.

@Aidan63
Copy link
Contributor

Aidan63 commented Apr 6, 2025

One year later I've taken another look at all this and while the finer details have long since leaked out of my head I was able to port my kotlin style macros into the 2025 coroutines branch. It can be found here. https://github.com/Aidan63/haxe/tree/kt_coro

I was hoping there would be binaries available, but the CI has other ideas by the looks of it...

Here is a nadako-chat style sample, with io done by asys. https://gist.github.com/Aidan63/db02fe175cd446f3030f286f5aaa1024

Below are some notes.

Coroutine Starting

Starting a coroutine right now is a bit manual since I haven't updated start or added any similar functionality. For now I've lifted the restriction of calling coroutine functions from non coroutines, but you must provide a final completion of IContinuation<Any>.

final cont = new BlockingContinuation(new EventLoopScheduler(Thread.current().events));

foo(cont); // foo is a coroutine function

cont.wait();

In this case the wait call of the blocking continuation class pumps the threads event loop until the coroutine completes. The event loop scheduler ensures all continuations are executed on the initial thread.

Hoisted Variables

Variables and arguments which are used between states are hoisted into the generated class, and any access is remapped to those class fields. Initially I was using the original variable name as the classes field name, but this seemed to cause issue with generated variables. I was seeing variables with names which were just backticks, I'm guessing these get renamed at some later stage into the _g style variables. To work around the issue I'm now generating field names based on the variable id, this does have the downsize of the state machine code now being much harder to follow.

image

Clear as mud...

CoroutineIntrinsic

This class has a magic currentContinuation function which returns the current IContinuation<Any>, this is what Coroutine.suspend now uses to get hold of the current continuation. I'm not sure if this should actually return the completion or the continuation, I'll have to double check against some decompiled kotlin.

Cancellation

I've not yet pulled in my cancellation token stuff. It's really easy to do so since it's all in haxe, I just need to double check that above intrinsic thing first.

Targets

This should all be target independent (although I do need to add an implementation of Coroutine.suspend which doesn't use a mutex for js), having said that I've only been testing with C++. I tried to compile with the jvm target but get a runtime error with the resulting jar.

image

I don't think there's anything fundamentally wrong here, I'm guessing I've just made a mistake somewhere in all the manual typed expressions I'm building and that the cpp target is a bit more lax on these things. I just haven't been bothered to trawl through a record dump yet looking for type mismatches.

@Simn
Copy link
Member Author

Simn commented Apr 7, 2025

I've started trying to merge current development and kt_coro here: https://github.com/HaxeFoundation/haxe/tree/kt_coro_rebase

Not very easy to merge, but should at least be a starting point. I'd send you a PR but I can't figure out how to send a PR to a fork.

I reset all gencpp stuff to development because you'll have to reapply changes here after the refactoring anyway.

@Aidan63
Copy link
Contributor

Aidan63 commented Apr 8, 2025

Great, thanks! The first thing I did when I checked out the 2025 branch was to try and merge dev in, and quickly gave up...

While that rebase branch compiles it doesn't seem to work correctly. From what I can tell the types passed into follow_with_coro now always return NotCoro, so it seems like the wrapping TAbstract has been lost.

I modified follow_with_coro to print out the type like so.

let follow_with_coro t =
	let rec s_type_kind t =
		let map tl = String.concat ", " (List.map s_type_kind tl) in
		match t with
		| TMono r ->
			begin match r.tm_type with
				| None -> Printf.sprintf "TMono (None)"
				| Some t -> "TMono (Some (" ^ (s_type_kind t) ^ "))"
			end
		| TEnum(en,tl) -> Printf.sprintf "TEnum(%s, [%s])" (s_type_path en.e_path) (map tl)
		| TInst(c,tl) -> Printf.sprintf "TInst(%s, [%s])" (s_type_path c.cl_path) (map tl)
		| TType(t,tl) -> Printf.sprintf "TType(%s, [%s])" (s_type_path t.t_path) (map tl)
		| TAbstract(a,tl) -> Printf.sprintf "TAbstract(%s, [%s])" (s_type_path a.a_path) (map tl)
		| TFun(tl,r) -> Printf.sprintf "TFun([%s], %s)" (String.concat ", " (List.map (fun (n,b,t) -> Printf.sprintf "%s%s:%s" (if b then "?" else "") n (s_type_kind t)) tl)) (s_type_kind r)
		| TAnon an -> "TAnon"
		| TDynamic t2 -> "TDynamic"
		| TLazy _ -> "TLazy"
	in

	match follow t with
	| TAbstract({a_path = (["haxe";"coro"],"Coroutine")},[t]) ->
		begin match follow t with
			| TFun(args,ret) ->
				Printf.printf "%s is a coro!\n" (s_type_kind t);
				Coro (args,ret)
			| t ->
				Printf.printf "%s, is a coro, but not a tfun...\n" (s_type_kind t);
				NotCoro t
		end
	| t ->
		Printf.printf "%s, not a coro...\n" (s_type_kind t);
		NotCoro t

and on my ky_style branch I see stuff like,

TFun([func:TFun([:TInst(haxe.coro.IContinuation, [TAbstract(Any, [])])], TAbstract(Void, []))], TMono (None)) is a coro!

but on that rebase branch it never finds coroutines. It shows those same TFuns, but reports them as not a coroutine.

TFun([func:TFun([:TInst(haxe.coro.IContinuation, [TAbstract(Any, [])])], TAbstract(Void, []))], TMono (None)), not a coro...

I've not dug any deeper that that so far.

@Simn
Copy link
Member Author

Simn commented Apr 8, 2025

Looks like I botched the most important line... I pushed an update, see if that helped.

Also, do you have a coro hello world so I can try compiling stuff myself? I don't remember how to set all this up.

@Aidan63
Copy link
Contributor

Aidan63 commented Apr 8, 2025

Thanks, that seems to have fixed it and a quick hello world now compiles.

import haxe.coro.*;
import haxe.Exception;
import sys.thread.EventLoop;
import sys.thread.Thread;

private class EventLoopScheduler implements IScheduler {
    final loop : EventLoop;

    public function new(loop) {
        this.loop = loop;
    }

    public function schedule(func : ()->Void) {
        loop.run(func);
    }
}

private class BlockingContinuation implements IContinuation<Any> {
	public final _hx_context:CoroutineContext;

	var running : Bool;
	var result : Int;
	var error : Exception;

	public function new(scheduler) {
		_hx_context = new CoroutineContext(scheduler);
		running     = true;
		result      = 0;
		error       = null;
	}

	public function resume(result:Any, error:Exception) {
		running = false;

		this.result = result;
		this.error  = error;
	}

	public function wait():Any {
		while (running) {
			Thread.current().events.progress();
		}

		if (error != null) {
			throw error;
		} else {
			return cast result;
		}
	}
}
class Main {
	@:coroutine static function test() {
		trace('hello');

		delay(1000);

		trace('world');
	}

	@:coroutine public static function delay(ms:Int):Void {
		return Coroutine.suspend(cont -> {
			haxe.Timer.delay(() -> cont.resume(null, null), ms);
		});
	}

	static function main() {
		final cont = new BlockingContinuation(new EventLoopScheduler(Thread.current().events));

		test(cont);

		cont.wait();
    }
}

This compiles fine with git hxcpp, it should work fine on any sys target but as mentioned in my other comment there's probably some dodgy typed expression building stopping jvm from running atleast.

Before I forget this is a very good article on how kotlins coroutines work, less about the state machine transformation and more about how it all comes together with suspension and continuation.

https://kt.academy/article/cc-under-the-hood

I've trawled through decompiled kotlin and it was very hard to understand. Kotlin's coroutines have many features and optimisations applied which obscures the root of what's going on. That article strips back all of that, I pretty much copied what they described for my original macro attempt and what I've ported to the compiler.

For anyone who does feel brave enough to dive into decompiled kotlin I found the following article quite helpful.

https://www.droidcon.com/2022/09/22/design-of-kotlin-coroutines/

It goes through all the core functions which create a coroutine and explains what they're there for. Very useful when trying to figure out if what you're looking at is important to what you want to know.

@Simn
Copy link
Member Author

Simn commented Apr 8, 2025

Thanks! I had to add some imports but with that I can compile it.

JVM is offended by this decompiled piece of code:

                if (Std.isOfType(_hx_tmp, Primitive.class)) {
                    return Primitive.suspended;
                }

The test function is declared to return Function, which isn't compatible with Primitive. With -D dump=record I can see cf_type = TAbstract(haxe.coro.Coroutine, [TFun([], TMono (Some (TAbstract(Void, []))))]);, so follow_with_coro will give us the TFun and that then generates Function.

I'm not sure which runtime return type this function is supposed to have.

@Aidan63
Copy link
Contributor

Aidan63 commented Apr 8, 2025

All coroutine functions should be transformed to return Any, so something like

@:coroutine function foo(s:String):Int;

should become

function foo(s:String, c:IContinuation<Any>):Any;

sounds like I've missed updating the cf_type of the field.

@Simn
Copy link
Member Author

Simn commented Apr 8, 2025

I have fixed this in b2fb3bc, genjvm still had the previous coroutine signature. With that, hello world works there now.

@skial skial mentioned this pull request Apr 9, 2025
1 task
@elliott5
Copy link

elliott5 commented Apr 9, 2025

All coroutine functions should be transformed to return Any, so something like

@:coroutine function foo(s:String):Int;

should become

function foo(s:String, c:IContinuation<Any>):Any;

Because of potential optional and rest arguments to functions in Haxe, should the c:IContinuation<Any> parameter not be the first in the generated function signature?

@Simn
Copy link
Member Author

Simn commented Apr 9, 2025

Because of potential optional and rest arguments to functions in Haxe, should the c:IContinuation<Any> parameter not be the first in the generated function signature?

Interesting question... My first thought is that this is indeed the case for rest arguments at least. Optional arguments are "probably" properly handled via null-padding, but I'm not sure.

@Aidan63
Copy link
Contributor

Aidan63 commented Apr 9, 2025

Probably makes sense to move the continuation object to the front of the argument list. I only put it at the end because "that's what kotlin do".

For reference a kotlin suspending function containing an argument with a default value, such as the following.

   suspend fun foo(i:Int = 7) {}

Has a extra compiler generated function to deal with it.

   // $FF: synthetic method
   public static Object foo$default(int var0, Continuation var1, int var2) {
      if ((var2 & 1) != 0) {
         var0 = 7;
      }

      return foo(var0, var1);
   }

The extra parameter at the end it used for testing if the actual argument is using the default value or not. Kotlin appears to have no way to express optional / default types in function signatures, so completly avoids the optional / default argument mess we have.

@elliott5
Copy link

I have concerns about the visibility and use of the "continuation object", which I expect the authors plan to resolve as part of the tidying-up phase towards the end of this PR's lifecycle.

In the really helpful "Hello World" example above the "continuation object" called cont appears in two unexpected places.

  1. When scheduling a coroutine - test(cont) below - the "continuation object" is used as a parameter when no such parameter appears in the test() function signature. But, by contrast, when calling a coroutine from within another coroutine - delay(1000) below - no "continuation object" is required.
	@:coroutine static function test() {
		trace('hello');

		delay(1000);

...

	@:coroutine public static function delay(ms:Int):Void {

...

	static function main() {
		final cont = new BlockingContinuation(new EventLoopScheduler(Thread.current().events));

		test(cont);

For ease of understanding, when initially starting a coroutine, maybe using some formula like haxe.coro.Start(contObj, funcName, parameters...) to generate the required code would be helpful.

  1. Accessing the "continuation object" - cont below - from within a coroutine, when the variable has not been explicitly declared.
	@:coroutine public static function delay(ms:Int):Void {
		return Coroutine.suspend(cont -> {
			haxe.Timer.delay(() -> cont.resume(null, null), ms);
		});
	}

Clearly, access to the "continuation object" is required from within the coroutine code, so some standard formula to enable that is also required, maybe Coroutine.continuation or similar.

@Simn
Copy link
Member Author

Simn commented Apr 10, 2025

I think 1. is just because of this:

Starting a coroutine right now is a bit manual since I haven't updated start or added any similar functionality.

This will ultimately be handled by the compiler itself when it comes across a coroutine call from a not-coroutine context.

I agree regarding 2., the appearance of cont here is quite surprising.

@elliott5
Copy link

Thank you for your prompt reply @Simn.

Having calls to coroutines from non-coroutine contexts handled by the compiler would be most understandable, provided that the non-coroutine context waits for the coroutine to complete, as if it were a normal subroutine.

If the coroutine is to execute independently, some annotation in the code that this is to occur would be easiest to understand. Obviously the Go language uses go someCoroutine() for this purpose.

On a related note, from the go2hx project's point-of-view, it may be helpful to be able to extend the underlying coroutine scheduler, as done in the "Hello World" example above. Do you plan for this be possible?

@Apprentice-Alchemist
Copy link
Contributor

A coroutine call in non-coroutine context should simply return some kind of coroutine object and the standard library can then provide apis to run/schedule it.

Implicitly making coroutine calls in non-coro contexts block is a very bad idea in my opinion.

@Simn
Copy link
Member Author

Simn commented Apr 10, 2025

Yes I didn't mean an implicit coroutine call, I meant the actual start method or whatever we end up using. My understanding is that this is where the continuation and everything is set up.

@Aidan63
Copy link
Contributor

Aidan63 commented Apr 10, 2025

I added an initial basic Coroutine.run function yesterday evening which blocks until the provided coroutine completes.

public static function run<T>(f:Coroutine<()->T>) {
	final loop = new EventLoop();
	final cont = new BlockingContinuation(loop, new EventLoopScheduler(loop));

	f(cont);

	return cast cont.wait();
}

There's no special "compiler magic" going on here right now, it's just sweeping the existing stuff under the carpet. This function now also creates it's own dedicated event loop to pump instead of the main threads.

So in the below example "Hello!" now won't be printed until the coroutine completes.

import sys.thread.Thread;
import haxe.coro.Coroutine;
import haxe.coro.Coroutine.delay;

@:coroutine function foo() {
    delay(1000);
}

function main() {
    Thread.current.events.run(() -> trace("Hello!"));

    Coroutine.run(foo);
}

I also added delay and yield coroutine functions to the main Coroutine class. In adding that Coroutine.run I came across another issue with jvm, this time with static function closures. I think I've managed to fix it in my reset coro branch at caa7024.

You can get access to the continuation of the current coroutine using the magic haxe.coro.Instrinsics.currentContinuation function, I've not been shouting about it yet as it's one thing I'm not too sure about and it's easy to mis-use and break coroutines with it. It is a useful building block for higher level abstractions though. The Coroutine.suspend function uses it internally for example.

@:coroutine public static function suspend<T>(func:(IContinuation<Any>)->Void):T {
	final cont = haxe.coro.Intrinsics.currentContinuation();
	final safe = new RacingContinuation(cont);

	func(safe);

	return cast safe.getOrThrow();
}

I want to find a better / safer way of accessing it since the CoroutineContext, which is stored in the continuation, is likely to game more things which will want to be accessed. While I've not yet ported it over, my macro version had the cancellation mechanisms stored in that context. Needing to do stuff like haxe.coro.currentContinuation()._hx_context.cancellationToken isn't particually pleasant...

On scheduling.
The ground work for custom scheduling has already been done. All coroutine continuations are currently bounced through the IScheduler interface (EventLoopScheduler in the first code snippet is an implementation of this interface). Not sure what shape it will all eventually take but a way to determine where and how coroutines continue will almost certainly have to be provided.

@Simn
Copy link
Member Author

Simn commented Apr 12, 2025

Closing in favor of #12168.

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.