diff --git a/.gitignore b/.gitignore index 1148a80b950..7a3fb70a367 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ lib.sexp src/compiler/version.ml tests/party tests/misc/projects/Issue10863/error.log +tests/misc/coroutines/dump diff --git a/README.md b/README.md index 5806703c00d..1f6016317ad 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,9 @@ -

- -

- -

- GitHub Build Status - SauceLabs Test Status - Gitter - Discord -

- -# - -Haxe is an open source toolkit that allows you to easily build cross-platform tools and applications that target many mainstream platforms. The Haxe toolkit includes: - - * **The Haxe programming language**, a modern, high-level, strictly-typed programming language - * **The Haxe cross-compiler**, a state-of-the-art, lightning-speed compiler for many targets - * **The Haxe standard library**, a complete, cross-platform library of common functionality - -Haxe allows you to compile for the following targets: - - * JavaScript - * C++ - * JVM - * Lua - * PHP 7 - * Python 3 - * [HashLink](https://hashlink.haxe.org/) - * [NekoVM](https://nekovm.org/) - * Flash (SWF Bytecode) - * And its own [interpreter](https://haxe.org/blog/eval/) - -You can try Haxe directly from your browser at [try.haxe.org](https://try.haxe.org)! - -For more information about Haxe, head to the [official Haxe website](https://haxe.org). - -## License - -The Haxe project has several licenses, covering different parts of the projects. - - * The Haxe compiler is released under the GNU General Public License version 2 or any later version. - * The Haxe standard library is released under the MIT license. - * The Neko virtual machine is released under the MIT license. Its bundled runtime libraries (ndll) and tools are released under open source licenses as described in https://github.com/HaxeFoundation/neko/blob/master/LICENSE - -For the complete Haxe licenses, please see https://haxe.org/foundation/open-source.html or [extra/LICENSE.txt](extra/LICENSE.txt). - -## Installing Haxe - -The latest stable release is available at [https://haxe.org/download/](https://haxe.org/download/). Pre-built binaries are available for your platform: - - * **[Windows installer](https://haxe.org/download/file/latest/haxe-latest-win.exe/)** - * **[Windows binaries](https://haxe.org/download/file/latest/haxe-latest-win.zip/)** - * **[OSX installer](https://haxe.org/download/file/latest/haxe-latest-osx-installer.pkg/)** - * **[OSX binaries](https://haxe.org/download/file/latest/haxe-latest-osx.tar.gz/)** - * **[Linux Software Packages](https://haxe.org/download/linux/)** - * **[Linux 32-bit binaries](https://haxe.org/download/file/latest/haxe-latest-linux32.tar.gz/)** - * **[Linux 64-bit binaries](https://haxe.org/download/file/latest/haxe-latest-linux64.tar.gz/)** - -Automated development builds are available from [build.haxe.org](http://build.haxe.org). - -## Building from source - -See [extra/BUILDING.md](extra/BUILDING.md). - -## Using Haxe - -For information on using Haxe, consult the [Haxe documentation](https://haxe.org/documentation/): - - * [Haxe Introduction](https://haxe.org/documentation/introduction/), an introduction to the Haxe toolkit - * [The Haxe Manual](https://haxe.org/manual/), the reference manual for the Haxe language - * [Haxe Code Cookbook](https://code.haxe.org), code snippets / learning resource - * [Haxe API](https://api.haxe.org), documentation for the Haxe standard and native APIs - * [Haxelib](https://lib.haxe.org), Haxelib is the package manager for the Haxe Toolkit. - -## Community - -You can get help and talk with fellow Haxers from around the world via: - - * [Haxe Community Forum](http://community.haxe.org) - * [Haxe on Stack Overflow](https://stackoverflow.com/questions/tagged/haxe) - * [Haxe Gitter chatroom](https://gitter.im/HaxeFoundation/haxe/) - * [Haxe Discord server](https://discordapp.com/invite/0uEuWH3spjck73Lo) - -:+1: Get notified of the latest Haxe news, don't forget to read the [Haxe roundups](https://haxe.io/). - -## Version compatibility - -Haxe | Neko | SWF | Python | HL | PHP | Lua | ---------------- | ----- | ----- | ------ | ---- | ---- | --- | -2.* | 1.* | 8-10 | - | - | - | - | -3.0.0 | 2.0.0 | | - | - | 5.1+ | - | -3.2.0 | | 12-14 | 3.2+ | - | | - | -3.3.0 | 2.1.0 | 21 | | - | | 5.1, 5.2, 5.3, LuaJIT 2.0, 2.1 | -3.4.0 | | | | 1.1 | 5.4+ and 7.0+ (with `-D php7`) | | -4.0.0 | 2.3.0 | | | 1.11 | 7.0+ | | - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for more. Thank you! +* First PR: https://github.com/HaxeFoundation/haxe/pull/10128 +* Second PR: https://github.com/HaxeFoundation/haxe/pull/11554 +* Third PR: https://github.com/HaxeFoundation/haxe/pull/12168 + +* Original design repo: https://github.com/nadako/haxe-coroutines/ +* Related Kotlin document: https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md +* Coroutines under the hood: https://kt.academy/article/cc-under-the-hood +* Design of Kotlin coroutines: https://www.droidcon.com/2022/09/22/design-of-kotlin-coroutines/ +* Mega document: https://github.com/JetBrains/kotlin/blob/master/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md \ No newline at end of file diff --git a/src-json/meta.json b/src-json/meta.json index 5c236f1d950..93f3936e195 100644 --- a/src-json/meta.json +++ b/src-json/meta.json @@ -142,6 +142,18 @@ "targets": ["TAbstract"], "links": ["https://haxe.org/manual/types-abstract-core-type.html"] }, + { + "name": "Coroutine", + "metadata": ":coroutine", + "doc": "Transform function into a coroutine", + "targets": ["TClassField"] + }, + { + "name": "CoroutineTransformed", + "metadata": ":coroutine.transformed", + "doc": "Marks a field as being a coroutine that has already been transformed", + "targets": ["TClassField"] + }, { "name": "CppFileCode", "metadata": ":cppFileCode", diff --git a/src/codegen/fixOverrides.ml b/src/codegen/fixOverrides.ml index 12d6514b02e..88bbc2d1c94 100644 --- a/src/codegen/fixOverrides.ml +++ b/src/codegen/fixOverrides.ml @@ -41,7 +41,11 @@ let fix_override com c f fd = let f2 = (try Some (find_field com c f) with Not_found -> None) in match f2,fd with | Some (f2), Some(fd) -> - let targs, tret = (match follow f2.cf_type with TFun (args,ret) -> args, ret | _ -> die "" __LOC__) in + let targs, tret = + match follow_with_coro f2.cf_type with + | Coro (args,ret) -> Common.expand_coro_type com.basic args ret + | NotCoro (TFun(args, ret)) -> args, ret + | _ -> die "" __LOC__ in let changed_args = ref [] in let prefix = "_tmp_" in let nargs = List.map2 (fun ((v,ct) as cur) (_,_,t2) -> @@ -86,7 +90,11 @@ let fix_override com c f fd = f.cf_expr <- Some { fde with eexpr = TFunction fd2 }; f.cf_type <- TFun(targs,tret); | Some(f2), None when (has_class_flag c CInterface) -> - let targs, tret = (match follow f2.cf_type with TFun (args,ret) -> args, ret | _ -> die "" __LOC__) in + let targs, tret = + match follow_with_coro f2.cf_type with + | Coro (args,ret) -> Common.expand_coro_type com.basic args ret + | NotCoro (TFun(args, ret)) -> args, ret + | _ -> die "" __LOC__ in f.cf_type <- TFun(targs,tret) | _ -> () diff --git a/src/context/common.ml b/src/context/common.ml index 3eef8c80c8b..30954fd0be9 100644 --- a/src/context/common.ml +++ b/src/context/common.ml @@ -754,9 +754,20 @@ let create timer_ctx compilation_step cs version args display_mode = tfloat = mk_mono(); tbool = mk_mono(); tstring = mk_mono(); + texception = mk_mono(); tnull = (fun _ -> die "Could use locate abstract Null (was it redefined?)" __LOC__); tarray = (fun _ -> die "Could not locate class Array (was it redefined?)" __LOC__); titerator = (fun _ -> die "Could not locate typedef Iterator (was it redefined?)" __LOC__); + tunit = mk_mono(); + tcoro = { + tcoro = (fun _ -> die "Could not locate abstract Coroutine (was it redefined?)" __LOC__); + continuation = mk_mono(); + base_continuation_class = null_class; + suspension_state = mk_mono(); + suspension_result = (fun _ -> die "Could not locate class ContinuationResult (was it redefined?)" __LOC__); + suspension_result_class = null_class; + immediate_suspension_result_class = null_class; + } }; std = null_class; file_keys = new file_keys; @@ -887,6 +898,17 @@ let clone com is_macro_context = tnull = (fun _ -> die "Could use locate abstract Null (was it redefined?)" __LOC__); tarray = (fun _ -> die "Could not locate class Array (was it redefined?)" __LOC__); titerator = (fun _ -> die "Could not locate typedef Iterator (was it redefined?)" __LOC__); + texception = mk_mono(); + tunit = mk_mono(); + tcoro = { + tcoro = (fun _ -> die "Could not locate abstract Coroutine (was it redefined?)" __LOC__); + continuation = mk_mono(); + base_continuation_class = null_class; + suspension_state = mk_mono(); + suspension_result = (fun _ -> die "Could not locate class ContinuationResult (was it redefined?)" __LOC__); + suspension_result_class = null_class; + immediate_suspension_result_class = null_class; + }; }; std = null_class; module_to_file = new hashtbl_lookup; @@ -1097,6 +1119,11 @@ let get_entry_point com = (snd path, c, e) ) com.main.main_path +let expand_coro_type basic args ret = + let args = args @ [("_hx_continuation",false,basic.tcoro.continuation)] in + let ret = if ExtType.is_void (follow ret) then basic.tunit else ret in + (args,basic.tcoro.suspension_result ret) + let make_unforced_lazy t_proc f where = let r = ref (lazy_available t_dynamic) in r := lazy_wait (fun() -> diff --git a/src/context/typecore.ml b/src/context/typecore.ml index 5322386f945..067013a0044 100644 --- a/src/context/typecore.ml +++ b/src/context/typecore.ml @@ -92,6 +92,7 @@ type typer_pass_tasks = { type function_mode = | FunFunction + | FunCoroutine | FunNotFunction type typer_globals = { @@ -113,6 +114,7 @@ type typer_globals = { mutable delayed_display : DisplayTypes.display_exception_kind option; root_typer : typer; (* api *) + mutable continuation_api : ContTypes.continuation_api option; do_macro : typer -> macro_mode -> path -> string -> expr list -> pos -> macro_result; do_load_macro : typer -> bool -> path -> string -> pos -> ((string * bool * t) list * t * tclass * Type.tclass_field); do_load_module : ?origin:module_dep_origin -> typer -> path -> pos -> module_def; @@ -288,8 +290,11 @@ module TyperManager = struct let e = create_ctx_e ctx.e.curfun FunNotFunction in create ctx ctx.m ctx.c f e PTypeField ctx.type_params + let is_coroutine_context ctx = + ctx.e.function_mode = FunCoroutine + let is_function_context ctx = match ctx.e.function_mode with - | FunFunction -> + | FunFunction | FunCoroutine -> true | FunNotFunction -> false diff --git a/src/core/tFunctions.ml b/src/core/tFunctions.ml index 91893c5e81d..3b874182cbd 100644 --- a/src/core/tFunctions.ml +++ b/src/core/tFunctions.ml @@ -649,6 +649,21 @@ let rec follow_lazy_and_mono t = match t with | _ -> t +type maybe_coro = + | Coro of tsignature + | NotCoro of t + +let follow_with_coro t = match follow t with + | TAbstract({a_path = (["haxe";"coro"],"Coroutine")},[t]) -> + begin match follow t with + | TFun(args,ret) -> + Coro (args,ret) + | t -> + NotCoro t + end + | t -> + NotCoro t + let rec ambiguate_funs t = match follow t with | TFun _ -> TFun ([], t_dynamic) diff --git a/src/core/tType.ml b/src/core/tType.ml index c422b85f4b6..79f4aad4330 100644 --- a/src/core/tType.ml +++ b/src/core/tType.ml @@ -481,6 +481,18 @@ and build_state = exception Type_exception of t +(* TODO: Most of this can probably be moved to the typer context + with lazy initialization from the coro code. *) +type coro_types = { + mutable tcoro : (string * bool * t) list -> t -> t; + mutable continuation : t; + mutable base_continuation_class : tclass; + mutable suspension_state : t; + mutable suspension_result : t -> t; + mutable suspension_result_class : tclass; + mutable immediate_suspension_result_class : tclass; +} + type basic_types = { mutable tvoid : t; mutable tany : t; @@ -490,7 +502,10 @@ type basic_types = { mutable tnull : t -> t; mutable tstring : t; mutable tarray : t -> t; - mutable titerator : t -> t + mutable texception : t; + mutable titerator : t -> t; + mutable tunit : t; + mutable tcoro : coro_types; } type class_field_scope = diff --git a/src/core/texpr.ml b/src/core/texpr.ml index 6447cb1a077..9e8e029f934 100644 --- a/src/core/texpr.ml +++ b/src/core/texpr.ml @@ -649,6 +649,24 @@ let for_remap basic v etype e1 e2 p = mk (TWhile((mk (TParenthesis ehasnext) ehasnext.etype ehasnext.epos),ebody,NormalWhile)) basic.tvoid e1.epos; ]) basic.tvoid p +let not_while_true_to_while_true basic e1 e2 flag t p = + let e_break = mk TBreak t_dynamic p in + let e_not = mk (TUnop(Not,Prefix,Builder.mk_parent e1)) e1.etype e1.epos in + let e_if eo = mk (TIf(e_not,e_break,eo)) basic.tvoid p in + let rec map_continue e = match e.eexpr with + | TContinue -> + duplicate_tvars e_identity (e_if (Some e)) + | TWhile _ -> + e + | _ -> + map_expr map_continue e + in + let e2 = if flag = NormalWhile then e2 else map_continue e2 in + let e_if = e_if None in + let e_block = if flag = NormalWhile then concat e_if e2 else concat e2 e_if in + let e_true = mk (TConst (TBool true)) basic.tbool p in + mk (TWhile(e_true,e_block,NormalWhile)) t p + (* -------------------------------------------------------------------------- *) (* BUILD META DATA OBJECT *) diff --git a/src/coro/contTypes.ml b/src/coro/contTypes.ml new file mode 100644 index 00000000000..66d20a205ff --- /dev/null +++ b/src/coro/contTypes.ml @@ -0,0 +1,25 @@ +open Type + +type continuation_api = { + state : tclass_field; + result : tclass_field; + error : tclass_field; + completion : tclass_field; + context : tclass_field; + goto_label : tclass_field; + recursing : tclass_field; + immediate_result : texpr -> texpr; + immediate_error : texpr -> Type.t -> texpr; +} + +let create_continuation_api immediate_result immediate_error state result error completion context goto_label recursing = { + immediate_result; + immediate_error; + state; + result; + error; + completion; + context; + goto_label; + recursing; +} \ No newline at end of file diff --git a/src/coro/coro.ml b/src/coro/coro.ml new file mode 100644 index 00000000000..497a41f6184 --- /dev/null +++ b/src/coro/coro.ml @@ -0,0 +1,554 @@ +open Globals +open Type +open CoroTypes +open CoroFunctions +open Texpr +open ContTypes + +let localFuncCount = ref 0 + +type coro_for = + | LocalFunc of tfunc * tvar + | ClassField of tclass * tclass_field * tfunc * pos (* expr pos *) + +type coro_cls = { + params : typed_type_param list; + param_types : Type.t list; + cls_t : Type.t; + result_type : Type.t; + cont_type : Type.t; +} + +let substitute_type_params subst t = + let rec loop t = match t with + | TInst({cl_kind = KTypeParameter ttp}, []) -> + (try List.assq ttp subst with Not_found -> t) + | _ -> + Type.map loop t + in + loop t + +module ContinuationClassBuilder = struct + type coro_class = { + cls : tclass; + name_pos : pos; + (* inside = inside the continuation class *) + inside : coro_cls; + (* outside = in the original function *) + outside : coro_cls; + type_param_subst : (typed_type_param * Type.t) list; + coro_type : coro_for; + continuation_api : ContTypes.continuation_api; + (* Some coroutine classes (member functions, local functions) need to capture state, this field stores that *) + captured : tclass_field option; + } + + let create ctx coro_type = + let basic = ctx.typer.t in + (* Mangle class names to hopefully get unique names and avoid collisions *) + let name, cf_captured, result_type, name_pos = + let captured_field_name = "captured" in + match coro_type with + | ClassField (cls, field, tf, _) -> + Printf.sprintf "HxCoro_%s_%s_%s" (ctx.typer.m.curmod.m_path |> fst |> String.concat "_") (ctx.typer.m.curmod.m_path |> snd) field.cf_name, + (if has_class_field_flag field CfStatic then + None + else + Some (mk_field captured_field_name ctx.typer.c.tthis field.cf_name_pos field.cf_name_pos)), + tf.tf_type, + field.cf_name_pos + | LocalFunc(f,v) -> + let n = Printf.sprintf "HxCoroAnonFunc_%i" !localFuncCount in + localFuncCount := !localFuncCount + 1; + + let args = List.map (fun (v, _) -> (v.v_name, false, v.v_type)) f.tf_args in + let t = TFun (Common.expand_coro_type basic args f.tf_type) in + + n, Some (mk_field captured_field_name t v.v_pos v.v_pos), f.tf_type, v.v_pos + in + + let result_type = if ExtType.is_void (follow result_type) then ctx.typer.t.tunit else result_type in + (* Is there a pre-existing function somewhere to a valid path? *) + let cls_path = ((fst ctx.typer.m.curmod.m_path) @ [ Printf.sprintf "_%s" (snd ctx.typer.m.curmod.m_path) ]), name in + let cls = mk_class ctx.typer.m.curmod cls_path name_pos name_pos in + let params_outside = ctx.typer.type_params in + let params_inside = List.map (fun ttp -> + (* TODO: this duplicates clone_type_parameter *) + let c = ttp.ttp_class in + let map = fun t -> t in (* TODO: ? *) + let c = {c with cl_path = ([],ttp.ttp_name)} in + let def = Option.map map ttp.ttp_default in + let constraints = match ttp.ttp_constraints with + | None -> None + | Some constraints -> Some (lazy (List.map map (Lazy.force constraints))) + in + mk_type_param c TPHType (* !!! *) def constraints + ) params_outside in + cls.cl_params <- params_inside; + + let continuation_api = match ctx.typer.g.continuation_api with + | Some api -> + api + | None -> + let cf_state = PMap.find "state" basic.tcoro.suspension_result_class.cl_fields in + let cf_result = PMap.find "result" basic.tcoro.suspension_result_class.cl_fields in + let cf_error = PMap.find "error" basic.tcoro.suspension_result_class.cl_fields in + let cf_completion = PMap.find "completion" basic.tcoro.base_continuation_class.cl_fields in + let cf_context = PMap.find "context" basic.tcoro.base_continuation_class.cl_fields in + let cf_goto_label = PMap.find "gotoLabel" basic.tcoro.base_continuation_class.cl_fields in + let cf_recursing = PMap.find "recursing" basic.tcoro.base_continuation_class.cl_fields in + let immediate_result,immediate_error = + let c = basic.tcoro.immediate_suspension_result_class in + let cf_result = PMap.find "withResult" c.cl_statics in + let cf_error = PMap.find "withError" c.cl_statics in + (fun e -> + CallUnification.make_static_call_better ctx.typer c cf_result [e.etype] [e] (TInst(c,[e.etype])) e.epos + ), (fun e t -> + CallUnification.make_static_call_better ctx.typer c cf_error [] [e] (TInst(c,[t])) e.epos + ) + in + let api = ContTypes.create_continuation_api immediate_result immediate_error cf_state cf_result cf_error cf_completion cf_context cf_goto_label cf_recursing in + ctx.typer.g.continuation_api <- Some api; + api + in + + let param_types_inside = extract_param_types params_inside in + let param_types_outside = extract_param_types params_outside in + let subst = List.combine params_outside param_types_inside in + let result_type_inside = substitute_type_params subst result_type in + cls.cl_super <- Some (basic.tcoro.base_continuation_class, [result_type_inside]); + cf_captured |> Option.may (fun cf -> cf.cf_type <- substitute_type_params subst cf.cf_type); + + { + cls = cls; + name_pos; + inside = { + params = params_inside; + param_types = param_types_inside; + cls_t = TInst(cls,param_types_inside); + result_type = result_type_inside; + cont_type = TInst(basic.tcoro.base_continuation_class,[result_type_inside]); + }; + outside = { + params = params_outside; + param_types = param_types_outside; + cls_t = TInst(cls,param_types_outside); + result_type = result_type; + cont_type = TInst(basic.tcoro.base_continuation_class,[result_type]); + }; + type_param_subst = subst; + coro_type = coro_type; + continuation_api; + captured = cf_captured; + } + + let mk_ctor ctx coro_class initial_state = + let basic = ctx.typer.t in + let b = ctx.builder in + let name = "completion" in + let ethis = mk (TConst TThis) coro_class.inside.cls_t coro_class.name_pos in + + let vargcompletion = alloc_var VGenerated name basic.tcoro.continuation coro_class.name_pos in + let evarargcompletion = b#local vargcompletion coro_class.name_pos in + let einitialstate = b#int initial_state coro_class.name_pos in + let esuper = b#call (b#super coro_class.inside.cont_type coro_class.name_pos) [ evarargcompletion; einitialstate ] basic.tvoid in + + let this_field cf = + b#instance_field ethis coro_class.cls coro_class.inside.param_types cf cf.cf_type + in + + let captured = + coro_class.captured + |> Option.map + (fun field -> + let vargcaptured = alloc_var VGenerated "captured" field.cf_type coro_class.name_pos in + let eargcaptured = b#local vargcaptured coro_class.name_pos in + let ecapturedfield = this_field field in + vargcaptured, b#assign ecapturedfield eargcaptured) + in + + (* If the coroutine field is not static then our HxCoro class needs to capture this for future resuming *) + + let eblock, tfun_args, tfunction_args = + let extra_exprs, extra_tfun_args, extra_tfunction_args = + captured |> + Option.map_default + (fun (v, expr) -> + [ expr ], + [ (v.v_name, false, v.v_type) ], + [ (v, None) ]) + ([], [], []) + in + + b#void_block (esuper :: extra_exprs), + extra_tfun_args @ [ (name, false, basic.tcoro.continuation) ], + extra_tfunction_args @ [ (vargcompletion, None) ] + in + + let field = mk_field "new" (TFun (tfun_args, basic.tvoid)) coro_class.name_pos coro_class.name_pos in + let func = TFunction { tf_type = basic.tvoid; tf_args = tfunction_args; tf_expr = eblock } in + let expr = mk func field.cf_type coro_class.name_pos in + field.cf_expr <- Some expr; + field.cf_kind <- Method MethNormal; + + if ctx.coro_debug then + s_expr_debug expr |> Printf.printf "%s\n"; + + field + + let mk_invoke_resume ctx coro_class = + let basic = ctx.typer.t in + let b = ctx.builder in + let tret_invoke_resume = coro_class.inside.cls_t in + let ethis = b#this coro_class.inside.cls_t coro_class.name_pos in + let ecorocall = + let this_field cf = + b#instance_field ethis coro_class.cls coro_class.inside.param_types cf cf.cf_type + in + match coro_class.coro_type with + | ClassField (cls, field, f, _) when has_class_field_flag field CfStatic -> + let args = (f.tf_args |> List.map (fun (v, _) -> Texpr.Builder.default_value v.v_type coro_class.name_pos)) @ [ ethis ] in + let efunction = Builder.make_static_field cls field coro_class.name_pos in + b#call efunction args tret_invoke_resume + | ClassField (cls, field,f, _) -> + let args = (f.tf_args |> List.map (fun (v, _) -> Texpr.Builder.default_value v.v_type coro_class.name_pos)) @ [ ethis ] in + let captured = coro_class.captured |> Option.get in + let ecapturedfield = this_field captured in + let efunction = b#instance_field ecapturedfield cls [] (* TODO: check *) field field.cf_type in + b#call efunction args tret_invoke_resume + | LocalFunc(f,_) -> + let args = (List.map (fun (v, _) -> Texpr.Builder.default_value v.v_type coro_class.name_pos) f.tf_args) @ [ ethis ] in + let captured = coro_class.captured |> Option.get in + let ecapturedfield = this_field captured in + b#call ecapturedfield args tret_invoke_resume + in + (* TODO: this is awkward, it would be better to avoid the entire expression and work with the correct types right away *) + let rec map_expr_type e = + Type.map_expr_type map_expr_type (substitute_type_params coro_class.type_param_subst) (fun v -> v) e + in + let ecorocall = map_expr_type ecorocall in + + let field = mk_field "invokeResume" (TFun ([], tret_invoke_resume)) coro_class.name_pos coro_class.name_pos in + add_class_field_flag field CfOverride; + let block = b#void_block [ b#return ecorocall ] in + let func = TFunction { tf_type = tret_invoke_resume; tf_args = []; tf_expr = block } in + let expr = mk (func) basic.tvoid coro_class.name_pos in + field.cf_expr <- Some expr; + field.cf_kind <- Method MethNormal; + + if ctx.coro_debug then + s_expr_debug expr |> Printf.printf "%s\n"; + + field +end + +let create_continuation_class ctx coro_class initial_state = + let ctor = ContinuationClassBuilder.mk_ctor ctx coro_class initial_state in + let resume = ContinuationClassBuilder.mk_invoke_resume ctx coro_class in + TClass.add_field coro_class.cls resume; + Option.may (TClass.add_field coro_class.cls) coro_class.captured; + coro_class.cls.cl_constructor <- Some ctor; + if ctx.coro_debug then + Printer.s_tclass "\t" coro_class.cls |> Printf.printf "%s\n"; + + ctx.typer.m.curmod.m_types <- ctx.typer.m.curmod.m_types @ [ TClassDecl coro_class.cls ] + +let coro_to_state_machine ctx coro_class cb_root exprs args vtmp vcompletion vcontinuation stack_item_inserter start_exception = + let basic = ctx.typer.t in + let b = ctx.builder in + let cont = coro_class.ContinuationClassBuilder.continuation_api in + let eloop, initial_state, fields = CoroToTexpr.block_to_texpr_coroutine ctx cb_root cont coro_class.cls coro_class.outside.param_types args [ vcompletion.v_id; vcontinuation.v_id ] exprs coro_class.name_pos stack_item_inserter start_exception in + (* update cf_type to use inside type parameters *) + List.iter (fun cf -> + cf.cf_type <- substitute_type_params coro_class.type_param_subst cf.cf_type; + TClass.add_field coro_class.cls cf + ) fields; + create_continuation_class ctx coro_class initial_state; + let continuation_var = b#var_init_null vcontinuation in + + let std_is e t = + let type_expr = mk (TTypeExpr (module_type_of_type t)) t_dynamic coro_class.name_pos in + Texpr.Builder.resolve_and_make_static_call ctx.typer.com.std "isOfType" [e;type_expr] coro_class.name_pos + in + + let prefix_arg = + match coro_class.coro_type with + | ClassField (_, field, _, _) when has_class_field_flag field CfStatic -> + [] + | ClassField _ -> + [ b#this ctx.typer.c.tthis coro_class.name_pos ] + | LocalFunc(f,v) -> + [ b#local v coro_class.name_pos ] + in + + let {CoroToTexpr.econtinuation;ecompletion;estate;eresult;egoto;eerror} = exprs in + + let continuation_assign = + let t = coro_class.outside.cls_t in + + let ecastedcompletion = mk_cast ecompletion t coro_class.name_pos in + + let tcond = + let erecursingfield = b#instance_field ecastedcompletion coro_class.cls coro_class.outside.param_types cont.recursing basic.tbool in + let estdis = std_is ecompletion t in + let erecursingcheck = b#op_eq erecursingfield (b#bool false coro_class.name_pos) in + b#op_bool_and estdis erecursingcheck + in + let tif = b#assign econtinuation ecastedcompletion in + let tif = b#void_block [tif] in + let ctor_args = prefix_arg @ [ ecompletion ] in + let telse = b#assign econtinuation (mk (TNew (coro_class.cls, coro_class.outside.param_types, ctor_args)) t coro_class.name_pos) in + b#if_then_else tcond tif telse basic.tvoid + in + + let continuation_field cf t = + b#instance_field econtinuation coro_class.cls coro_class.outside.param_types cf t + in + b#void_block [ + continuation_var; + continuation_assign; + b#assign + (continuation_field cont.recursing basic.tbool) + (b#bool true coro_class.name_pos); + b#var_init vtmp eresult; + eloop; + b#return (b#null basic.tany coro_class.name_pos); + ] + +let coro_to_normal ctx coro_class cb_root exprs vcontinuation = + let open ContinuationClassBuilder in + let open CoroToTexpr in + let basic = ctx.typer.t in + let b = ctx.builder in + create_continuation_class ctx coro_class 0; + let rec loop cb previous_el = + let bad_pos = coro_class.name_pos in + let loop cb el = + if not (has_block_flag cb CbGenerated) then begin + add_block_flag cb CbGenerated; + loop cb el + end else + el,false + in + let loop_as_block cb = + let el,term = loop cb [] in + b#void_block el,term + in + let current_el = ref (previous_el @ (get_block_exprs cb)) in + let continue cb_next e = + loop cb_next (!current_el @ [e]) + in + let maybe_continue cb_next term e = match cb_next with + | Some cb_next when not term -> + continue cb_next e + | _ -> + (!current_el @ [e]),true + in + let add e = current_el := !current_el @ [e] in + let terminate e = + add e; + !current_el,true + in + begin match cb.cb_next with + | NextSub(cb_sub,cb_next) -> + let e_next,term = loop_as_block cb_sub in + maybe_continue cb_next term e_next + | NextReturn e1 -> + let e1 = coro_class.continuation_api.immediate_result e1 in + terminate (b#return e1); + | NextThrow e1 -> + if ctx.throw then + terminate (b#throw e1) + else begin + let e1 = coro_class.continuation_api.immediate_error e1 coro_class.inside.result_type in + terminate (b#return e1); + end + | NextUnknown | NextReturnVoid -> + let e1 = coro_class.continuation_api.immediate_result (b#null t_dynamic coro_class.name_pos) in + terminate (b#return e1); + | NextBreak _ -> + terminate (b#break bad_pos); + | NextContinue _ -> + terminate (b#continue bad_pos); + | NextIfThen(e1,cb_then,cb_next) -> + let e_then,_ = loop_as_block cb_then in + let e_if = b#if_then e1 e_then in + continue cb_next e_if + | NextIfThenElse(e1,cb_then,cb_else,cb_next) -> + let e_then,term_then = loop_as_block cb_then in + let e_else,term_else = loop_as_block cb_else in + let e_if = b#if_then_else e1 e_then e_else basic.tvoid in + maybe_continue cb_next (term_then && term_else) e_if + | NextSwitch(switch,cb_next) -> + let term = ref true in + let p = ref switch.cs_subject.epos in + let switch_cases = List.map (fun (el,cb) -> + let e,term' = loop_as_block cb in + term := !term && term'; + p := Ast.punion !p e.epos; + { + case_patterns = el; + case_expr = e; + } + ) switch.cs_cases in + let switch_default = Option.map (fun cb -> + let e,term' = loop_as_block cb in + p := Ast.punion !p e.epos; + term := !term && term'; + e + ) switch.cs_default in + let switch = { + switch_subject = switch.cs_subject; + switch_cases; + switch_default; + switch_exhaustive = switch.cs_exhaustive + } in + maybe_continue cb_next (switch.switch_exhaustive && !term) (mk (TSwitch switch) basic.tvoid !p) + | NextWhile(e1,cb_body,cb_next) -> + let e_body,_ = loop_as_block cb_body in + let e_while = mk (TWhile(e1,e_body,NormalWhile)) basic.tvoid (Ast.punion e1.epos e_body.epos) in + maybe_continue cb_next false e_while + | NextTry(cb_try,catches,cb_next) -> + let e_try,term = loop_as_block cb_try in + let p = ref e_try.epos in + let term = ref term in + let catches = List.map (fun (v,cb) -> + let e,term' = loop_as_block cb in + p := Ast.punion !p e.epos; + term := !term && term'; + (v,e) + ) catches.cc_catches in + let e_try = mk (TTry(e_try,catches)) basic.tvoid !p in + maybe_continue cb_next !term e_try + | NextFallThrough _ | NextGoto _ -> + !current_el,false + | NextSuspend(suspend,cb_next) -> + let e_sus = CoroToTexpr.make_suspending_call basic suspend {exprs.ecompletion with epos = suspend.cs_pos} in + add (mk (TReturn (Some e_sus)) t_dynamic e_sus.epos); + !current_el,true + end + in + let el,_ = loop cb_root [] in + let e = b#void_block el in + let e = if ctx.throw || ctx.nothrow then + e + else begin + let catch = + let v = alloc_var VGenerated "e" t_dynamic e.epos in + let ev = b#local v e.epos in + let eerr = coro_class.continuation_api.immediate_error ev coro_class.inside.result_type in + let eret = b#return eerr in + (v,eret) + in + mk (TTry(e,[catch])) basic.tvoid e.epos + end in + b#void_block [e] + +let fun_to_coro ctx coro_type = + let basic = ctx.typer.t in + let b = ctx.builder in + + let coro_class = ContinuationClassBuilder.create ctx coro_type in + let cont = coro_class.continuation_api in + + (* Generate and assign the continuation variable *) + let vcompletion = alloc_var VGenerated "_hx_completion" basic.tcoro.continuation coro_class.name_pos in + let ecompletion = b#local vcompletion coro_class.name_pos in + + let vcontinuation = alloc_var VGenerated "_hx_continuation" coro_class.outside.cls_t coro_class.name_pos in + let econtinuation = b#local vcontinuation coro_class.name_pos in + + let continuation_field cf t = + b#instance_field econtinuation basic.tcoro.base_continuation_class coro_class.outside.param_types cf t + in + + let egoto = continuation_field cont.goto_label basic.tint in + let estate = continuation_field cont.state basic.tcoro.suspension_state in + let eresult = continuation_field cont.result basic.tany in + let eerror = continuation_field cont.error basic.texception in + + let vtmp = alloc_var VGenerated "_hx_tmp" basic.tany coro_class.name_pos in + let etmp = b#local vtmp coro_class.name_pos in + + let expr, args, name = + match coro_type with + | ClassField (_, cf, f, p) -> + f.tf_expr, f.tf_args, cf.cf_name + | LocalFunc(f,v) -> + f.tf_expr, f.tf_args, v.v_name + in + + let cb_root = make_block ctx (Some(expr.etype, coro_class.name_pos)) in + + ignore(CoroFromTexpr.expr_to_coro ctx etmp cb_root expr); + let exprs = {CoroToTexpr.econtinuation;ecompletion;estate;eresult;egoto;eerror;etmp} in + let stack_item_inserter pos = + let field, eargs = + match coro_type with + | ClassField (cls, field, _, _) -> + PMap.find "setClassFuncStackItem" basic.tcoro.base_continuation_class.cl_fields, + [ + b#string (s_class_path cls) coro_class.name_pos; + b#string field.cf_name coro_class.name_pos; + ] + | LocalFunc (f, v) -> + PMap.find "setLocalFuncStackItem" basic.tcoro.base_continuation_class.cl_fields, + [ + b#int v.v_id coro_class.name_pos; + ] + in + let eaccess = continuation_field field field.cf_type in + let l1,c1,_,_ = Lexer.get_pos_coords pos in + let eargs = eargs @ [ + b#string pos.pfile coro_class.name_pos; + b#int l1 coro_class.name_pos; + b#int c1 coro_class.name_pos; + b#int pos.pmin coro_class.name_pos; + b#int pos.pmax coro_class.name_pos; + ] in + mk (TCall (eaccess, eargs)) basic.tvoid coro_class.name_pos + in + let start_exception = + let cf = PMap.find "startException" basic.tcoro.base_continuation_class.cl_fields in + let ef = continuation_field cf cf.cf_type in + (fun e -> + mk (TCall(ef,[e])) basic.tvoid coro_class.name_pos + ) + in + let tf_expr,cb_root = try + let cb_root = if ctx.optimize then CoroFromTexpr.optimize_cfg ctx cb_root else cb_root in + coro_to_state_machine ctx coro_class cb_root exprs args vtmp vcompletion vcontinuation stack_item_inserter start_exception, cb_root + with CoroTco cb_root -> + coro_to_normal ctx coro_class cb_root exprs vcontinuation,cb_root + in + + let tf_args = args @ [ (vcompletion,None) ] in + (* I'm not sure what this should be, but let's stick to the widest one for now. + Cpp dies if I try to use coro_class.outside.cls_t here, which might be something + to investigate independently. *) + let tf_type = basic.tcoro.suspension_result coro_class.outside.result_type in + if ctx.coro_debug then begin + print_endline ("BEFORE:\n" ^ (s_expr_debug expr)); + CoroDebug.create_dotgraph (DotGraph.get_dump_path (SafeCom.of_com ctx.typer.com) (ctx.typer.c.curclass.cl_path) name) cb_root + end; + let e = mk (TFunction {tf_args; tf_expr; tf_type}) (TFun (tf_args |> List.map (fun (v, _) -> (v.v_name, false, v.v_type)), tf_type)) tf_expr.epos in + if ctx.coro_debug then print_endline ("AFTER:\n" ^ (s_expr_debug e)); + e + +let create_coro_context typer meta = + let optimize = not (Define.raw_defined typer.Typecore.com.defines "coroutine.noopt") in + let builder = new CoroElsewhere.texpr_builder typer.t in + let ctx = { + builder; + typer; + coro_debug = Meta.has (Meta.Custom ":coroutine.debug") meta; + optimize; + allow_tco = optimize && not (Meta.has (Meta.Custom ":coroutine.notco") meta); + throw = Define.raw_defined typer.com.defines "coroutine.throw"; + nothrow = Meta.has (Meta.Custom ":coroutine.nothrow") meta; + vthis = None; + next_block_id = 0; + current_catch = None; + has_catch = false; + } in + ctx diff --git a/src/coro/coroControl.ml b/src/coro/coroControl.ml new file mode 100644 index 00000000000..fcd58308d31 --- /dev/null +++ b/src/coro/coroControl.ml @@ -0,0 +1,33 @@ +open Globals +open Type +open Texpr + +type coro_control = + | CoroPending + | CoroReturned + | CoroThrown + +let mk_int basic i = Texpr.Builder.make_int basic i null_pos + +let mk_control basic (c : coro_control) = mk_int basic (Obj.magic c) + +let make_custom_control_switch basic e_subject cases p = + let cases = List.map (fun (l,e) -> { + case_patterns = List.map (mk_control basic) l; + case_expr = e; + }) cases in + let switch = { + switch_subject = e_subject; + switch_cases = cases; + switch_default = None; + switch_exhaustive = true; + } in + mk (TSwitch switch) basic.tvoid p + +let make_control_switch basic e_subject e_pending e_returned e_thrown p = + let cases = [ + [CoroPending],e_pending; + [CoroReturned],e_returned; + [CoroThrown],e_thrown; + ] in + make_custom_control_switch basic e_subject cases p \ No newline at end of file diff --git a/src/coro/coroDebug.ml b/src/coro/coroDebug.ml new file mode 100644 index 00000000000..12ff8bc1f3a --- /dev/null +++ b/src/coro/coroDebug.ml @@ -0,0 +1,94 @@ + +open CoroTypes +open CoroFunctions +open Type + +let create_dotgraph path cb = + print_endline (String.concat "." path); + let ch,close = DotGraph.start_graph path ".coro" in + let pctx = print_context() in + let st = s_type pctx in + let se = s_expr_pretty true "" false st in + let edges = DynArray.create () in + let rec block cb = + let edge_block label cb_target = + block cb_target; + DynArray.add edges (cb.cb_id,cb_target.cb_id,label,true); + in + let maybe_edge_block label = Option.may (edge_block label) in + let s = String.concat "\n" (DynArray.to_list (DynArray.map se cb.cb_el)) in + let flags = if has_block_flag cb CbResumeState then " resume" else if has_block_flag cb CbSuspendState then " suspend" else "" in + let s = if s = "" then Printf.sprintf "(%i%s)" cb.cb_id flags else Printf.sprintf "(%i%s)\n%s" cb.cb_id flags s in + let snext = match cb.cb_next with + | NextUnknown -> + None + | NextSub(cb_sub,cb_next) -> + maybe_edge_block "next" cb_next; + edge_block "sub" cb_sub; + None + | NextBreak cb_break -> + DynArray.add edges (cb.cb_id,cb_break.cb_id,"goto",false); + Some "break" + | NextContinue cb_continue -> + DynArray.add edges (cb.cb_id,cb_continue.cb_id,"goto",false); + Some "continue" + | NextReturnVoid -> + Some "return" + | NextReturn e -> + Some ("return " ^ se e) + | NextThrow e -> + Some ("throw " ^ se e) + | NextIfThen(e,cb_then,cb_next) -> + edge_block "next" cb_next; + edge_block "then" cb_then; + Some ("if " ^ se e) + | NextIfThenElse(e,cb_then,cb_else,cb_next) -> + maybe_edge_block "next" cb_next; + edge_block "then" cb_then; + edge_block "else" cb_else; + Some ("if " ^ se e) + | NextSwitch(switch,cb_next) -> + maybe_edge_block "next" cb_next; + List.iter (fun (el,cb_case) -> + edge_block (String.concat " | " (List.map se el)) cb_case + ) switch.cs_cases; + Option.may (fun cb_default -> edge_block "default" cb_default) switch.cs_default; + Some ("switch " ^ se switch.cs_subject) + | NextWhile(e,cb_body,cb_next) -> + maybe_edge_block "next" cb_next; + edge_block "body" cb_body; + Some ("while " ^ se e) + | NextTry(cb_try,catch,cb_next) -> + maybe_edge_block "next" cb_next; + edge_block "try" cb_try; + DynArray.add edges (cb_try.cb_id,catch.cc_cb.cb_id,"catch",true); + Printf.fprintf ch "n%i [shape=box,label=\"(%i)\"];\n" catch.cc_cb.cb_id catch.cc_cb.cb_id; + List.iter (fun (v,cb_catch) -> + block cb_catch; + DynArray.add edges (catch.cc_cb.cb_id,cb_catch.cb_id,(st v.v_type),true); + ) catch.cc_catches; + None + | NextSuspend(suspend,cb_next) -> + maybe_edge_block "next" cb_next; + Some (Printf.sprintf "%s(%s)" (se suspend.cs_fun) (String.concat ", " (List.map se suspend.cs_args))) + | NextFallThrough cb_next -> + DynArray.add edges (cb.cb_id,cb_next.cb_id,"fall-through",false); + None + | NextGoto cb_next -> + DynArray.add edges (cb.cb_id,cb_next.cb_id,"goto",false); + None + in + let s = match snext with + | None -> + s + | Some snext -> + if s = "" then snext else s ^ "\n" ^ snext + in + Printf.fprintf ch "n%i [shape=box,label=\"%s\"];\n" cb.cb_id (StringHelper.s_escape s); + in + ignore(block cb); + DynArray.iter (fun (id_from,id_to,label,tree_edge) -> + let style = if tree_edge then "style=\"solid\",color=\"black\"" else "style=\"dashed\", color=\"lightgray\"" in + Printf.fprintf ch "n%i -> n%i[%s label=\"%s\"];\n" id_from id_to style (StringHelper.s_escape label); + ) edges; + close(); \ No newline at end of file diff --git a/src/coro/coroElsewhere.ml b/src/coro/coroElsewhere.ml new file mode 100644 index 00000000000..0e71eec3f60 --- /dev/null +++ b/src/coro/coroElsewhere.ml @@ -0,0 +1,77 @@ +(* + Code that should eventually be moved elsewhere. +*) + +open Globals +open Ast +open Type + +class texpr_builder (basic : basic_types) = +object(self) + method assign (lhs : texpr) (rhs : texpr) = + mk (TBinop(OpAssign,lhs,rhs)) lhs.etype (punion lhs.epos rhs.epos) + + method binop (op : binop) (lhs : texpr) (rhs : texpr) (t : Type.t) = + mk (TBinop(op,lhs,rhs)) t (punion lhs.epos rhs.epos) + + method bool (b : bool) (p : pos) = + mk (TConst (TBool b)) basic.tbool p + + method break (p : pos) = + mk TBreak t_dynamic p + + method call (e1 : texpr) (el : texpr list) (tret : Type.t) = + mk (TCall(e1,el)) tret (punion e1.epos (punion_el e1.epos el)) + + method continue (p : pos) = + mk TContinue t_dynamic p + + method local (v : tvar) (p : pos) = + mk (TLocal v) v.v_type p + + method if_then (eif : texpr) (ethen : texpr) = + mk (TIf(eif,ethen,None)) basic.tvoid (punion eif.epos ethen.epos) + + method if_then_else (eif : texpr) (ethen : texpr) (eelse : texpr) (t : Type.t) = + mk (TIf(eif,ethen,Some eelse)) t (punion eif.epos eelse.epos) + + method instance_field (e : texpr) (c : tclass) (params : Type.t list) (cf : tclass_field) (t : Type.t) = + mk (TField(e,FInstance(c,params,cf))) t e.epos + + method int (i : int) (p : pos) = + mk (TConst (TInt (Int32.of_int i))) basic.tint p + + method null (t : Type.t) (p : pos) = + mk (TConst TNull) t p + + method op_bool_and (e1 : texpr) (e2 : texpr) = + self#binop OpBoolAnd e1 e2 basic.tbool + + method op_eq (e1 : texpr) (e2 : texpr) = + self#binop OpEq e1 e2 basic.tbool + + method return (e : texpr) = + mk (TReturn (Some e)) t_dynamic e.epos + + method string (s : string) (p : pos) = + mk (TConst (TString s)) basic.tstring p + + method super (t: Type.t) (p : pos) = + mk (TConst TSuper) t p + + method this (t : Type.t) (p : pos) = + mk (TConst TThis) t p + + method throw (e : texpr) = + mk (TThrow e) t_dynamic e.epos + + method var_init (v : tvar) (e : texpr) = + mk (TVar(v,Some e)) basic.tvoid (punion v.v_pos e.epos) + + method var_init_null (v : tvar) = + self#var_init v (self#null v.v_type v.v_pos) + + method void_block (el : texpr list) = + mk (TBlock el) basic.tvoid (Texpr.punion_el null_pos el) + +end \ No newline at end of file diff --git a/src/coro/coroFromTexpr.ml b/src/coro/coroFromTexpr.ml new file mode 100644 index 00000000000..f3b14297627 --- /dev/null +++ b/src/coro/coroFromTexpr.ml @@ -0,0 +1,486 @@ +open Globals +open Type +open CoroTypes +open CoroFunctions + +let e_no_value = Texpr.Builder.make_null t_dynamic null_pos + +type coro_ret = + | RLocal of tvar + | RTerminate of (coro_block -> texpr -> unit) + | RValue + | RBlock + +let expr_to_coro ctx etmp cb_root e = + let ordered_value_marker = ref false in + let start_ordered_value_list () = + let old = !ordered_value_marker in + (fun () -> + let cur = !ordered_value_marker in + ordered_value_marker := old; + cur + ) + in + let make_block typepos = + make_block ctx typepos + in + let block_from_e e = + make_block (Some(e.etype,e.epos)) + in + let has_side_effect e = match e.eexpr with + | TVar _ -> + (* has_side_effect doesn't consider var declarations a side effect which may just be wrong *) + true + | _ -> + OptimizerTexpr.has_side_effect e + in + let add_expr cb e = + if cb.cb_next = NextUnknown && e != e_no_value && has_side_effect e then + DynArray.add cb.cb_el e + in + let terminate cb kind t p = + if cb.cb_next = NextUnknown then + cb.cb_next <- kind; + in + let fall_through cb_from cb_to = + terminate cb_from (NextFallThrough cb_to) t_dynamic null_pos + in + let goto cb_from cb_to = + terminate cb_from (NextGoto cb_to) t_dynamic null_pos + in + let loop_stack = ref [] in + let rec loop cb ret e = match e.eexpr with + (* special cases *) + | TConst TThis -> + Some (cb,e) + (* simple values *) + | TConst _ | TLocal _ | TTypeExpr _ | TIdent _ -> + Some (cb,e) + (* compound values *) + | TBlock [e1] -> + loop cb ret e1 + | TBlock el -> + let cb_sub = block_from_e e in + let ret = match ret,el with + | RValue,_ :: _ -> + (* + If we have a multi-element block in a value-place we might need a temp var + because the result expression might reference local variables declared in + that block (https://github.com/Aidan63/haxe/issues/79). + *) + let v = alloc_var VGenerated "tmp" e.etype e.epos in + add_expr cb {e with eexpr = TVar(v,None)}; + RLocal v + | _ -> + ret + in + let sub_next = loop_block cb_sub ret e in + let cb_next = match sub_next with + | None -> + None + | Some (cb_sub_next,e1) -> + let cb_next = make_block None in + fall_through cb_sub_next cb_next; + Some (cb_next,e1) + in + terminate cb (NextSub(cb_sub,Option.map fst cb_next)) e.etype e.epos; + cb_next + | TArray(e1,e2) -> + let cb = ordered_loop cb [e1;e2] in + Option.map (fun (cb,el) -> match el with + | [e1;e2] -> + (cb,{e with eexpr = TArray(e1,e2)}) + | _ -> + die "" __LOC__ + ) cb + | TArrayDecl el -> + let cb = ordered_loop cb el in + Option.map (fun (cb,el) -> (cb,{e with eexpr = TArrayDecl el})) cb + | TObjectDecl fl -> + let cb = ordered_loop cb (List.map snd fl) in + Option.map (fun (cb,el) -> + let fl = List.map2 (fun (f,_) e -> (f,e)) fl el in + (cb,{e with eexpr = TObjectDecl fl}) + ) cb + | TField(e1,fa) -> + (* TODO: this is quite annoying because factoring out field access behaves very creatively on + some targets. This means that (coroCall()).field doesn't work (and isn't tested). *) + Some (cb,e) + | TEnumParameter(e1,ef,i) -> + let cb = loop cb RValue e1 in + Option.map (fun (cb,e) -> (cb,{e with eexpr = TEnumParameter(e1,ef,i)})) cb + | TEnumIndex e1 -> + let cb = loop cb RValue e1 in + Option.map (fun (cb,e1) -> (cb,{e with eexpr = TEnumIndex e1})) cb + | TNew(c,tl,el) -> + let cb = ordered_loop cb el in + Option.map (fun (cb,e1) -> cb,{e with eexpr = TNew(c,tl,el)}) cb + (* rewrites & forwards *) + | TCast(e1,o) -> + let cb = loop cb ret e1 in + Option.map (fun (cb,e1) -> (cb,{e with eexpr = TCast(e1,o)})) cb + | TParenthesis e1 -> + let cb = loop cb ret e1 in + Option.map (fun (cb,e1) -> (cb,{e with eexpr = TParenthesis e1})) cb + | TMeta(meta,e1) -> + let cb = loop cb ret e1 in + Option.map (fun (cb,e1) -> (cb,{e with eexpr = TMeta(meta,e1)})) cb + | TUnop(op,flag,e1) -> + let cb = loop cb ret (* TODO: is this right? *) e1 in + Option.map (fun (cb,e1) -> (cb,{e with eexpr = TUnop(op,flag,e1)})) cb + | TBinop(OpAssign,({eexpr = TLocal v} as e1),e2) -> + let cb = loop_assign cb (RLocal v) e2 in + Option.map (fun (cb,e2) -> (cb,{e with eexpr = TBinop(OpAssign,e1,e2)})) cb + (* TODO: OpAssignOp and other OpAssign *) + | TBinop(op,e1,e2) -> + let cb = loop cb RValue e1 in + begin match cb with + | None -> + None + | Some (cb,e1) -> + let cb2 = loop cb RValue e2 in + begin match cb2 with + | None -> + add_expr cb e1; + None + | Some (cb,e2) -> + Some (cb,{e with eexpr = TBinop(op,e1,e2)}) + end + end + (* variables *) + | TVar(v,None) -> + add_expr cb e; + Some (cb,e_no_value) + | TVar(v,Some e1) -> + add_expr cb {e with eexpr = TVar(v,None)}; + let cb = loop_assign cb (RLocal v) e1 in + cb + (* calls *) + | TCall(e1,el) -> + let cb = ordered_loop cb (e1 :: el) in + Option.map (fun (cb,el) -> + begin match el with + | e1 :: el -> + begin match follow_with_coro e1.etype with + | Coro _ -> + let cb_next = block_from_e e1 in + add_block_flag cb_next CbResumeState; + let suspend = { + cs_fun = e1; + cs_args = el; + cs_pos = e.epos + } in + add_block_flag cb CbSuspendState; + terminate cb (NextSuspend(suspend,Some cb_next)) t_dynamic null_pos; + cb_next,etmp + | _ -> + cb,{e with eexpr = TCall(e1,el)} + end + | [] -> + die "" __LOC__ + end + ) cb + (* terminators *) + | TBreak -> + terminate cb (NextBreak (Lazy.force (snd (List.hd !loop_stack)))) e.etype e.epos; + None + | TContinue -> + terminate cb (NextContinue (fst (List.hd !loop_stack))) e.etype e.epos; + None + | TReturn None -> + terminate cb NextReturnVoid e.etype e.epos; + None + | TReturn (Some e1) -> + let f_terminate cb e1 = + terminate cb (NextReturn e1) e.etype e.epos; + in + let ret = RTerminate f_terminate in + let cb_ret = loop_assign cb ret e1 in + Option.may (fun (cb_ret,e1) -> terminate cb_ret (NextReturn e1) e.etype e.epos) cb_ret; + None + | TThrow e1 -> + let f_terminate cb e1 = + terminate cb (NextThrow e1) e.etype e.epos; + in + let ret = RTerminate f_terminate in + let cb_ret = loop_assign cb ret e1 in + Option.may (fun (cb_ret,e1) -> terminate cb_ret (NextThrow e1) e.etype e.epos) cb_ret; + None + (* branching *) + | TIf(e1,e2,None) -> + let cb = loop cb RValue e1 in + Option.map (fun (cb,e1) -> + let cb_then = block_from_e e2 in + let cb_then_next = loop_block cb_then RBlock e2 in + let cb_next = make_block None in + Option.may (fun (cb_then_next,_) -> fall_through cb_then_next cb_next) cb_then_next; + terminate cb (NextIfThen(e1,cb_then,cb_next)) e.etype e.epos; + cb_next,e_no_value + ) cb + | TIf(e1,e2,Some e3) -> + let cb = loop cb RValue e1 in + begin match cb with + | None -> + None + | Some(cb,e1) -> + let cb_then = block_from_e e2 in + let cb_then_next = loop_block cb_then ret e2 in + let cb_else = block_from_e e3 in + let cb_else_next = loop_block cb_else ret e3 in + let cb_next = match cb_then_next,cb_else_next with + | Some (cb_then_next,_),Some(cb_else_next,_) -> + let cb_next = make_block None in + fall_through cb_then_next cb_next; + fall_through cb_else_next cb_next; + Some cb_next + | (Some (cb_branch_next,_),None) | (None,Some (cb_branch_next,_)) -> + let cb_next = make_block None in + fall_through cb_branch_next cb_next; + Some cb_next + | None,None -> + None + in + terminate cb (NextIfThenElse(e1,cb_then,cb_else,cb_next)) e.etype e.epos; + Option.map (fun cb_next -> (cb_next,e_no_value)) cb_next + end + | TSwitch switch -> + let e1 = switch.switch_subject in + let cb = loop cb RValue e1 in + begin match cb with + | None -> + None + | Some(cb,e1) -> + let cb_next = lazy (make_block None) in + let cases = List.map (fun case -> + let cb_case = block_from_e case.case_expr in + let cb_case_next = loop_block cb_case ret case.case_expr in + Option.may (fun (cb_case_next,_) -> + fall_through cb_case_next (Lazy.force cb_next); + ) cb_case_next; + (case.case_patterns,cb_case) + ) switch.switch_cases in + let def = match switch.switch_default with + | None -> + None + | Some e -> + let cb_default = block_from_e e in + let cb_default_next = loop_block cb_default ret e in + Option.may (fun (cb_default_next,_) -> + fall_through cb_default_next (Lazy.force cb_next); + ) cb_default_next; + Some cb_default + in + let switch = { + cs_subject = e1; + cs_cases = cases; + cs_default = def; + cs_exhaustive = switch.switch_exhaustive + } in + let cb_next = if Lazy.is_val cb_next || not switch.cs_exhaustive then Some (Lazy.force cb_next) else None in + terminate cb (NextSwitch(switch,cb_next)) e.etype e.epos; + Option.map (fun cb_next -> (cb_next,e_no_value)) cb_next + end + | TWhile(e1,e2,flag) when not (is_true_expr e1) -> + loop cb ret (Texpr.not_while_true_to_while_true ctx.typer.com.Common.basic e1 e2 flag e.etype e.epos) + | TWhile(e1,e2,flag) (* always while(true) *) -> + let cb_next = lazy (make_block None) in + let cb_body = block_from_e e2 in + loop_stack := (cb_body,cb_next) :: !loop_stack; + let cb_body_next = loop_block cb_body RBlock e2 in + Option.may (fun (cb_body_next,_) -> goto cb_body_next cb_body) cb_body_next; + loop_stack := List.tl !loop_stack; + let cb_next = if Lazy.is_val cb_next then Some (Lazy.force cb_next) else None in + terminate cb (NextWhile(e1,cb_body,cb_next)) e.etype e.epos; + Option.map (fun cb_next -> (cb_next,e_no_value)) cb_next + | TTry(e1,catches) -> + ctx.has_catch <- true; + let cb_next = lazy (make_block None) in + let catches = List.map (fun (v,e) -> + let cb_catch = block_from_e e in + add_expr cb_catch (mk (TVar(v,Some etmp)) ctx.typer.t.tvoid null_pos); + let cb_catch_next = loop_block cb_catch ret e in + Option.may (fun (cb_catch_next,_) -> + fall_through cb_catch_next (Lazy.force cb_next); + ) cb_catch_next; + v,cb_catch + ) catches in + let catch = make_block None in + (* This block is handled in a special way in the texpr transformer, let's mark it as + already generated so we don't generate it twice. *) + add_block_flag catch CbGenerated; + let old = ctx.current_catch in + ctx.current_catch <- Some catch; + let catch = { + cc_cb = catch; + cc_catches = catches; + } in + let cb_try = block_from_e e1 in + let cb_try_next = loop_block cb_try ret e1 in + ctx.current_catch <- old; + Option.may (fun (cb_try_next,_) -> + fall_through cb_try_next (Lazy.force cb_next) + ) cb_try_next; + let cb_next = if Lazy.is_val cb_next then Some (Lazy.force cb_next) else None in + terminate cb (NextTry(cb_try,catch,cb_next)) e.etype e.epos; + Option.map (fun cb_next -> (cb_next,e_no_value)) cb_next + | TFunction tf -> + Some (cb,e) + and ordered_loop cb el = + let close = start_ordered_value_list () in + let rec aux' cb acc el = match el with + | [] -> + Some (cb,List.rev acc) + | e :: el -> + let cb' = loop cb RValue e in + match cb' with + | None -> + List.iter (fun e -> + add_expr cb e + ) (List.rev acc); + None + | Some (cb,e) -> + aux' cb (e :: acc) el + in + let cb = aux' cb [] el in + let _ = close () in + cb + and loop_assign cb ret e = + let cb = loop cb ret e in + match cb with + | Some (cb,e) when e != e_no_value -> + begin match ret with + | RBlock -> + add_expr cb e; + Some (cb,e_no_value) + | RValue -> + Some (cb,e) + | RLocal v -> + let ev = Texpr.Builder.make_local v v.v_pos in + let eass = Texpr.Builder.binop OpAssign ev e ev.etype ev.epos in + add_expr cb eass; + Some (cb,ev) + | RTerminate f -> + f cb e; + None + end + | Some(cb,e) -> + Some(cb,e) + | None -> + None + and loop_block cb ret e = + let el = match e.eexpr with + | TBlock el -> + el + | _ -> + [e] + in + let rec aux' cb el = match el with + | [] -> + assert false + | [e] -> + loop_assign cb ret e + | e :: el -> + let cb = loop cb RBlock e in + begin match cb with + | None -> + None + | Some(cb,e) -> + add_expr cb e; + aux' cb el + end + in + match el with + | [] -> + None + | _ -> + aux' cb el + in + loop_block cb_root RBlock e + +let optimize_cfg ctx cb = + let forward_el cb_from cb_to = + if DynArray.length cb_from.cb_el > 0 then begin + if DynArray.length cb_to.cb_el = 0 then begin + DynArray.iter (fun e -> DynArray.add cb_to.cb_el e) cb_from.cb_el + end else begin + let e = mk (TBlock (DynArray.to_list cb_from.cb_el)) ctx.typer.t.tvoid null_pos in + DynArray.set cb_to.cb_el 0 (concat e (DynArray.get cb_to.cb_el 0)) + end + end + in + (* first pass: find empty blocks and store their replacement*) + let forward = Array.make ctx.next_block_id None in + let rec loop cb = + if not (has_block_flag cb CbEmptyMarked) then begin + add_block_flag cb CbEmptyMarked; + match cb.cb_next with + | NextSub(cb_sub,None) -> + loop cb_sub; + forward_el cb cb_sub; + if has_block_flag cb CbResumeState then add_block_flag cb_sub CbResumeState; + forward.(cb.cb_id) <- Some cb_sub + | NextFallThrough cb_next | NextGoto cb_next when DynArray.empty cb.cb_el && not (has_block_flag cb CbResumeState) -> + loop cb_next; + forward.(cb.cb_id) <- Some cb_next + | _ -> + coro_iter loop cb + end + in + loop cb; + (* second pass: map graph to skip forwarding block *) + let rec loop cb = match forward.(cb.cb_id) with + | Some cb -> + loop cb + | None -> + if not (has_block_flag cb CbForwardMarked) then begin + add_block_flag cb CbForwardMarked; + coro_next_map loop cb; + end; + cb + in + let cb = loop cb in + let is_empty_termination_block cb = match cb with + | None -> + true + | Some cb -> + DynArray.empty cb.cb_el && match cb.cb_next with + | NextReturnVoid | NextUnknown -> + true + | _ -> + false + in + let rec loop cb = + if not (has_block_flag cb CbTcoChecked) then begin + add_block_flag cb CbTcoChecked; + begin match cb.cb_next with + | NextSuspend(_,cb_next) -> + if not (is_empty_termination_block cb_next) then + raise Exit; + | _ -> + () + end; + coro_iter loop cb; + end + in + if ctx.allow_tco && not ctx.has_catch then + (try loop cb; raise (CoroTco cb) with Exit -> ()); + (* third pass: reindex cb_id for tighter switches. Breadth-first because that makes the numbering more natural, maybe. *) + let i = ref 0 in + let queue = Queue.create () in + Queue.push cb queue; + let rec loop () = + if not (Queue.is_empty queue) then begin + let cb = Queue.pop queue in + if not (has_block_flag cb CbReindexed) then begin + add_block_flag cb CbReindexed; + cb.cb_id <- !i; + incr i; + coro_iter (fun cb -> Queue.add cb queue) cb; + Option.may (fun cb -> Queue.add cb queue) cb.cb_catch; + end; + loop () + end + in + loop (); + ctx.next_block_id <- !i; + cb \ No newline at end of file diff --git a/src/coro/coroFunctions.ml b/src/coro/coroFunctions.ml new file mode 100644 index 00000000000..f84c942cf0d --- /dev/null +++ b/src/coro/coroFunctions.ml @@ -0,0 +1,122 @@ +open Globals +open Type +open CoroTypes + +let make_block ctx typepos = + let id = ctx.next_block_id in + ctx.next_block_id <- ctx.next_block_id + 1; + { + cb_id = id; + cb_el = DynArray.create (); + cb_typepos = typepos; + cb_next = NextUnknown; + cb_catch = ctx.current_catch; + cb_flags = 0; + } + +let add_block_flag cb (flag : cb_flag) = + cb.cb_flags <- set_flag cb.cb_flags (Obj.magic flag) + +let has_block_flag cb (flag : cb_flag) = + has_flag cb.cb_flags (Obj.magic flag) + +let get_block_exprs cb = + let rec loop idx acc = + if idx < 0 then + acc + else begin + let acc = match DynArray.unsafe_get cb.cb_el idx with + | {eexpr = TBlock el} -> + el @ acc + | e -> + e :: acc + in + loop (idx - 1) acc + end in + loop (DynArray.length cb.cb_el - 1) [] + +let coro_iter f cb = + let fo = Option.may f in + fo cb.cb_catch; + match cb.cb_next with + | NextSub(cb_sub,cb_next) -> + f cb_sub; + fo cb_next + | NextIfThen(_,cb_then,cb_next) -> + f cb_then; + f cb_next; + | NextIfThenElse(_,cb_then,cb_else,cb_next) -> + f cb_then; + f cb_else; + fo cb_next; + | NextSwitch(switch,cb_next) -> + List.iter (fun (_,cb) -> f cb) switch.cs_cases; + Option.may f switch.cs_default; + fo cb_next; + | NextWhile(e,cb_body,cb_next) -> + f cb_body; + fo cb_next; + | NextTry(cb_try,catch,cb_next) -> + f cb_try; + f catch.cc_cb; + List.iter (fun (_,cb) -> f cb) catch.cc_catches; + fo cb_next; + | NextSuspend(call,cb_next) -> + fo cb_next + | NextBreak cb_next | NextContinue cb_next | NextFallThrough cb_next | NextGoto cb_next -> + f cb_next; + | NextUnknown | NextReturnVoid | NextReturn _ | NextThrow _ -> + () + +let coro_next_map f cb = + Option.may (fun cb_catch -> cb.cb_catch <- Some (f cb_catch)) cb.cb_catch; + let fo = Option.map f in + match cb.cb_next with + | NextSub(cb_sub,cb_next) -> + let cb_sub = f cb_sub in + let cb_next = fo cb_next in + cb.cb_next <- NextSub(cb_sub,cb_next); + | NextIfThen(e,cb_then,cb_next) -> + let cb_then = f cb_then in + let cb_next = f cb_next in + cb.cb_next <- NextIfThen(e,cb_then,cb_next); + | NextIfThenElse(e,cb_then,cb_else,cb_next) -> + let cb_then = f cb_then in + let cb_else = f cb_else in + let cb_next = fo cb_next in + cb.cb_next <- NextIfThenElse(e,cb_then,cb_else,cb_next); + | NextSwitch(switch,cb_next) -> + let cases = List.map (fun (el,cb) -> (el,f cb)) switch.cs_cases in + let def = Option.map f switch.cs_default in + let switch = { + switch with cs_cases = cases; cs_default = def + } in + let cb_next = fo cb_next in + cb.cb_next <- NextSwitch(switch,cb_next); + | NextWhile(e,cb_body,cb_next) -> + let cb_body = f cb_body in + let cb_next = fo cb_next in + cb.cb_next <- NextWhile(e,cb_body,cb_next); + | NextTry(cb_try,catch,cb_next) -> + let cb_try = f cb_try in + let cc_cb = f catch.cc_cb in + let catches = List.map (fun (v,cb) -> (v,f cb)) catch.cc_catches in + let catch = { + cc_cb; + cc_catches = catches + } in + let cb_next = fo cb_next in + cb.cb_next <- NextTry(cb_try,catch,cb_next); + | NextSuspend(call,cb_next) -> + let cb_next = fo cb_next in + cb.cb_next <- NextSuspend(call,cb_next); + | NextBreak cb_next -> + cb.cb_next <- NextBreak (f cb_next); + | NextContinue cb_next -> + cb.cb_next <- NextContinue (f cb_next); + | NextGoto cb_next -> + cb.cb_next <- NextGoto (f cb_next); + | NextFallThrough cb_next -> + cb.cb_next <- NextFallThrough (f cb_next); + | NextReturnVoid | NextReturn _ | NextThrow _ | NextUnknown -> + () \ No newline at end of file diff --git a/src/coro/coroToTexpr.ml b/src/coro/coroToTexpr.ml new file mode 100644 index 00000000000..15cc5168063 --- /dev/null +++ b/src/coro/coroToTexpr.ml @@ -0,0 +1,418 @@ +open Globals +open CoroTypes +open CoroFunctions +open Type +open Texpr +open CoroControl + +type coro_state = { + cs_id : int; + mutable cs_el : texpr list; +} + +type coro_to_texpr_exprs = { + econtinuation : texpr; + ecompletion : texpr; + estate : texpr; + eresult : texpr; + egoto : texpr; + eerror : texpr; + etmp : texpr; +} + +let make_suspending_call basic call econtinuation = + (* lose Coroutine type for the called function not to confuse further filters and generators *) + let tfun = match follow_with_coro call.cs_fun.etype with + | Coro (args, ret) -> + let args,ret = Common.expand_coro_type basic args ret in + TFun (args, ret) + | NotCoro _ -> + die "Unexpected coroutine type" __LOC__ + in + let efun = { call.cs_fun with etype = tfun } in + let args = call.cs_args @ [ econtinuation ] in + mk (TCall (efun, args)) (basic.tcoro.suspension_result basic.tany) call.cs_pos + +let handle_locals ctx b cls states tf_args forbidden_vars econtinuation = + let module IntSet = Set.Make(struct + let compare a b = b - a + type t = int + end) in + + (* function arguments are accessible from the initial state without hoisting needed, so set that now *) + let arg_state_set = IntSet.of_list [ (List.hd states).cs_id ] in + let var_usages = tf_args |> List.map (fun (v, _) -> v.v_id, arg_state_set) |> List.to_seq |> Hashtbl.of_seq in + + (* First iteration, just add newly discovered local variables *) + (* After this var_usages will contain all arguments and local vars and the states sets will be just the creation state *) + (* We don't handle locals here so we don't poison the var_usage hashtbl with non local var data *) + List.iter (fun state -> + let rec loop e = + match e.eexpr with + | TVar (v, eo) -> + Option.may loop eo; + Hashtbl.replace var_usages v.v_id (IntSet.of_list [ state.cs_id ]) + | _ -> + Type.iter loop e + in + List.iter loop state.cs_el + ) states; + + (* Second interation, visit all locals and update any local variable state sets *) + List.iter (fun state -> + let rec loop e = + match e.eexpr with + | TLocal (v) -> + (match Hashtbl.find_opt var_usages v.v_id with + | Some set -> + Hashtbl.replace var_usages v.v_id (IntSet.add state.cs_id set) + | None -> + ()) + | _ -> + Type.iter loop e + in + List.iter loop state.cs_el + ) states; + + let is_used_across_states v_id = + let many_states set v_id = + IntSet.elements set |> List.length > 1 in + (* forbidden vars are things like the _hx_continuation variable, they should not be hoisted *) + let non_coro_var v_id = + forbidden_vars |> List.exists (fun id -> id = v_id) |> not in + + match Hashtbl.find_opt var_usages v_id with + | Some set when many_states set v_id && non_coro_var v_id -> + true + | _ -> + false + in + + let fields = + tf_args + |> List.filter_map (fun (v, _) -> + if is_used_across_states v.v_id then + Some (v.v_id, mk_field (Printf.sprintf "_hx_hoisted%i" v.v_id) v.v_type v.v_pos v.v_pos) + else + None) + |> List.to_seq + |> Hashtbl.of_seq in + + (* Third iteration, create fields for vars used across states and remap access to those fields *) + List.iter (fun state -> + let rec loop e = + match e.eexpr with + | TVar (v, eo) when is_used_across_states v.v_id -> + let name = Printf.sprintf "_hx_hoisted%i" v.v_id in + let field = mk_field name v.v_type v.v_pos v.v_pos in + + Hashtbl.replace fields v.v_id field; + + begin match eo with + | None -> + (* We need an expression, so let's just emit `null`. The analyzer will clean this up. *) + b#null t_dynamic e.epos + | Some e -> + let efield = b#instance_field econtinuation cls [] field field.cf_type in + let einit = + match eo with + | None -> Builder.default_value v.v_type v.v_pos + | Some e -> Type.map_expr loop e in + b#assign efield einit + end + (* A local of a var should never appear before its declaration, right? *) + | TLocal (v) when is_used_across_states v.v_id -> + let field = Hashtbl.find fields v.v_id in + + b#instance_field econtinuation cls [] field field.cf_type + | _ -> + Type.map_expr loop e + in + state.cs_el <- List.map loop state.cs_el + ) states; + + (* We need to do this argument copying as the last thing we do *) + (* Doing it when the initial fields hashtbl is created will cause the third iterations TLocal to re-write them... *) + List.iter (fun (v, _) -> + if is_used_across_states v.v_id then + let initial = List.hd states in + let field = Hashtbl.find fields v.v_id in + let efield = b#instance_field econtinuation cls [] field field.cf_type in + let assign = b#assign efield (b#local v v.v_pos) in + + initial.cs_el <- assign :: initial.cs_el) tf_args; + fields + +let block_to_texpr_coroutine ctx cb cont cls params tf_args forbidden_vars exprs p stack_item_inserter start_exception = + let {econtinuation;ecompletion;estate;eresult;egoto;eerror;etmp} = exprs in + let com = ctx.typer.com in + let b = ctx.builder in + + let set_state id = b#assign egoto (b#int id p) in + + let set_control (c : coro_control) = b#assign estate (CoroControl.mk_control com.basic c) in + + let std_is e t = + let type_expr = mk (TTypeExpr (module_type_of_type t)) t_dynamic p in + Texpr.Builder.resolve_and_make_static_call com.std "isOfType" [e;type_expr] p + in + + let ereturn = b#return econtinuation in + + let mk_suspending_call call = + let p = call.cs_pos in + let base_continuation_field_on e cf t = + b#instance_field e com.basic.tcoro.suspension_result_class [com.basic.tany] cf t + in + let ecreatecoroutine = make_suspending_call com.basic call econtinuation in + + let vcororesult = alloc_var VGenerated "_hx_tmp" (com.basic.tcoro.suspension_result com.basic.tany) p in + let ecororesult = b#local vcororesult p in + let cororesult_var = b#var_init vcororesult ecreatecoroutine in + let open ContTypes in + let esubject = base_continuation_field_on ecororesult cont.state cont.state.cf_type in + let esuspended = b#void_block [ + set_control CoroPending; + ereturn; + ] in + let ereturned = b#assign etmp (base_continuation_field_on ecororesult cont.result com.basic.tany) in + let ethrown = b#void_block [ + b#assign eresult (* TODO: wrong type? *) (base_continuation_field_on ecororesult cont.result com.basic.tany); + b#assign etmp (base_continuation_field_on ecororesult cont.error cont.error.cf_type); + b#break p; + ] in + let estate_switch = CoroControl.make_control_switch com.basic esubject esuspended ereturned ethrown p in + [ + stack_item_inserter call.cs_pos; + cororesult_var; + estate_switch; + ] + in + + let states = ref [] in + + let init_state = cb.cb_id in + + let make_state id el = { + cs_id = id; + cs_el = el; + } in + + (* TODO: this sucks a bit and its usage isn't much better *) + let wrap_thrown,get_caught = match com.basic.texception with + | TInst(c,_) -> + (fun e -> Texpr.Builder.resolve_and_make_static_call c "thrown" [e] e.epos), + (fun e -> Texpr.Builder.resolve_and_make_static_call c "caught" [e] e.epos) + | _ -> + die "" __LOC__ + in + let eif_error = + let el = if ctx.throw then + [b#throw eerror] + else [ + b#assign etmp eerror; + b#break p; + ] in + let e_then = b#void_block el in + b#if_then + (b#binop OpNotEq eerror (b#null eerror.etype p) com.basic.tbool) + e_then + in + + let exc_state_map = Array.init ctx.next_block_id (fun _ -> ref []) in + let generate cb = + let el = get_block_exprs cb in + + let add_state next_id extra_el = + let el = el in + let el = match next_id with + | None -> + el + | Some id -> + el @ [set_state id] + in + let el = if has_block_flag cb CbResumeState then + eif_error :: el + else + el + in + let el = el @ extra_el in + states := (make_state cb.cb_id el) :: !states; + begin match cb.cb_catch with + | None -> + () + | Some cb' -> + let r = exc_state_map.(cb'.cb_id) in + r := cb.cb_id :: !r + end; + cb.cb_id + in + match cb.cb_next with + | NextSuspend (call, cb_next) -> + let ecallcoroutine = mk_suspending_call call in + add_state (Option.map (fun cb_next -> cb_next.cb_id) cb_next) ecallcoroutine; + | NextUnknown -> + add_state (Some (-1)) [set_control CoroReturned; ereturn] + | NextFallThrough cb_next | NextGoto cb_next | NextBreak cb_next | NextContinue cb_next -> + add_state (Some cb_next.cb_id) [] + | NextReturnVoid -> + add_state (Some (-1)) [ set_control CoroReturned; ereturn ] + | NextReturn e -> + add_state (Some (-1)) [ set_control CoroReturned; b#assign eresult e; ereturn ] + | NextThrow e1 -> + if ctx.throw then + add_state None ([stack_item_inserter e1.epos; start_exception (b#bool true p); b#throw e1]) + else + add_state None ([stack_item_inserter e1.epos; start_exception (b#bool true p); b#assign etmp e1; b#break p ]) + | NextSub (cb_sub,cb_next) -> + add_state (Some cb_sub.cb_id) [] + + | NextIfThen (econd,cb_then,cb_next) -> + let eif = b#if_then_else econd (set_state cb_then.cb_id) (set_state cb_next.cb_id) com.basic.tint in + add_state None [eif] + + | NextIfThenElse (econd,cb_then,cb_else,cb_next) -> + let eif = b#if_then_else econd (set_state cb_then.cb_id) (set_state cb_else.cb_id) com.basic.tint in + add_state None [eif] + + | NextSwitch(switch,cb_next) -> + let esubj = switch.cs_subject in + let ecases = List.map (fun (patterns,cb) -> + {case_patterns = patterns;case_expr = set_state cb.cb_id} + ) switch.cs_cases in + let next_id = match switch.cs_default with + | Some cb -> + Some (set_state cb.cb_id) + | None -> + Option.map (fun cb_next -> set_state cb_next.cb_id) cb_next + in + let eswitch = mk_switch esubj ecases next_id true in + let eswitch = mk (TSwitch eswitch) com.basic.tvoid p in + + add_state None [eswitch] + + | NextWhile (e_cond,cb_body,cb_next) -> + add_state (Some cb_body.cb_id) [] + + | NextTry (cb_try,catch,cb_next) -> + let new_exc_state_id = catch.cc_cb.cb_id in + let erethrow = match catch.cc_cb.cb_catch with + | Some cb -> + set_state cb.cb_id + | None -> + b#void_block [ + b#break p + ] + in + let eif = + List.fold_left (fun enext (vcatch,cb_catch) -> + match follow vcatch.v_type with + | TDynamic _ -> + set_state cb_catch.cb_id (* no next *) + | t -> + let etypecheck = std_is etmp vcatch.v_type in + b#if_then_else etypecheck (set_state cb_catch.cb_id) enext com.basic.tvoid + ) erethrow (List.rev catch.cc_catches) + in + states := (make_state new_exc_state_id [eif]) :: !states; + add_state (Some cb_try.cb_id) [] + in + let rec loop cb = + if not (has_block_flag cb CbGenerated) then begin + add_block_flag cb CbGenerated; + ignore(generate cb); + coro_iter loop cb; + end + in + loop cb; + + let states = !states in + let states = states |> List.sort (fun state1 state2 -> state1.cs_id - state2.cs_id) in + + let fields = handle_locals ctx b cls states tf_args forbidden_vars econtinuation in + + let ethrow = b#void_block [ + b#assign etmp (b#string "Invalid coroutine state" p); + b#break p + ] in + + let switch = + let cases = List.map (fun state -> + {case_patterns = [b#int state.cs_id p]; + case_expr = b#void_block state.cs_el; + }) states in + mk_switch egoto cases (Some ethrow) true + in + let eswitch = mk (TSwitch switch) com.basic.tvoid p in + + let eloop = mk (TWhile (b#bool true p, eswitch, NormalWhile)) com.basic.tvoid p in + + let etry = if ctx.nothrow || (ctx.throw && not ctx.has_catch) then + eloop + else + mk (TTry ( + eloop, + [ + let vcaught = alloc_var VGenerated "e" t_dynamic p in + let ecaught = b#local vcaught p in + let e = b#void_block [ + start_exception (b#bool false p); + b#assign etmp ecaught + ] in + (vcaught,e) + ] + )) com.basic.tvoid p + in + + let eexchandle = + let cases = DynArray.create () in + Array.iteri (fun i l -> match !l with + | [] -> + () + | l -> + let patterns = List.map (fun i -> b#int i p) l in + let expr = b#void_block [ + set_state i; + ] in + DynArray.add cases {case_patterns = patterns; case_expr = expr}; + ) exc_state_map; + let el = if ctx.throw then [ + b#throw etmp + ] else begin + let field = PMap.find "buildCallStack" com.basic.tcoro.base_continuation_class.cl_fields in + let eaccess = b#instance_field econtinuation com.basic.tcoro.base_continuation_class params field field.cf_type in + let ewrapped_call = mk (TCall (eaccess, [ ])) com.basic.tvoid p in + [ + ewrapped_call; + b#assign eerror (wrap_thrown etmp); + set_control CoroThrown; + ereturn; + ] + end in + let default = b#void_block el in + if DynArray.empty cases then + default + else begin + let switch = { + switch_subject = egoto; + switch_cases = DynArray.to_list cases; + switch_default = Some default; + switch_exhaustive = true + } in + mk (TSwitch switch) com.basic.tvoid p + end + in + + let etry = b#void_block [ + etry; + eexchandle; + ] in + + let eloop = if ctx.has_catch then + mk (TWhile (b#bool true p, etry, NormalWhile)) com.basic.tvoid p + else + (* If there is no catch we don't need to pseudo-goto back into the state loop, so we don't need a control loop. *) + etry + in + + eloop, init_state, fields |> Hashtbl.to_seq_values |> List.of_seq diff --git a/src/coro/coroTypes.ml b/src/coro/coroTypes.ml new file mode 100644 index 00000000000..1699f09456d --- /dev/null +++ b/src/coro/coroTypes.ml @@ -0,0 +1,74 @@ +open Globals +open Type + +type coro_block = { + mutable cb_id : int; + cb_el : texpr DynArray.t; + cb_typepos : (Type.t * pos) option; + mutable cb_catch : coro_block option; + mutable cb_next : coro_next; + mutable cb_flags : int; +} + +and coro_block_next = coro_block option + +and coro_next = + | NextUnknown + | NextSub of coro_block * coro_block_next + | NextReturnVoid + | NextReturn of texpr + | NextThrow of texpr + | NextIfThen of texpr * coro_block * coro_block + | NextIfThenElse of texpr * coro_block * coro_block * coro_block_next + | NextSwitch of coro_switch * coro_block_next + | NextWhile of texpr * coro_block * coro_block_next + | NextTry of coro_block * coro_catch * coro_block_next + | NextSuspend of coro_suspend * coro_block_next + (* graph connections from here on, careful with traversal *) + | NextBreak of coro_block + | NextContinue of coro_block + | NextFallThrough of coro_block + | NextGoto of coro_block + +and coro_switch = { + cs_subject : texpr; + cs_cases : (texpr list * coro_block) list; + cs_default : coro_block option; + cs_exhaustive : bool; +} + +and coro_catch = { + cc_cb : coro_block; + cc_catches : (tvar * coro_block) list; +} + +and coro_suspend = { + cs_fun : texpr; + cs_args : texpr list; + cs_pos : pos; +} + +type coro_ctx = { + builder : CoroElsewhere.texpr_builder; + typer : Typecore.typer; + coro_debug : bool; + optimize : bool; + allow_tco : bool; + throw : bool; + nothrow : bool; + mutable vthis : tvar option; + mutable next_block_id : int; + mutable current_catch : coro_block option; + mutable has_catch : bool; +} + +type cb_flag = + | CbEmptyMarked + | CbForwardMarked + | CbTcoChecked + | CbReindexed + | CbGenerated + | CbSuspendState + | CbResumeState + +exception CoroTco of coro_block \ No newline at end of file diff --git a/src/generators/cpp/gen/cppGenClassImplementation.ml b/src/generators/cpp/gen/cppGenClassImplementation.ml index 3490ca70338..d0bd6d05e68 100644 --- a/src/generators/cpp/gen/cppGenClassImplementation.ml +++ b/src/generators/cpp/gen/cppGenClassImplementation.ml @@ -831,8 +831,8 @@ let generate_managed_class base_ctx tcpp_class = output_cpp "#endif\n\n"); let generate_script_function isStatic field scriptName callName = - match follow field.cf_type with - | TFun (args, return_type) when not (is_data_member field) -> + if not (is_data_member field) then + let gen (args, return_type) = let isTemplated = not isStatic in if isTemplated then output_cpp "\ntemplate"; output_cpp @@ -880,43 +880,51 @@ let generate_managed_class base_ctx tcpp_class = if ret <> "v" then output_cpp ")"; output_cpp ";\n}\n"; signature - | _ -> "" + in + match follow_with_coro field.cf_type with + | Coro (args, return) -> Common.expand_coro_type ctx.ctx_common.basic args return |> gen + | NotCoro TFun (args, return) -> gen (args, return) + | _ -> "" + else + "" in if scriptable then ( let dump_script_func idx func = - match func.tcf_field.cf_type with - | TFun (f_args, _) -> - let args = print_tfun_arg_list true f_args in - let return_type = type_to_string func.tcf_func.tf_type in - let ret = if return_type = "Void" || return_type = "void" then " " else "return " in - let vtable = Printf.sprintf "__scriptVTable[%i]" (idx + 1) in - - Printf.sprintf "\t%s %s(%s) {\n" return_type func.tcf_name args |> output_cpp; - Printf.sprintf ("\tif (%s) {\n") vtable |> output_cpp; - output_cpp "\t\t::hx::CppiaCtx *__ctx = ::hx::CppiaCtx::getCurrent();\n"; - output_cpp "\t\t::hx::AutoStack __as(__ctx);\n"; - output_cpp ("\t\t__ctx->pushObject( this );\n"); - - List.iter - (fun (name, opt, t) -> - Printf.sprintf "\t\t__ctx->push%s(%s);\n" (CppCppia.script_type t opt) (keyword_remap name) |> output_cpp) - f_args; + let f_args = + match follow_with_coro func.tcf_field.cf_type with + | Coro (args, return) -> Common.expand_coro_type ctx.ctx_common.basic args return |> fst + | NotCoro TFun (args, _) -> args + | _ -> abort "expected function type to be tfun" func.tcf_field.cf_pos + in + let args = print_tfun_arg_list true f_args in + let return_type = type_to_string func.tcf_func.tf_type in + let ret = if return_type = "Void" || return_type = "void" then " " else "return " in + let vtable = Printf.sprintf "__scriptVTable[%i]" (idx + 1) in - output_cpp - ("\t\t" ^ ret ^ "__ctx->run" ^ CppCppia.script_type func.tcf_func.tf_type false ^ "(" ^ vtable ^ ");\n"); - output_cpp ("\t} else " ^ ret); + Printf.sprintf "\t%s %s(%s) {\n" return_type func.tcf_name args |> output_cpp; + Printf.sprintf ("\tif (%s) {\n") vtable |> output_cpp; + output_cpp "\t\t::hx::CppiaCtx *__ctx = ::hx::CppiaCtx::getCurrent();\n"; + output_cpp "\t\t::hx::AutoStack __as(__ctx);\n"; + output_cpp ("\t\t__ctx->pushObject( this );\n"); + + List.iter + (fun (name, opt, t) -> + Printf.sprintf "\t\t__ctx->push%s(%s);\n" (CppCppia.script_type t opt) (keyword_remap name) |> output_cpp) + f_args; - let names = List.map (fun (n, _, _) -> keyword_remap n) f_args in + output_cpp + ("\t\t" ^ ret ^ "__ctx->run" ^ CppCppia.script_type func.tcf_func.tf_type false ^ "(" ^ vtable ^ ");\n"); + output_cpp ("\t} else " ^ ret); - output_cpp - (class_name ^ "::" ^ func.tcf_name ^ "(" ^ String.concat "," names ^ ");"); + let names = List.map (fun (n, _, _) -> keyword_remap n) f_args in - if return_type <> "void" then output_cpp "return null();"; + output_cpp + (class_name ^ "::" ^ func.tcf_name ^ "(" ^ String.concat "," names ^ ");"); - output_cpp "}\n"; - | _ -> - abort "expected function type to be tfun" func.tcf_field.cf_pos + if return_type <> "void" then output_cpp "return null();"; + + output_cpp "}\n" in let script_name = class_name ^ "__scriptable" in diff --git a/src/generators/cpp/gen/cppReferences.ml b/src/generators/cpp/gen/cppReferences.ml index 294a72febaa..1e3d8e7bbde 100644 --- a/src/generators/cpp/gen/cppReferences.ml +++ b/src/generators/cpp/gen/cppReferences.ml @@ -89,39 +89,45 @@ let find_referenced_types_flags ctx obj filter super_deps constructor_deps heade let rec visit_type in_type = if not (List.exists (fun t2 -> Type.fast_eq in_type t2) !visited) then ( visited := in_type :: !visited; - (match follow in_type with - | TMono r -> ( match r.tm_type with None -> () | Some t -> visit_type t) - | TEnum (enum, _) -> ( - match is_extern_enum enum with - | true -> add_extern_enum enum - | false -> add_type enum.e_path) - (* If a class has a template parameter, then we treat it as dynamic - except - for the Array, Class, FastIterator or Pointer classes, for which we do a fully typed object *) - | TInst (klass, params) -> ( - match klass.cl_path with - | [], "Array" - | [], "Class" - | [ "cpp" ], "FastIterator" - | [ "cpp" ], "Pointer" - | [ "cpp" ], "ConstPointer" - | [ "cpp" ], "Function" - | [ "cpp" ], "RawPointer" - | [ "cpp" ], "RawConstPointer" -> - List.iter visit_type params - | _ when is_native_gen_class klass -> add_native_gen_class klass - | _ when is_extern_class klass -> - add_extern_class klass; - List.iter visit_type params - | _ -> ( - match klass.cl_kind with - | KTypeParameter _ -> () - | _ -> add_type klass.cl_path)) - | TAbstract (a, params) when is_scalar_abstract a -> - add_extern_type (TAbstractDecl a) - | TFun (args, haxe_type) -> - visit_type haxe_type; - List.iter (fun (_, _, t) -> visit_type t) args - | _ -> ()); + (match follow_with_coro in_type with + | Coro (args, return) -> + let args, return = Common.expand_coro_type ctx.ctx_common.basic args return in + visit_type return; + List.iter (fun (_, _, t) -> visit_type t) args + | NotCoro t -> + (match follow t with + | TMono r -> ( match r.tm_type with None -> () | Some t -> visit_type t) + | TEnum (enum, _) -> ( + match is_extern_enum enum with + | true -> add_extern_enum enum + | false -> add_type enum.e_path) + (* If a class has a template parameter, then we treat it as dynamic - except + for the Array, Class, FastIterator or Pointer classes, for which we do a fully typed object *) + | TInst (klass, params) -> ( + match klass.cl_path with + | [], "Array" + | [], "Class" + | [ "cpp" ], "FastIterator" + | [ "cpp" ], "Pointer" + | [ "cpp" ], "ConstPointer" + | [ "cpp" ], "Function" + | [ "cpp" ], "RawPointer" + | [ "cpp" ], "RawConstPointer" -> + List.iter visit_type params + | _ when is_native_gen_class klass -> add_native_gen_class klass + | _ when is_extern_class klass -> + add_extern_class klass; + List.iter visit_type params + | _ -> ( + match klass.cl_kind with + | KTypeParameter _ -> () + | _ -> add_type klass.cl_path)) + | TAbstract (a, params) when is_scalar_abstract a -> + add_extern_type (TAbstractDecl a) + | TFun (args, haxe_type) -> + visit_type haxe_type; + List.iter (fun (_, _, t) -> visit_type t) args + | _ -> ())); visited := List.tl !visited) in let visit_params expression = diff --git a/src/generators/genhl.ml b/src/generators/genhl.ml index 663de6dd440..d48d82bd3e0 100644 --- a/src/generators/genhl.ml +++ b/src/generators/genhl.ml @@ -480,6 +480,14 @@ let rec to_type ?tref ctx t = | ["hl"], "GUID" -> HGUID | ["hl"], "NativeArray" -> HArray (to_type ctx (List.hd pl)) | ["haxe";"macro"], "Position" -> HAbstract ("macro_pos", alloc_string ctx "macro_pos") + | ["haxe";"coro"], "Coroutine" -> + begin match pl with + | [TFun(args,ret)] -> + let args,ret = Common.expand_coro_type ctx.com.basic args ret in + to_type ctx (TFun(args,ctx.com.basic.tcoro.suspension_result ret)) + | _ -> + die "" __LOC__ + end | _ -> failwith ("Unknown core type " ^ s_type_path a.a_path)) else get_rec_cache ctx t diff --git a/src/generators/genjvm.ml b/src/generators/genjvm.ml index 01ca8df003f..077ad9c46ab 100644 --- a/src/generators/genjvm.ml +++ b/src/generators/genjvm.ml @@ -168,6 +168,14 @@ let rec jsignature_of_type gctx stack t = end | [],"EnumValue" -> java_enum_sig object_sig + | ["haxe";"coro"],"Coroutine" -> + begin match tl with + | [TFun(args,ret)] -> + let args,ret = Common.expand_coro_type gctx.gctx.basic args ret in + jsignature_of_type (TFun(args,gctx.gctx.basic.tcoro.suspension_result ret)) + | _ -> + die "" __LOC__ + end | _ -> if Meta.has Meta.CoreType a.a_meta then TObject(a.a_path,List.map jtype_argument_of_type tl) @@ -197,11 +205,12 @@ let rec jsignature_of_type gctx stack t = | TInst(c,tl) -> TObject(c.cl_path,List.map jtype_argument_of_type tl) | TEnum(en,tl) -> TObject(en.e_path,List.map jtype_argument_of_type tl) - | TFun(tl,tr) -> method_sig (List.map (fun (_,o,t) -> - let jsig = jsignature_of_type t in - let jsig = if o then get_boxed_type jsig else jsig in - jsig - ) tl) (return_of_type gctx stack tr) + | TFun(tl,tr) -> + method_sig (List.map (fun (_,o,t) -> + let jsig = jsignature_of_type t in + let jsig = if o then get_boxed_type jsig else jsig in + jsig + ) tl) (return_of_type gctx stack tr) | TAnon an -> object_sig | TType(td,tl) -> begin match gctx.typedef_interfaces#get_interface_class td.t_path with @@ -765,8 +774,11 @@ class texpr_to_jvm method read cast e1 fa = let read_static_closure path cf = - let args,ret = match follow cf.cf_type with - | TFun(tl,tr) -> List.map (fun (n,_,t) -> n,self#vtype t) tl,(return_of_type gctx tr) + let args,ret = match follow_with_coro cf.cf_type with + | NotCoro TFun(tl,tr) -> List.map (fun (n,_,t) -> n,self#vtype t) tl,(return_of_type gctx tr) + | Coro (tl,tr) -> + let tl,tr = Common.expand_coro_type gctx.gctx.basic tl tr in + List.map (fun (n,_,t) -> n,self#vtype t) tl,(return_of_type gctx tr) | _ -> die "" __LOC__ in self#read_static_closure path cf.cf_name args ret cf.cf_type @@ -1558,9 +1570,12 @@ class texpr_to_jvm (* calls *) method call_arguments ?(cast=true) t el = - let tl,tr = match follow t with - | TFun(tl,tr) -> + let tl,tr = match follow_with_coro t with + | NotCoro (TFun(tl,tr)) -> tl,return_of_type gctx tr + | Coro(args,ret) -> + let args,ret = Common.expand_coro_type gctx.gctx.basic args ret in + args,return_of_type gctx ret | _ -> List.map (fun e -> ("",false,e.etype)) el,Some (object_sig) in @@ -1963,7 +1978,8 @@ class texpr_to_jvm if not jm#is_terminated then self#texpr' ret e method texpr' ret e = - code#set_line (Lexer.get_error_line e.epos); + if e.epos.pmin >= 0 then + code#set_line (Lexer.get_error_line e.epos); match e.eexpr with | TVar(v,Some e1) -> self#texpr (rvalue_type gctx v.v_type (Some v.v_name)) e1; @@ -2428,8 +2444,9 @@ class tclass_to_jvm gctx c = object(self) maybe_make_bridge cf_impl.cf_name jsig_super jsig_impl in let find_overload map_type c cf = - let tl = match follow (map_type cf.cf_type) with - | TFun(tl,_) -> tl + let tl = match follow_with_coro (map_type cf.cf_type) with + | Coro (tl, _) -> tl + | NotCoro TFun(tl,_) -> tl | _ -> die "" __LOC__ in OverloadResolution.resolve_instance_overload false map_type c cf.cf_name (List.map (fun (_,_,t) -> Texpr.Builder.make_null t null_pos) tl) diff --git a/src/generators/genneko.ml b/src/generators/genneko.ml index 217cbf941c4..b534423dd17 100644 --- a/src/generators/genneko.ml +++ b/src/generators/genneko.ml @@ -235,9 +235,7 @@ and gen_expr ctx e = | TBinop (op,e1,e2) -> gen_binop ctx p op e1 e2 | TField (e2,FClosure (_,f)) -> - (match follow e.etype with - | TFun (args,_) -> - let n = List.length args in + let emit n = if n > 5 then Error.abort "Cannot create closure with more than 5 arguments" e.epos; let tmp = ident p "@tmp" in EBlock [ @@ -247,6 +245,14 @@ and gen_expr ctx e = else call p (ident p ("@closure" ^ string_of_int n)) [tmp;ident p "@fun"] ] , p + in + (match follow_with_coro e.etype with + | NotCoro TFun (args,_) -> + let n = List.length args in + emit n + | Coro (args,_) -> + let n = List.length args in + emit (n + 1) | _ -> die "" __LOC__) | TEnumParameter (e,_,i) -> EArray (field p (gen_expr ctx e) "args",int p i),p diff --git a/src/generators/genphp7.ml b/src/generators/genphp7.ml index 21dff91f866..e0f48c9cf3b 100644 --- a/src/generators/genphp7.ml +++ b/src/generators/genphp7.ml @@ -410,9 +410,10 @@ let rec needs_temp_var expr = (** @return (arguments_list, return_type) *) -let get_function_signature (field:tclass_field) : (string * bool * Type.t) list * Type.t = - match follow field.cf_type with - | TFun (args, return_type) -> (args, return_type) +let get_function_signature basic (field:tclass_field) : (string * bool * Type.t) list * Type.t = + match follow_with_coro field.cf_type with + | Coro (args, return_type) -> Common.expand_coro_type basic args return_type + | NotCoro TFun (args, return_type) -> args, return_type | _ -> fail field.cf_pos __LOC__ (** @@ -598,9 +599,16 @@ let fix_tsignature_args args = (** Inserts `null`s if there are missing optional args before empty rest arguments. *) -let fix_call_args callee_type exprs = - match follow callee_type with - | TFun (args,_) -> +let fix_call_args basic callee_type exprs = + let args = + match follow_with_coro callee_type with + | Coro (args, return_type) -> Some (Common.expand_coro_type basic args return_type |> fst) + | NotCoro TFun (args,_) -> Some args + | _ -> None + in + + match args with + | Some args -> (match List.rev args with | (_,_,t) :: args_rev when is_rest_type t && List.length args_rev > List.length exprs -> let rec loop args exprs = @@ -612,7 +620,7 @@ let fix_call_args callee_type exprs = loop args exprs | _ -> exprs ) - | _ -> exprs + | None -> exprs (** Escapes all "$" chars and encloses `str` into double quotes @@ -1490,8 +1498,15 @@ class code_writer (ctx:php_generator_context) hx_type_path php_name = | current :: _ -> match self#parent_expr with | Some { eexpr = TCall (target, params) } when current != (reveal_expr target) -> - (match follow target.etype with - | TFun (args,_) -> + let args = + match follow_with_coro target.etype with + | Coro (args, return_type) -> Some (Common.expand_coro_type ctx.pgc_common.basic args return_type |> fst) + | NotCoro TFun (args,_) -> Some args + | _ -> None + in + + (match args with + | Some args -> let rec check args params = match args, params with | (_, _, t) :: _, param :: _ when current == (reveal_expr param) -> @@ -2302,7 +2317,7 @@ class code_writer (ctx:php_generator_context) hx_type_path php_name = | FInstance (_, _, ({ cf_kind = Method _ } as field)) | FClosure (_, ({ cf_kind = Method _ } as field)) -> self#write ((self#use hxstring_type_path) ^ "::" ^ (field_name field) ^ "("); - write_args self#write self#write_expr (fix_call_args field.cf_type (expr :: args)); + write_args self#write self#write_expr (fix_call_args ctx.pgc_common.basic field.cf_type (expr :: args)); self#write ")" | _ -> let msg = @@ -2651,7 +2666,7 @@ class code_writer (ctx:php_generator_context) hx_type_path php_name = if not !no_call then begin self#write "("; - write_args self#write self#write_expr (fix_call_args target_expr.etype args); + write_args self#write self#write_expr (fix_call_args ctx.pgc_common.basic target_expr.etype args); self#write ")" end (** @@ -2718,7 +2733,7 @@ class code_writer (ctx:php_generator_context) hx_type_path php_name = self#write ("new " ^ (self#use ~prefix:needs_php_prefix inst_class.cl_path) ^ "("); let args = match inst_class.cl_constructor with - | Some field -> fix_call_args field.cf_type args + | Some field -> fix_call_args ctx.pgc_common.basic field.cf_type args | None -> args in write_args self#write self#write_expr args; @@ -3455,8 +3470,15 @@ class class_builder ctx (cls:tclass) = | Some (cls, _) -> let fields = if is_static then cls.cl_statics else cls.cl_fields in try - match (PMap.find name fields).cf_type with - | TFun (args,_) -> + let args = + match follow_with_coro (PMap.find name fields).cf_type with + | Coro (args, return_type) -> Some (Common.expand_coro_type ctx.pgc_common.basic args return_type |> fst) + | NotCoro TFun (args,_) -> Some args + | _ -> None + in + + match args with + | Some args -> let rec count args mandatory total = match args with | [] -> @@ -3793,7 +3815,7 @@ class class_builder ctx (cls:tclass) = self#validate_method_name field; writer#reset; writer#indent 1; - let (args, return_type) = get_function_signature field in + let (args, return_type) = get_function_signature ctx.pgc_common.basic field in List.iter (fun (arg_name, _, _) -> writer#declared_local_var arg_name) args; self#write_doc (DocMethod (args, return_type, (gen_doc_text_opt field.cf_doc))) field.cf_meta; writer#write_indentation; @@ -3820,13 +3842,14 @@ class class_builder ctx (cls:tclass) = self#validate_method_name field; writer#reset; writer#indent 1; - let (args, return_type) = get_function_signature field in + let (args, return_type) = get_function_signature ctx.pgc_common.basic field in List.iter (fun (arg_name, _, _) -> writer#declared_local_var arg_name) args; self#write_doc (DocMethod (args, return_type, (gen_doc_text_opt field.cf_doc))) field.cf_meta; let visibility_kwd = get_visibility field.cf_meta in writer#write_with_indentation (visibility_kwd ^ " function " ^ (field_name field)); (match field.cf_expr with | None -> (* interface *) + (* let args, _ = Common.expand_coro_type ctx.pgc_common.basic args return_type in *) writer#write " ("; write_args writer#write (writer#write_arg true) (fix_tsignature_args args); writer#write ");\n"; diff --git a/src/optimization/analyzerTypes.ml b/src/optimization/analyzerTypes.ml index 5ad70b6d7f2..acec94b0328 100644 --- a/src/optimization/analyzerTypes.ml +++ b/src/optimization/analyzerTypes.ml @@ -83,20 +83,14 @@ module BasicBlock = struct ss_exhaustive : bool; } - and suspend_call = { - efun : texpr; (* coroutine function expression *) - args : texpr list; (* call arguments without the continuation *) - pos : pos; (* call position *) - } - and terminator_kind = - | TermNone - | TermCondBranch of texpr - | TermReturn of pos - | TermReturnValue of texpr * pos - | TermBreak of pos - | TermContinue of pos - | TermThrow of texpr * pos + | TermNone + | TermCondBranch of texpr + | TermReturn of pos + | TermReturnValue of texpr * pos + | TermBreak of pos + | TermContinue of pos + | TermThrow of texpr * pos and t = { bb_id : int; (* The unique ID of the block *) diff --git a/src/typing/callUnification.ml b/src/typing/callUnification.ml index 62208cc6a76..ed8fa5162aa 100644 --- a/src/typing/callUnification.ml +++ b/src/typing/callUnification.ml @@ -177,7 +177,7 @@ let unify_call_args ctx el args r callp inline force_inline in_overload = in let el = try loop el args with exc -> restore(); raise exc; in restore(); - el,TFun(args,r) + el type overload_kind = | OverloadProper (* @:overload or overload *) @@ -286,10 +286,9 @@ let unify_field_call ctx fa el_typed el p inline = let attempt_call cf in_overload = let monos = Monomorph.spawn_constrained_monos map cf.cf_params in let t = map (apply_params cf.cf_params monos cf.cf_type) in - match follow t with - | TFun(args,ret) -> + let make args ret coro = let args_typed,args = unify_typed_args ctx tmap args el_typed p in - let el,_ = + let el = try unify_call_args ctx el args ret p inline is_forced_inline in_overload with DisplayException.DisplayException de -> @@ -297,13 +296,23 @@ let unify_field_call ctx fa el_typed el p inline = in (* here *) let el = el_typed @ el in - let tf = TFun(args_typed @ args,ret) in + let args = (args_typed @ args) in + let tf = if coro then ctx.t.tcoro.tcoro args ret else TFun(args,ret) in let mk_call () = let ef = mk (TField(fa.fa_on,FieldAccess.apply_fa cf fa.fa_host)) t fa.fa_pos in !make_call_ref ctx ef el ret ~force_inline:inline p in make_field_call_candidate el ret monos tf cf (mk_call,extract_delayed_display()) - | t -> + in + match follow_with_coro t with + | Coro(args,ret) when not (TyperManager.is_coroutine_context ctx) -> + let args, ret = expand_coro_type ctx.com.basic args ret in + make args ret false + | Coro(args,ret) -> + make args ret true + | NotCoro (TFun(args,ret)) -> + make args ret false + | NotCoro t -> raise_typing_error (s_type (print_context()) t ^ " cannot be called") p in let unknown_ident_error = ref None in @@ -542,14 +551,21 @@ object(self) in mk (TCall (e,el)) t p in - let rec loop t = match follow t with - | TFun (args,r) -> + let make args ret = let args_typed,args_left = unify_typed_args ctx (fun t -> t) args el_typed p in - let el, tfunc = unify_call_args ctx el args_left r p false false false in + let el = unify_call_args ctx el args_left ret p false false false in let el = el_typed @ el in - let r = match tfunc with TFun(_,r) -> r | _ -> die "" __LOC__ in - mk (TCall (e,el)) r p - | TAbstract(a,tl) as t -> + mk (TCall (e,el)) ret p + in + let rec loop t = match follow_with_coro t with + | Coro(args,ret) when not (TyperManager.is_coroutine_context ctx) -> + let args, ret = expand_coro_type ctx.com.basic args ret in + make args ret + | Coro(args,ret) -> + make args ret + | NotCoro(TFun(args,ret)) -> + make args ret + | NotCoro(TAbstract(a,tl) as t) -> let check_callable () = if Meta.has Meta.Callable a.a_meta then loop (Abstract.get_underlying_type a tl) @@ -564,12 +580,12 @@ object(self) | _ -> check_callable(); end - | TMono _ -> + | NotCoro (TMono _)-> let t = mk_mono() in let el = el_typed @ List.map (fun e -> type_expr ctx e WithType.value) el in unify ctx (tfun (List.map (fun e -> e.etype) el) t) e.etype e.epos; mk (TCall (e,el)) t p - | t -> + | NotCoro t -> default t in loop e.etype diff --git a/src/typing/macroContext.ml b/src/typing/macroContext.ml index 7b79c9746e6..84556a400bc 100644 --- a/src/typing/macroContext.ml +++ b/src/typing/macroContext.ml @@ -951,7 +951,7 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = incr index; (EArray ((EArrayDecl [e],p),(EConst (Int (string_of_int (!index), None)),p)),p) ) el in - let elt = fst (CallUnification.unify_call_args mctx constants (List.map fst eargs) t_dynamic p false false false) in + let elt = CallUnification.unify_call_args mctx constants (List.map fst eargs) t_dynamic p false false false in List.map2 (fun ((n,_,t),mct) e -> let e, et = (match e.eexpr with (* get back our index and real expression *) @@ -1025,7 +1025,7 @@ let type_macro ctx mode cpath f (el:Ast.expr list) p = e let call_macro mctx args margs call p = - let el, _ = CallUnification.unify_call_args mctx args margs t_dynamic p false false false in + let el = CallUnification.unify_call_args mctx args margs t_dynamic p false false false in call (List.map (fun e -> try Interp.make_const e with Exit -> raise_typing_error "Argument should be a constant" e.epos) el) let resolve_init_macro com e = diff --git a/src/typing/typeload.ml b/src/typing/typeload.ml index dbea83f3655..9527ff46c48 100644 --- a/src/typing/typeload.ml +++ b/src/typing/typeload.ml @@ -862,8 +862,13 @@ let init_core_api ctx c = | _ -> raise_typing_error ("Field " ^ f.cf_name ^ " has different property access than core type") p; end; - (match follow f.cf_type, follow f2.cf_type with - | TFun (pl1,_), TFun (pl2,_) -> + (match follow_with_coro f.cf_type, follow_with_coro f2.cf_type with + | Coro _,NotCoro _ -> + raise_typing_error "Method should be coroutine" p + | NotCoro _,Coro _ -> + raise_typing_error "Method should not be coroutine" p; + | NotCoro (TFun (pl1,_)), NotCoro(TFun (pl2,_)) + | Coro (pl1,_), Coro(pl2,_) -> if List.length pl1 != List.length pl2 then raise_typing_error "Argument count mismatch" p; List.iter2 (fun (n1,_,_) (n2,_,_) -> if n1 <> n2 then raise_typing_error ("Method parameter name '" ^ n2 ^ "' should be '" ^ n1 ^ "'") p; diff --git a/src/typing/typeloadCheck.ml b/src/typing/typeloadCheck.ml index b814fc5a0c1..e03393fc1d0 100644 --- a/src/typing/typeloadCheck.ml +++ b/src/typing/typeloadCheck.ml @@ -64,8 +64,13 @@ let valid_redefinition map1 map2 f1 t1 f2 t2 = (* child, parent *) end; begin match f1.cf_kind,f2.cf_kind with | Method m1, Method m2 when not (m1 = MethDynamic) && not (m2 = MethDynamic) -> - begin match follow t1, follow t2 with - | TFun (args1,r1) , TFun (args2,r2) -> + begin match follow_with_coro t1, follow_with_coro t2 with + | Coro _,NotCoro _ -> + raise (Unify_error [Unify_custom "Method should be coroutine"]) + | NotCoro _,Coro _ -> + raise (Unify_error [Unify_custom "Method should not be coroutine"]); + | NotCoro (TFun (args1,r1)), NotCoro(TFun (args2,r2)) + | Coro (args1,r1), Coro(args2,r2) -> if not (List.length args1 = List.length args2) then raise (Unify_error [Unify_custom "Different number of function arguments"]); let i = ref 0 in begin try diff --git a/src/typing/typeloadFields.ml b/src/typing/typeloadFields.ml index 26f63cac109..374e60edb17 100644 --- a/src/typing/typeloadFields.ml +++ b/src/typing/typeloadFields.ml @@ -869,7 +869,9 @@ module TypeBinding = struct (match e.eexpr with | TBlock [] | TBlock [{ eexpr = TConst _ }] | TConst _ | TObjectDecl [] -> () | _ -> TClass.set_cl_init c e); - cf.cf_expr <- Some (mk (TFunction tf) t p); + let e = mk (TFunction tf) t p in + let e = if TyperManager.is_coroutine_context ctx && not (Meta.has Meta.CoroutineTransformed cf.cf_meta) then Coro.fun_to_coro (Coro.create_coro_context ctx cf.cf_meta) (ClassField(c, cf, tf, p)) else e in + cf.cf_expr <- Some e; cf.cf_type <- t; check_field_display ctx fctx c cf; end; @@ -1256,9 +1258,21 @@ let create_method (ctx,cctx,fctx) c f cf fd p = ctx.type_params <- params @ ctx.type_params; let args,ret = setup_args_ret ctx cctx fctx (fst f.cff_name) fd p in - let function_mode = FunFunction in + let is_coroutine = Meta.has Meta.Coroutine f.cff_meta in + let function_mode = if is_coroutine then FunCoroutine else FunFunction in let targs = args#for_type in - let t = TFun (targs,ret) in + let t = if not is_coroutine then + TFun (targs,ret) + else if Meta.has Meta.CoroutineTransformed cf.cf_meta then begin + match List.rev targs with + | _ :: targs -> + (* Ignore trailing continuation for actual signature *) + ctx.t.tcoro.tcoro (List.rev targs) ret + | _ -> + die "" __LOC__ + end else + ctx.t.tcoro.tcoro targs ret + in cf.cf_type <- t; cf.cf_kind <- Method (if fctx.is_macro then MethMacro else if fctx.is_inline then MethInline else if dynamic then MethDynamic else MethNormal); cf.cf_params <- params; diff --git a/src/typing/typer.ml b/src/typing/typer.ml index 834e0250ea8..da239c83fd1 100644 --- a/src/typing/typer.ml +++ b/src/typing/typer.ml @@ -935,7 +935,7 @@ and type_new ctx ptp el with_type force_inline p = | None -> raise_typing_error_ext (make_error (No_constructor (TClassDecl c)) p) | Some(tl,tr) -> - let el,_ = unify_call_args ctx el tl tr p false false false in + let el = unify_call_args ctx el tl tr p false false false in mk (TNew (c,params,el)) t p end | TAbstract({a_impl = Some c} as a,tl) when not (Meta.has Meta.MultiType a.a_meta) -> @@ -1106,7 +1106,7 @@ and type_map_declaration ctx e1 el with_type p = let el = (mk (TVar (v,Some enew)) t_dynamic p) :: (List.rev el) in mk (TBlock el) tmap p -and type_local_function ctx_from kind f with_type p = +and type_local_function ctx_from kind f with_type want_coroutine p = let name,inline = match kind with FKNamed (name,inline) -> Some name,inline | _ -> None,false in let params = TypeloadFunction.type_function_params ctx_from f TPHLocal (match name with None -> "localfun" | Some (n,_) -> n) in let curfun = match ctx_from.e.curfun with @@ -1115,11 +1115,21 @@ and type_local_function ctx_from kind f with_type p = | FunMemberAbstractLocal -> FunMemberAbstractLocal | _ -> FunMemberClassLocal in - let function_mode = FunFunction in + let is_coroutine = match name, with_type with + | None, WithType.WithType (texpected,_) when not (ExtType.is_mono (follow texpected)) -> + (match follow_with_coro texpected with + | Coro _ -> + true + | _ -> + false) + | _ -> + want_coroutine + in + let function_mode = if is_coroutine then FunCoroutine else FunFunction in let ctx = TyperManager.clone_for_expr ctx_from curfun function_mode in let vname,pname= match name with | None -> - if params <> [] then begin + if params <> [] || is_coroutine then begin Some(gen_local_prefix,VGenerated),null_pos end else None,p @@ -1153,8 +1163,9 @@ and type_local_function ctx_from kind f with_type p = let m = new unification_matrix (arity + 1) in let rec loop l = match l with | t :: l -> - begin match follow t with - | TFun(args,ret) when List.length args = arity -> + begin match follow_with_coro t with + | NotCoro(TFun(args,ret)) + | Coro(args,ret) when List.length args = arity -> List.iteri (fun i (_,_,t) -> (* We don't want to bind monomorphs because we want the widest type *) let t = dynamify_monos t in @@ -1187,14 +1198,15 @@ and type_local_function ctx_from kind f with_type p = (match with_type with | WithType.WithType(t,_) -> let rec loop stack t = - (match follow t with - | TFun (args2,tr) when List.length args2 = List.length targs -> + (match follow_with_coro t with + | NotCoro (TFun (args2,tr)) + | Coro(args2,tr) when List.length args2 = List.length targs -> List.iter2 (fun (_,_,t1) (_,_,t2) -> maybe_unify_arg t1 t2 ) targs args2; (* unify for top-down inference unless we are expecting Void *) maybe_unify_ret tr - | TAbstract(a,tl) -> + | NotCoro (TAbstract(a,tl)) -> begin match get_abstract_froms ctx a tl with | [(_,t2)] -> if not (List.exists (shallow_eq t) stack) then loop (t :: stack) t2 @@ -1221,8 +1233,9 @@ and type_local_function ctx_from kind f with_type p = | WithType.NoValue -> if name = None then display_error ctx.com "Unnamed lvalue functions are not supported" p | _ -> - ()); - let ft = TFun (targs,rt) in + () + ); + let ft = if is_coroutine then ctx.t.tcoro.tcoro targs rt else TFun(targs,rt) in let ft = match with_type with | WithType.NoValue -> ft @@ -1245,8 +1258,10 @@ and type_local_function ctx_from kind f with_type p = tf_expr = e; } in let e = mk (TFunction tf) ft p in + let e = if TyperManager.is_coroutine_context ctx then Coro.fun_to_coro (Coro.create_coro_context ctx ctx.f.meta) (LocalFunc(tf,Option.get v)) else e in match v with - | None -> e + | None -> + e | Some v -> Typeload.generate_args_meta ctx.com None (fun m -> v.v_meta <- m :: v.v_meta) f.f_args; let open LocalUsage in @@ -1545,6 +1560,20 @@ and type_meta ?(mode=MGet) ctx m e1 with_type p = | (EReturn e, p) -> type_return ~implicit:true ctx e with_type p | _ -> e() end + | (Meta.Coroutine,_,_) -> + let old = ctx.f.meta in + let rec loop e1 = match fst e1 with + | EMeta(m,e1) -> + ctx.f.meta <- m :: ctx.f.meta; + loop e1 + | EFunction (kind, f) -> + type_local_function ctx kind f with_type true p + | _ -> + e () + in + let e = loop e1 in + ctx.f.meta <- old; + e (* Allow `${...}` reification because it's a noop and happens easily with macros *) | (Meta.Dollar "",_,p) -> e() @@ -1892,7 +1921,7 @@ and type_expr ?(mode=MGet) ctx (e,p) (with_type:WithType.t) = | EUnop (op,flag,e) -> type_unop ctx op flag e with_type p | EFunction (kind,f) -> - type_local_function ctx kind f with_type p + type_local_function ctx kind f with_type false p | EUntyped e -> let old = ctx.f.untyped in ctx.f.untyped <- true; diff --git a/src/typing/typerBase.ml b/src/typing/typerBase.ml index e6cb480d21d..21209f50064 100644 --- a/src/typing/typerBase.ml +++ b/src/typing/typerBase.ml @@ -172,6 +172,16 @@ let get_this ctx p = | FunMemberAbstract -> let v = (try PMap.find "this" ctx.f.locals with Not_found -> raise_typing_error "Cannot reference this abstract here" p) in mk (TLocal v) v.v_type p + | FunConstructor | FunMember when TyperManager.is_coroutine_context ctx -> + let v = match ctx.f.vthis with + | None -> + let v = add_local ctx VGenerated (Printf.sprintf "%sthis" gen_local_prefix) ctx.c.tthis p in + ctx.f.vthis <- Some v; + v + | Some v -> + v + in + mk (TLocal v) ctx.c.tthis p | FunConstructor | FunMember -> mk (TConst TThis) ctx.c.tthis p diff --git a/src/typing/typerEntry.ml b/src/typing/typerEntry.ml index 9346e9ffe35..3c6e4695ae1 100644 --- a/src/typing/typerEntry.ml +++ b/src/typing/typerEntry.ml @@ -114,7 +114,8 @@ let load_unit ctx = | TEnumDecl en -> (match snd en.e_path with | "Unit" -> - ctx.m.import_resolution#add (module_type_resolution mt None null_pos); + ctx.t.tunit <- TEnum(en,[]); + (* ctx.m.import_resolution#add (module_type_resolution mt None null_pos); *) | _ -> ()) | _ -> () ) m.m_types @@ -135,6 +136,61 @@ let load_enum_tools ctx = | _ -> die "" __LOC__ +let load_coro ctx = + let m = TypeloadModule.load_module ctx (["haxe";"coro"],"Coroutine") null_pos in + List.iter (function + | TAbstractDecl({a_path = (["haxe";"coro"],"Coroutine")} as a) -> + let mk_coro args ret = + TAbstract(a,[TFun(args,ret)]) + in + ctx.t.tcoro.tcoro <- mk_coro + | _ -> + () + ) m.m_types; + let m = TypeloadModule.load_module ctx (["haxe";"coro"],"IContinuation") null_pos in + List.iter (function + | TClassDecl({ cl_path = (["haxe";"coro"], "IContinuation") } as cl) -> + ctx.t.tcoro.continuation <- TInst(cl, [ ctx.t.tany ]); + | _ -> + () + ) m.m_types; + let m = TypeloadModule.load_module ctx (["haxe";"coro"],"BaseContinuation") null_pos in + List.iter (function + | TClassDecl({ cl_path = (["haxe";"coro"], "BaseContinuation") } as cl) -> + ctx.t.tcoro.base_continuation_class <- cl; + | _ -> + () + ) m.m_types; + let m = TypeloadModule.load_module ctx (["haxe";"coro"],"SuspensionResult") null_pos in + List.iter (function + | TClassDecl({ cl_path = (["haxe";"coro"], "SuspensionResult") } as cl) -> + ctx.t.tcoro.suspension_result <- (fun t -> TInst(cl, [t])); + ctx.t.tcoro.suspension_result_class <- cl; + | _ -> + () + ) m.m_types; + let m = TypeloadModule.load_module ctx (["haxe";"coro"],"ImmediateSuspensionResult") null_pos in + List.iter (function + | TClassDecl({ cl_path = (["haxe";"coro"], "ImmediateSuspensionResult") } as cl) -> + ctx.t.tcoro.immediate_suspension_result_class <- cl; + | _ -> + () + ) m.m_types; + let m = TypeloadModule.load_module ctx (["haxe";"coro"],"SuspensionState") null_pos in + List.iter (function + | TAbstractDecl({a_path = (["haxe";"coro"],"SuspensionState")} as a) -> + ctx.t.tcoro.suspension_state <- TAbstract(a,[]) + | _ -> + () + ) m.m_types; + let m = TypeloadModule.load_module ctx (["haxe"],"Exception") null_pos in + List.iter (function + | TClassDecl({ cl_path = (["haxe"], "Exception") } as cl) -> + ctx.t.texception <- TInst(cl, []) + | _ -> + () + ) m.m_types + let create com macros = let rec ctx = { com = com; @@ -154,6 +210,7 @@ let create com macros = return_partial_type = false; build_count = 0; t_dynamic_def = t_dynamic; + continuation_api = None; do_macro = MacroContext.type_macro; do_load_macro = MacroContext.load_macro'; do_load_module = TypeloadModule.load_module; @@ -190,9 +247,10 @@ let create com macros = load_string ctx; load_std ctx; load_any ctx; - (* load_unit ctx; *) + load_unit ctx; load_array ctx; load_enum_tools ctx; + load_coro ctx; ignore(TypeloadModule.load_module ctx (["haxe"],"Exception") null_pos); ctx.g.complete <- true; ctx diff --git a/std/StdTypes.hx b/std/StdTypes.hx index b67e3c45556..37e18ac524b 100644 --- a/std/StdTypes.hx +++ b/std/StdTypes.hx @@ -20,7 +20,6 @@ * DEALINGS IN THE SOFTWARE. */ // standard Haxe types - /** The standard `Void` type. Only `null` values can be of the type `Void`. @@ -169,4 +168,4 @@ typedef KeyValueIterable = { @see https://haxe.org/manual/types-abstract-array-access.html **/ -extern interface ArrayAccess {} +extern interface ArrayAccess {} \ No newline at end of file diff --git a/std/eval/_std/haxe/coro/EventLoopImpl.hx b/std/eval/_std/haxe/coro/EventLoopImpl.hx new file mode 100644 index 00000000000..319e9ad0760 --- /dev/null +++ b/std/eval/_std/haxe/coro/EventLoopImpl.hx @@ -0,0 +1,221 @@ +package haxe.coro; + +import sys.thread.Mutex; + +/** + When an event loop has an available event to execute. +**/ +private enum NextEventTime { + /** There's already an event waiting to be executed */ + Now; + /** No new events are expected. */ + Never; + /** An event is expected to be ready for execution at `time`. */ + At(time:Float); +} + +private class SimpleEventLoop { + final mutex = new Mutex(); + final oneTimeEvents = new ArrayVoid>>(); + var oneTimeEventsIdx = 0; + var regularEvents:Null; + + public function new():Void {} + + /** + Schedule event for execution every `intervalMs` milliseconds in current loop. + **/ + public function repeat(event:()->Void, intervalMs:Int):EventHandler { + var interval = 0.001 * intervalMs; + var event = new RegularEvent(event, haxe.Timer.stamp() + interval, interval); + mutex.acquire(); + insertEventByTime(event); + mutex.release(); + return event; + } + + function insertEventByTime(event:RegularEvent):Void { + switch regularEvents { + case null: + regularEvents = event; + case current: + var previous = null; + while(true) { + if(current == null) { + previous.next = event; + event.previous = previous; + break; + } else if(event.nextRunTime < current.nextRunTime) { + event.next = current; + current.previous = event; + switch previous { + case null: + regularEvents = event; + case _: + event.previous = previous; + previous.next = event; + current.previous = event; + } + break; + } else { + previous = current; + current = current.next; + } + } + } + } + + /** + Prevent execution of a previously scheduled event in current loop. + **/ + public function cancel(eventHandler:EventHandler):Void { + var event:RegularEvent = eventHandler; + mutex.acquire(); + event.cancelled = true; + if(regularEvents == event) { + regularEvents = event.next; + } + switch event.next { + case null: + case e: e.previous = event.previous; + } + switch event.previous { + case null: + case e: e.next = event.next; + } + event.next = event.previous = null; + mutex.release(); + } + + /** + Execute `event` as soon as possible. + **/ + public function run(event:()->Void):Void { + mutex.acquire(); + oneTimeEvents[oneTimeEventsIdx++] = event; + mutex.release(); + } + + /** + Executes all pending events. + + The returned time stamps can be used with `Sys.time()` for calculations. + + Depending on a target platform this method may be non-reentrant. It must + not be called from event callbacks. + **/ + public function progress():NextEventTime { + return switch __progress(haxe.Timer.stamp(), [], []) { + case -2: Now; + case -1: Never; + case time: At(time); + } + } + + /** + Execute all pending events. + Wait and execute as many events as the number of times `promise()` was called. + Runs until all repeating events are cancelled and no more events are expected. + + Depending on a target platform this method may be non-reentrant. It must + not be called from event callbacks. + **/ + public function loop():Void { + var recycleRegular = []; + var recycleOneTimers = []; + while(true) { + var r = __progress(haxe.Timer.stamp(), recycleRegular, recycleOneTimers); + switch r { + case -1: + case -2: + break; + case time: + var timeout = time - haxe.Timer.stamp(); + } + } + } + + /** + `.progress` implementation with a reusable array for internal usage. + The `nextEventAt` field of the return value denotes when the next event + is expected to run: + * -1 - never + * -2 - now + * other values - at specified time + **/ + inline function __progress(now:Float, recycleRegular:Array, recycleOneTimers:Array<()->Void>):Float { + var regularsToRun = recycleRegular; + var eventsToRunIdx = 0; + // When the next event is expected to run + var nextEventAt:Float = -1; + + mutex.acquire(); + // Collect regular events to run + var current = regularEvents; + while(current != null) { + if(current.nextRunTime <= now) { + regularsToRun[eventsToRunIdx++] = current; + current.nextRunTime += current.interval; + nextEventAt = -2; + } else if(nextEventAt == -1 || current.nextRunTime < nextEventAt) { + nextEventAt = current.nextRunTime; + } + current = current.next; + } + mutex.release(); + + // Run regular events + for(i in 0...eventsToRunIdx) { + if(!regularsToRun[i].cancelled) + regularsToRun[i].run(); + regularsToRun[i] = null; + } + eventsToRunIdx = 0; + + var oneTimersToRun = recycleOneTimers; + mutex.acquire(); + // Collect pending one-time events + for(i => event in oneTimeEvents) { + switch event { + case null: + break; + case _: + oneTimersToRun[eventsToRunIdx++] = event; + oneTimeEvents[i] = null; + } + } + oneTimeEventsIdx = 0; + mutex.release(); + + //run events + for(i in 0...eventsToRunIdx) { + oneTimersToRun[i](); + oneTimersToRun[i] = null; + } + + // Some events were executed. They could add new events to run. + if(eventsToRunIdx > 0) { + nextEventAt = -2; + } + return nextEventAt; + } +} + +abstract EventHandler(RegularEvent) from RegularEvent to RegularEvent {} + +private class RegularEvent { + public var nextRunTime:Float; + public final interval:Float; + public final run:()->Void; + public var next:Null; + public var previous:Null; + public var cancelled:Bool = false; + + public function new(run:()->Void, nextRunTime:Float, interval:Float) { + this.run = run; + this.nextRunTime = nextRunTime; + this.interval = interval; + } +} + +typedef EventLoopImpl = SimpleEventLoop; \ No newline at end of file diff --git a/std/haxe/coro/BaseContinuation.hx b/std/haxe/coro/BaseContinuation.hx new file mode 100644 index 00000000000..476f07e73f7 --- /dev/null +++ b/std/haxe/coro/BaseContinuation.hx @@ -0,0 +1,106 @@ +package haxe.coro; + +import haxe.coro.context.Context; +import haxe.coro.schedulers.Scheduler; +import haxe.CallStack.StackItem; +import haxe.Exception; + +abstract class BaseContinuation extends SuspensionResult implements IContinuation implements IStackFrame { + public final completion:IContinuation; + + public final context:Context; + + public var gotoLabel:Int; + + public var recursing:Bool; + + function new(completion:IContinuation, initialLabel:Int) { + this.completion = completion; + + context = completion.context; + gotoLabel = initialLabel; + error = null; + result = null; + recursing = false; + } + + public final function resume(result:Any, error:Exception):Void { + this.result = result; + this.error = error; + context.get(Scheduler.key).schedule(() -> { + recursing = false; + + #if coroutine.throw + try { + #end + final result = invokeResume(); + switch (result.state) { + case Pending: + return; + case Returned: + completion.resume(result.result, null); + case Thrown: + completion.resume(result.result, result.error); + } + #if coroutine.throw + } catch (e:Dynamic) { + completion.resume(null, @:privateAccess Exception.thrown(e)); + } + #end + }); + } + + public function callerFrame():Null { + return if (completion is IStackFrame) { + cast completion; + } else { + null; + } + } + + public function getStackItem():Null { + return cast result; + } + + public function setClassFuncStackItem(cls:String, func:String, file:String, line:Int, pos:Int, pmin:Int, pmax:Int) { + result = cast StackItem.FilePos(StackItem.Method(cls, func), file, line, pos); + #if eval + eval.vm.Context.callMacroApi("associate_enum_value_pos")(result, haxe.macro.Context.makePosition({file: file, min: pmin, max: pmax})); + #end + } + + public function setLocalFuncStackItem(id:Int, file:String, line:Int, pos:Int, pmin:Int, pmax:Int) { + result = cast StackItem.FilePos(StackItem.LocalFunction(id), file, line, pos); + #if eval + eval.vm.Context.callMacroApi("associate_enum_value_pos")(result, haxe.macro.Context.makePosition({file: file, min: pmin, max: pmax})); + #end + } + + public function startException(fromThrow:Bool) { + if (fromThrow) { + /* + This comes from a coro-level throw, which pushes its position via one of the functions + above. In this case we turn result into the stack item array now. + */ + result = cast [result]; + } else { + /* + This means we caught an exception, which must come from outside our current coro. We + don't need our current result value because if anything it points to the last + suspension call. + */ + result = cast []; + } + } + + public function buildCallStack() { + var frame = callerFrame(); + if (frame != null) { + var result:Array = cast result; + result ??= []; + result.push(frame.getStackItem()); + } + } + + abstract function invokeResume():SuspensionResult; +} \ No newline at end of file diff --git a/std/haxe/coro/Coroutine.hx b/std/haxe/coro/Coroutine.hx new file mode 100644 index 00000000000..bd191ab9116 --- /dev/null +++ b/std/haxe/coro/Coroutine.hx @@ -0,0 +1,60 @@ +package haxe.coro; + +import haxe.coro.EventLoop; +import haxe.coro.schedulers.EventLoopScheduler; +import haxe.coro.schedulers.Scheduler; +import haxe.coro.continuations.RacingContinuation; +import haxe.coro.continuations.BlockingContinuation; + +private class CoroSuspend extends haxe.coro.BaseContinuation { + public function new(completion:haxe.coro.IContinuation) { + super(completion, 1); + } + + public function invokeResume():SuspensionResult { + return Coroutine.suspend(null, this); + } +} + +/** + Coroutine function. +**/ +@:callable +@:coreType +abstract Coroutine { + @:coroutine @:coroutine.transformed + public static function suspend(func:haxe.coro.IContinuation->Void, completion:haxe.coro.IContinuation):T { + var continuation = new CoroSuspend(completion); + var safe = new haxe.coro.continuations.RacingContinuation(completion, continuation); + func(safe); + safe.resolve(); + return cast continuation; + } + + @:coroutine @:coroutine.nothrow public static function delay(ms:Int):Void { + Coroutine.suspend(cont -> { + cont.context.get(Scheduler.key).scheduleIn(() -> cont.resume(null, null), ms); + }); + } + + @:coroutine @:coroutine.nothrow public static function yield():Void { + Coroutine.suspend(cont -> { + cont.context.get(Scheduler.key).schedule(() -> cont.resume(null, null)); + }); + } + + public static function run(f:Coroutine<() -> T>):T { + final loop = new EventLoop(); + final cont = new BlockingContinuation(loop, new EventLoopScheduler(loop)); + final result = f(cont); + + return switch (result.state) { + case Pending: + cont.wait(); + case Returned: + result.result; + case Thrown: + throw result.error; + } + } +} diff --git a/std/haxe/coro/EventLoop.hx b/std/haxe/coro/EventLoop.hx new file mode 100644 index 00000000000..7f796421238 --- /dev/null +++ b/std/haxe/coro/EventLoop.hx @@ -0,0 +1,38 @@ +package haxe.coro; + +#if (target.threaded && !cppia && !eval) +import sys.thread.EventLoop; +private typedef EventLoopImpl = sys.thread.EventLoop; +#else +import haxe.coro.EventLoopImpl; +private typedef EventLoopImpl = haxe.coro.EventLoopImpl; +#end + +@:coreApi abstract EventLoop(EventLoopImpl) { + public function new() { + this = new EventLoopImpl(); + } + + public function tick():Bool { + return switch this.progress() { + case Never: + false; + case _: + true; + } + } + + public function run(func:()->Void):Void { + this.run(func); + } + + public function runIn(func:()->Void, ms:Int):Void { + var handle : EventHandler = null; + + handle = this.repeat(() -> { + this.cancel(handle); + + func(); + }, ms); + } +} \ No newline at end of file diff --git a/std/haxe/coro/EventLoopImpl.hx b/std/haxe/coro/EventLoopImpl.hx new file mode 100644 index 00000000000..5929f955d6b --- /dev/null +++ b/std/haxe/coro/EventLoopImpl.hx @@ -0,0 +1,208 @@ +package haxe.coro; + +/** + When an event loop has an available event to execute. +**/ +private enum NextEventTime { + /** There's already an event waiting to be executed */ + Now; + /** No new events are expected. */ + Never; + /** An event is expected to be ready for execution at `time`. */ + At(time:Float); +} + +private class SimpleEventLoop { + final oneTimeEvents = new ArrayVoid>>(); + var oneTimeEventsIdx = 0; + var regularEvents:Null; + + public function new():Void {} + + /** + Schedule event for execution every `intervalMs` milliseconds in current loop. + **/ + public function repeat(event:()->Void, intervalMs:Int):EventHandler { + var interval = 0.001 * intervalMs; + var event = new RegularEvent(event, haxe.Timer.stamp() + interval, interval); + insertEventByTime(event); + return event; + } + + function insertEventByTime(event:RegularEvent):Void { + switch regularEvents { + case null: + regularEvents = event; + case current: + var previous = null; + while(true) { + if(current == null) { + previous.next = event; + event.previous = previous; + break; + } else if(event.nextRunTime < current.nextRunTime) { + event.next = current; + current.previous = event; + switch previous { + case null: + regularEvents = event; + case _: + event.previous = previous; + previous.next = event; + current.previous = event; + } + break; + } else { + previous = current; + current = current.next; + } + } + } + } + + /** + Prevent execution of a previously scheduled event in current loop. + **/ + public function cancel(eventHandler:EventHandler):Void { + var event:RegularEvent = eventHandler; + event.cancelled = true; + if(regularEvents == event) { + regularEvents = event.next; + } + switch event.next { + case null: + case e: e.previous = event.previous; + } + switch event.previous { + case null: + case e: e.next = event.next; + } + event.next = event.previous = null; + } + + /** + Execute `event` as soon as possible. + **/ + public function run(event:()->Void):Void { + oneTimeEvents[oneTimeEventsIdx++] = event; + } + + /** + Executes all pending events. + + The returned time stamps can be used with `Sys.time()` for calculations. + + Depending on a target platform this method may be non-reentrant. It must + not be called from event callbacks. + **/ + public function progress():NextEventTime { + return switch __progress(haxe.Timer.stamp(), [], []) { + case -2: Now; + case -1: Never; + case time: At(time); + } + } + + /** + Execute all pending events. + Wait and execute as many events as the number of times `promise()` was called. + Runs until all repeating events are cancelled and no more events are expected. + + Depending on a target platform this method may be non-reentrant. It must + not be called from event callbacks. + **/ + public function loop():Void { + var recycleRegular = []; + var recycleOneTimers = []; + while(true) { + var r = __progress(haxe.Timer.stamp(), recycleRegular, recycleOneTimers); + switch r { + case -1: + case -2: + break; + case time: + var timeout = time - haxe.Timer.stamp(); + } + } + } + + /** + `.progress` implementation with a reusable array for internal usage. + The `nextEventAt` field of the return value denotes when the next event + is expected to run: + * -1 - never + * -2 - now + * other values - at specified time + **/ + inline function __progress(now:Float, recycleRegular:Array, recycleOneTimers:Array<()->Void>):Float { + var regularsToRun = recycleRegular; + var eventsToRunIdx = 0; + // When the next event is expected to run + var nextEventAt:Float = -1; + + // Collect regular events to run + var current = regularEvents; + while(current != null) { + if(current.nextRunTime <= now) { + regularsToRun[eventsToRunIdx++] = current; + current.nextRunTime += current.interval; + nextEventAt = -2; + } else if(nextEventAt == -1 || current.nextRunTime < nextEventAt) { + nextEventAt = current.nextRunTime; + } + current = current.next; + } + + // Run regular events + for(i in 0...eventsToRunIdx) { + if(!regularsToRun[i].cancelled) + regularsToRun[i].run(); + regularsToRun[i] = null; + } + eventsToRunIdx = 0; + + var oneTimersToRun = recycleOneTimers; + // Collect pending one-time events + for(i => event in oneTimeEvents) { + switch event { + case null: + break; + case _: + oneTimersToRun[eventsToRunIdx++] = event; + oneTimeEvents[i] = null; + } + } + oneTimeEventsIdx = 0; + + //run events + for(i in 0...eventsToRunIdx) { + oneTimersToRun[i](); + oneTimersToRun[i] = null; + } + + // Some events were executed. They could add new events to run. + if(eventsToRunIdx > 0) { + nextEventAt = -2; + } + return nextEventAt; + } +} + +abstract EventHandler(RegularEvent) from RegularEvent to RegularEvent {} + +private class RegularEvent { + public var nextRunTime:Float; + public final interval:Float; + public final run:()->Void; + public var next:Null; + public var previous:Null; + public var cancelled:Bool = false; + + public function new(run:()->Void, nextRunTime:Float, interval:Float) { + this.run = run; + this.nextRunTime = nextRunTime; + this.interval = interval; + } +} + +typedef EventLoopImpl = SimpleEventLoop; \ No newline at end of file diff --git a/std/haxe/coro/IContinuation.hx b/std/haxe/coro/IContinuation.hx new file mode 100644 index 00000000000..da2b8517f10 --- /dev/null +++ b/std/haxe/coro/IContinuation.hx @@ -0,0 +1,10 @@ +package haxe.coro; + +import haxe.Exception; +import haxe.coro.context.Context; + +interface IContinuation { + final context:Context; + + function resume(result:T, error:Exception):Void; +} diff --git a/std/haxe/coro/IStackFrame.hx b/std/haxe/coro/IStackFrame.hx new file mode 100644 index 00000000000..8bbd0c1ac1c --- /dev/null +++ b/std/haxe/coro/IStackFrame.hx @@ -0,0 +1,8 @@ +package haxe.coro; + +import haxe.CallStack.StackItem; + +interface IStackFrame { + function getStackItem():Null; + function callerFrame():Null; +} \ No newline at end of file diff --git a/std/haxe/coro/ImmediateSuspensionResult.hx b/std/haxe/coro/ImmediateSuspensionResult.hx new file mode 100644 index 00000000000..ccd471a293e --- /dev/null +++ b/std/haxe/coro/ImmediateSuspensionResult.hx @@ -0,0 +1,23 @@ +package haxe.coro; + +import haxe.Exception; + +class ImmediateSuspensionResult extends SuspensionResult { + function new(result:T, error:Exception) { + this.result = result; + this.error = error; + this.state = error == null ? Returned : Thrown; + } + + static public function withResult(result:T) { + return new ImmediateSuspensionResult(result, null); + } + + static public function withError(error:T) { + return new ImmediateSuspensionResult(cast [] /* stack items */, @:privateAccess haxe.Exception.thrown(error)); + } + + public override function toString() { + return '[ImmediateSuspensionResult ${state.toString()}, $result]'; + } +} \ No newline at end of file diff --git a/std/haxe/coro/Mutex.hx b/std/haxe/coro/Mutex.hx new file mode 100644 index 00000000000..0916454feae --- /dev/null +++ b/std/haxe/coro/Mutex.hx @@ -0,0 +1,45 @@ +package haxe.coro; + +#if (target.threaded) +typedef Mutex = sys.thread.Mutex; +#else +typedef Mutex = StubMutex; + +/** + This is a stub version. + Creates a mutex, which can be used to acquire a temporary lock + to access some resource. The main difference with a lock is + that a mutex must always be released by the owner thread. +**/ +class StubMutex { + /** + Creates a stub mutex on non threaded target. + **/ + public function new():Void {} + + /** + This is a stub version. + The current thread acquire the mutex or wait if not available. + The same thread can acquire several times the same mutex but + must release it as many times it has been acquired. + **/ + public function acquire():Void {} + + /** + This is a stub version. + Try to acquire the mutex, returns true if acquire or false + if it's already locked by another thread. + **/ + public function tryAcquire():Bool { + return true; + } + + /** + This is a stub version. + Release a mutex that has been acquired by the current thread. + The behavior is undefined if the current thread does not own + the mutex. + **/ + public function release():Void {} +} +#end diff --git a/std/haxe/coro/SuspensionResult.hx b/std/haxe/coro/SuspensionResult.hx new file mode 100644 index 00000000000..d141e75226e --- /dev/null +++ b/std/haxe/coro/SuspensionResult.hx @@ -0,0 +1,13 @@ +package haxe.coro; + +import haxe.Exception; + +abstract class SuspensionResult { + public var state:SuspensionState; + public var result:T; + public var error:Exception; + + public function toString() { + return '[SuspensionResult ${state.toString()}, $result]'; + } +} \ No newline at end of file diff --git a/std/haxe/coro/SuspensionState.hx b/std/haxe/coro/SuspensionState.hx new file mode 100644 index 00000000000..e9a88452783 --- /dev/null +++ b/std/haxe/coro/SuspensionState.hx @@ -0,0 +1,18 @@ +package haxe.coro; + +@:using(SuspensionState.SuspensionStateTools) +enum abstract SuspensionState(Int) { + final Pending; + final Returned; + final Thrown; +} + +class SuspensionStateTools { + static public function toString(c:SuspensionState) { + return switch (c) { + case Pending: "Pending"; + case Returned: "Returned"; + case Thrown: "Thrown"; + } + } +} \ No newline at end of file diff --git a/std/haxe/coro/context/Context.hx b/std/haxe/coro/context/Context.hx new file mode 100644 index 00000000000..368afe4fc28 --- /dev/null +++ b/std/haxe/coro/context/Context.hx @@ -0,0 +1,59 @@ +package haxe.coro.context; + +import haxe.ds.BalancedTree; + +class ElementTree extends BalancedTree, IElement> { + override function compare(k1:Key, k2:Key) { + return k2.id - k1.id; + } + + override function copy():ElementTree { + var copied = new ElementTree(); + copied.root = root; + return copied; + } + + override function toString() { + var buf = new StringBuf(); + var first = true; + for (key => value in this) { + if (!first) { + buf.add(", "); + } else { + first = false; + } + buf.add('${key.name}: $value'); + } + return buf.toString(); + } +} + +abstract Context(ElementTree) { + public function new(tree:ElementTree) { + this = tree; + } + + public function add>(value:T) { + this.set(value.getKey(), value); + } + + public function clone():Context { + return new Context(this.copy()); + } + + public function set, V>(key:Key, value:T):Void { + this.set(key, value); + } + + public function get(key:Key):T { + return cast this.get(key); + } + + public function toString() { + return this.toString(); + } + + static public function empty() { + return new Context(new ElementTree()); + } +} diff --git a/std/haxe/coro/context/IElement.hx b/std/haxe/coro/context/IElement.hx new file mode 100644 index 00000000000..e082ab68ac3 --- /dev/null +++ b/std/haxe/coro/context/IElement.hx @@ -0,0 +1,5 @@ +package haxe.coro.context; + +interface IElement { + public function getKey():Key; +} diff --git a/std/haxe/coro/context/Key.hx b/std/haxe/coro/context/Key.hx new file mode 100644 index 00000000000..0410ee88a04 --- /dev/null +++ b/std/haxe/coro/context/Key.hx @@ -0,0 +1,21 @@ +package haxe.coro.context; + +class Key { + static var counter = 0; + static var counterMutex = new Mutex(); + + public final name:String; + public final id:Int; + + function new(id:Int, name:String) { + this.name = name; + this.id = id; + } + + static public function createNew(name:String) { + counterMutex.acquire(); + var id = counter++; + counterMutex.release(); + return new Key(id, name); + } +} \ No newline at end of file diff --git a/std/haxe/coro/continuations/BlockingContinuation.hx b/std/haxe/coro/continuations/BlockingContinuation.hx new file mode 100644 index 00000000000..6bf23632ef8 --- /dev/null +++ b/std/haxe/coro/continuations/BlockingContinuation.hx @@ -0,0 +1,60 @@ +package haxe.coro.continuations; + +import haxe.CallStack; +import haxe.coro.context.Context; +import haxe.coro.schedulers.Scheduler; + +class BlockingContinuation implements IContinuation { + public final context:Context; + + final loop:EventLoop; + + var running:Bool; + var result:T; + var error:Exception; + + public function new(loop:EventLoop, scheduler:Scheduler) { + this.loop = loop; + + context = Context.empty(); + context.set(Scheduler.key, scheduler); + running = true; + error = null; + } + + public function resume(result:T, error:Exception) { + running = false; + + this.result = result; + this.error = error; + } + + public function wait():T { + while (loop.tick() || running) { + // Busy wait + } + + if (error != null) { + // trace((cast result : haxe.CallStack)); + final topStack = []; + for (item in error.stack.asArray()) { + switch (item) { + // TODO: this needs a better check + case FilePos(_, _, -1, _): + break; + // this is a hack + case FilePos(Method(_, "invokeResume"), _): + break; + case _: + topStack.push(item); + } + } + final coroStack = (cast result : Array) ?? []; + final bottomStack = CallStack.callStack(); + error.stack = topStack.concat(coroStack).concat(bottomStack); + throw error; + } else { + return result; + } + } +} diff --git a/std/haxe/coro/continuations/RacingContinuation.hx b/std/haxe/coro/continuations/RacingContinuation.hx new file mode 100644 index 00000000000..496832fb5d2 --- /dev/null +++ b/std/haxe/coro/continuations/RacingContinuation.hx @@ -0,0 +1,85 @@ +package haxe.coro.continuations; + +import haxe.coro.context.Context; +import haxe.coro.schedulers.Scheduler; + +#if (target.threaded && !cppia) +import sys.thread.Lock; +import sys.thread.Mutex; +import sys.thread.Thread; +#else +private class Lock { + public function new() {} + + public inline function release() {} + + public inline function wait(?t:Float) {} +} + +private class Mutex { + public function new() {} + + public inline function acquire() {} + + public inline function release() {} +} + +private class Thread { + public static function create(f:Void->Void) { + f(); + } +} +#end + +@:coreApi class RacingContinuation implements IContinuation { + final inputCont:IContinuation; + final outputCont:SuspensionResult; + + final lock:Mutex; + + var assigned:Bool; + + public final context:Context; + + public function new(inputCont:IContinuation, outputCont:SuspensionResult) { + this.inputCont = inputCont; + this.outputCont = outputCont; + context = inputCont.context; + assigned = false; + lock = new Mutex(); + } + + public function resume(result:T, error:Exception):Void { + context.get(Scheduler.key).schedule(() -> { + lock.acquire(); + + if (assigned) { + lock.release(); + inputCont.resume(result, error); + } else { + assigned = true; + outputCont.result = result; + outputCont.error = error; + + lock.release(); + } + }); + } + + public function resolve():Void { + lock.acquire(); + if (assigned) { + if (outputCont.error != null) { + outputCont.state = Thrown; + lock.release(); + } else { + outputCont.state = Returned; + lock.release(); + } + } else { + assigned = true; + outputCont.state = Pending; + lock.release(); + } + } +} diff --git a/std/haxe/coro/schedulers/EventLoopScheduler.hx b/std/haxe/coro/schedulers/EventLoopScheduler.hx new file mode 100644 index 00000000000..6db4dfdb50c --- /dev/null +++ b/std/haxe/coro/schedulers/EventLoopScheduler.hx @@ -0,0 +1,25 @@ +package haxe.coro.schedulers; + +import haxe.coro.EventLoop; + +class EventLoopScheduler extends Scheduler { + + final loop : EventLoop; + + public function new(loop:EventLoop) { + super(); + this.loop = loop; + } + + public function schedule(func : ()->Void) { + loop.run(func); + } + + public function scheduleIn(func : ()->Void, ms:Int) { + loop.runIn(func, ms); + } + + public function toString() { + return '[EventLoopScheduler: $loop]'; + } +} \ No newline at end of file diff --git a/std/haxe/coro/schedulers/Scheduler.hx b/std/haxe/coro/schedulers/Scheduler.hx new file mode 100644 index 00000000000..93b47536032 --- /dev/null +++ b/std/haxe/coro/schedulers/Scheduler.hx @@ -0,0 +1,18 @@ +package haxe.coro.schedulers; + +import haxe.coro.context.Key; +import haxe.coro.context.IElement; + +abstract class Scheduler implements IElement { + public static final key:Key = Key.createNew('Scheduler'); + + function new() {} + + public abstract function schedule(func:() -> Void):Void; + + public abstract function scheduleIn(func:() -> Void, ms:Int):Void; + + public function getKey() { + return key; + } +} diff --git a/tests/misc/coroutines/.gitignore b/tests/misc/coroutines/.gitignore new file mode 100644 index 00000000000..444f0793565 --- /dev/null +++ b/tests/misc/coroutines/.gitignore @@ -0,0 +1,2 @@ +/test.js +/test.js.map diff --git a/tests/misc/coroutines/build-base.hxml b/tests/misc/coroutines/build-base.hxml new file mode 100644 index 00000000000..0480a254d98 --- /dev/null +++ b/tests/misc/coroutines/build-base.hxml @@ -0,0 +1,5 @@ +--class-path src +--library utest +--main Main +--debug +-D UTEST-PRINT-TESTS diff --git a/tests/misc/coroutines/build-cpp.hxml b/tests/misc/coroutines/build-cpp.hxml new file mode 100644 index 00000000000..3244b4108d1 --- /dev/null +++ b/tests/misc/coroutines/build-cpp.hxml @@ -0,0 +1,2 @@ +build-base.hxml +--cpp bin/cpp \ No newline at end of file diff --git a/tests/misc/coroutines/build-eval.hxml b/tests/misc/coroutines/build-eval.hxml new file mode 100644 index 00000000000..9b9dbcee78f --- /dev/null +++ b/tests/misc/coroutines/build-eval.hxml @@ -0,0 +1,2 @@ +build-base.hxml +--interp \ No newline at end of file diff --git a/tests/misc/coroutines/build-hl.hxml b/tests/misc/coroutines/build-hl.hxml new file mode 100644 index 00000000000..917f0cb77e5 --- /dev/null +++ b/tests/misc/coroutines/build-hl.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--hl bin/coro.hl +--cmd hl bin/coro.hl \ No newline at end of file diff --git a/tests/misc/coroutines/build-js.hxml b/tests/misc/coroutines/build-js.hxml new file mode 100644 index 00000000000..b68988c4f6a --- /dev/null +++ b/tests/misc/coroutines/build-js.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--js bin/coro.js +--cmd node bin/coro.js \ No newline at end of file diff --git a/tests/misc/coroutines/build-jvm.hxml b/tests/misc/coroutines/build-jvm.hxml new file mode 100644 index 00000000000..359b874b931 --- /dev/null +++ b/tests/misc/coroutines/build-jvm.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--jvm bin/coro.jar +--cmd java -jar bin/coro.jar \ No newline at end of file diff --git a/tests/misc/coroutines/build-lua.hxml b/tests/misc/coroutines/build-lua.hxml new file mode 100644 index 00000000000..178e2f3be48 --- /dev/null +++ b/tests/misc/coroutines/build-lua.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--lua bin/coro.lua +--cmd lua bin/coro.lua \ No newline at end of file diff --git a/tests/misc/coroutines/build-neko.hxml b/tests/misc/coroutines/build-neko.hxml new file mode 100644 index 00000000000..f42ea5fed25 --- /dev/null +++ b/tests/misc/coroutines/build-neko.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--neko bin/coro.n +--cmd neko bin/coro.n \ No newline at end of file diff --git a/tests/misc/coroutines/build-php.hxml b/tests/misc/coroutines/build-php.hxml new file mode 100644 index 00000000000..d75f939b821 --- /dev/null +++ b/tests/misc/coroutines/build-php.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--php bin/php +--cmd php bin/php/index.php \ No newline at end of file diff --git a/tests/misc/coroutines/build-python.hxml b/tests/misc/coroutines/build-python.hxml new file mode 100644 index 00000000000..e1c254dc524 --- /dev/null +++ b/tests/misc/coroutines/build-python.hxml @@ -0,0 +1,3 @@ +build-base.hxml +--python bin/coro.py +--cmd python bin/coro.py \ No newline at end of file diff --git a/tests/misc/coroutines/src/BaseCase.hx b/tests/misc/coroutines/src/BaseCase.hx new file mode 100644 index 00000000000..0a447a46b23 --- /dev/null +++ b/tests/misc/coroutines/src/BaseCase.hx @@ -0,0 +1,19 @@ +@:keepSub +@:keep +class BaseCase implements utest.ITest { + var dummy:String = ''; + + public function new() {} + + public function setup() { + dummy = ''; + } + + function assert(expected:Array, generator:Iterator, ?p:haxe.PosInfos) { + dummy = ''; + for (it in generator) { + Assert.equals(expected.shift(), it, p); + } + Assert.equals(0, expected.length, p); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/Helper.hx b/tests/misc/coroutines/src/Helper.hx new file mode 100644 index 00000000000..6f1c2ab3117 --- /dev/null +++ b/tests/misc/coroutines/src/Helper.hx @@ -0,0 +1,4 @@ +@:coroutine +function mapCalls(args:Array, f:CoroutineTRet>):Array { + return [for (arg in args) f(arg)]; +} diff --git a/tests/misc/coroutines/src/Main.hx b/tests/misc/coroutines/src/Main.hx new file mode 100644 index 00000000000..a89dbbf3cb7 --- /dev/null +++ b/tests/misc/coroutines/src/Main.hx @@ -0,0 +1,38 @@ +import yield.*; + +function main() { + + var cases = [ + new TestBasic(), + new TestTricky(), + new TestControlFlow(), + new TestTryCatch(), + new TestHoisting(), + new TestMisc(), + new TestMutex(), + // new TestGenerator(), + #if js + new TestJsPromise(), + #end + #if (!coroutine.throw && (jvm || cpp || eval)) + new TestCallStack(), + #end + // new TestYieldBasic(), + // new TestYieldIf(), + // new TestYieldFor(), + // new TestYieldClosure(), + // new TestYieldSwitch(), + // new TestYieldTryCatch(), + // new TestYieldWhile(), + ]; + + var runner = new utest.Runner(); + + for (eachCase in cases) { + runner.addCase(eachCase); + } + runner.addCases("issues"); + + utest.ui.Report.create(runner); + runner.run(); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/TestBasic.hx b/tests/misc/coroutines/src/TestBasic.hx new file mode 100644 index 00000000000..12e2a84a341 --- /dev/null +++ b/tests/misc/coroutines/src/TestBasic.hx @@ -0,0 +1,71 @@ +import haxe.Exception; +import haxe.coro.Coroutine.yield; + +class TestBasic extends utest.Test { + function testSimple() { + Assert.equals(42, Coroutine.run(@:coroutine function run() { + return simple(42); + })); + } + + function testErrorDirect() { + Assert.raises(() -> Coroutine.run(error), String); + } + + function testErrorPropagation() { + @:coroutine function propagate() { + error(); + } + + Assert.raises(() -> Coroutine.run(propagate), String); + } + + function testResumeWithError() { + @:coroutine function foo() { + Coroutine.suspend(cont -> { + cont.resume(null, new Exception("")); + }); + } + + Assert.raises(() -> Coroutine.run(foo), Exception); + } + + function testUnnamedLocalCoroutines() { + final c1 = @:coroutine function () { + yield(); + + return 10; + }; + + Assert.equals(10, Coroutine.run(c1)); + } + + function testLocalTypeParameters() { + Coroutine.run(@:coroutine function f():T { + return null; + }); + Assert.pass(); // The test is that this doesn't cause an unbound type parameter + } + + #if sys + + function testDelay() { + var elapsed = Coroutine.run(() -> { + var start = Sys.time(); + Coroutine.delay(500); + return Sys.time() - start; + }); + // This might not be super accurate, but it's good enough + Assert.isTrue(elapsed > 0.4); + } + + #end + + @:coroutine static function simple(arg:Int):Int { + return arg; + } + + @:coroutine static function error() { + throw "nope"; + } +} diff --git a/tests/misc/coroutines/src/TestCallStack.hx b/tests/misc/coroutines/src/TestCallStack.hx new file mode 100644 index 00000000000..3b4e322611f --- /dev/null +++ b/tests/misc/coroutines/src/TestCallStack.hx @@ -0,0 +1,43 @@ +import callstack.CallStackInspector; + +class TestCallStack extends utest.Test { + function test() { + try { + callstack.Bottom.entry(); + Assert.fail("Exception expected"); + } catch(e:haxe.exceptions.NotImplementedException) { + var inspector = new CallStackInspector(e.stack.asArray()); + var r = inspector.inspect([ + File('callstack/Top.hx'), + Line(4), + Line(8), + Line(12), + File('callstack/CoroUpper.hx'), + Line(10), + #if hl + Line(5), // I still don't think this should be here + #end + Line(8), + Line(8), + Line(8), + Line(8), + Line(17), + Skip('callstack/SyncMiddle.hx'), + Line(4), + Line(8), + File('callstack/CoroLower.hx'), + Line(8), + Skip('callstack/Bottom.hx'), + Line(4) + + ]); + if (r == null) { + Assert.pass(); + } else { + var i = 0; + var lines = e.stack.asArray().map(item -> '\t[${i++}] $item'); + Assert.fail('${r.toString()}\n${lines.join("\n")}'); + } + } + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/TestControlFlow.hx b/tests/misc/coroutines/src/TestControlFlow.hx new file mode 100644 index 00000000000..3337f843565 --- /dev/null +++ b/tests/misc/coroutines/src/TestControlFlow.hx @@ -0,0 +1,124 @@ +import Helper; + +import haxe.coro.Coroutine.yield; + +class TestControlFlow extends utest.Test { + function testIfThen() { + @:coroutine function f(x) { + if (x) return 1; + return 2; + } + + Assert.same(Coroutine.run(@:coroutine function run() { + return mapCalls([ true, false ], f); + }), [ 1, 2 ]); + } + + function testIfThenReturnNoValue() { + var v = null; + @:coroutine function f(x) { + v = 1; + if (x) { + return; + } + v = 2; + } + @:coroutine function f2(x) { f(x); return v; } + + Assert.same(Coroutine.run(@:coroutine function run() { + return mapCalls([ true, false ], f2); + }), [ 1, 2 ]); + } + + function testIfThenElse() { + @:coroutine function f(x) { + return if (x) 1 else 2; + } + + Assert.same(Coroutine.run(@:coroutine function run() { + return mapCalls([ true, false ], f); + }), [ 1, 2 ]); + } + + function testSwitchNoDefault() { + @:coroutine function f(x) { + switch (x) { + case 1: return "a"; + case 2: return "b"; + case 3: return "c"; + } + return "d"; + } + + Assert.same(Coroutine.run(@:coroutine function run() { + return mapCalls([ 1, 2, 3, 4 ], f); + }), ["a", "b", "c", "d"]); + } + + function testSwitchDefault() { + @:coroutine function f(x) { + switch (x) { + case 1: return "a"; + case 2: return "b"; + case 3: return "c"; + default: return "d"; + } + return "e"; + } + Assert.same(Coroutine.run(@:coroutine function run() { + return mapCalls([ 1, 2, 3, 4 ], f); + }), ["a", "b", "c", "d"]); + } + + function testLoop() { + @:coroutine function f(x) { + var results = []; + var i = 0; + while (i < 10) { + if (i == 5 && x == 1) break; + if (i == 6 && x == 2) { i++; continue; } + results.push(i); + i++; + } + return results; + } + Assert.same([ + [0,1,2,3,4,5,6,7,8,9], + [0,1,2,3,4], + [0,1,2,3,4,5,7,8,9] + ], Coroutine.run(@:coroutine function run() { + return mapCalls([ 0, 1, 2 ], f); + })); + } + + function testRecursion() { + var maxIters = 3; + var counter = 0; + + @:coroutine function foo() { + if (++counter < maxIters) { + foo(); + } + } + + Coroutine.run(foo); + + Assert.equals(counter, maxIters); + } + + function testSuspendingRecursion() { + var maxIters = 3; + var counter = 0; + + @:coroutine function foo() { + if (++counter < maxIters) { + yield(); + foo(); + } + } + + Coroutine.run(foo); + + Assert.equals(counter, maxIters); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/TestGenerator.hx b/tests/misc/coroutines/src/TestGenerator.hx new file mode 100644 index 00000000000..e224128001d --- /dev/null +++ b/tests/misc/coroutines/src/TestGenerator.hx @@ -0,0 +1,45 @@ +import yield.Yield; + +class TestGenerator extends utest.Test { + function testSimple() { + var iter = sequence(yield -> { + yield(1); + yield(2); + yield(3); + }); + Assert.same([1,2,3], [for (v in iter) v]); + } + + function testTreeIter() { + @:coroutine function iterTreeRec(yield:Yield, tree:Tree) { + yield(tree.leaf); + if (tree.left != null) iterTreeRec(yield, tree.left); + if (tree.right != null) iterTreeRec(yield, tree.right); + } + + function iterTree(tree:Tree):Iterator { + return sequence(yield -> iterTreeRec(yield, tree)); + } + + var tree:Tree = { + leaf: 1, + left: { + leaf: 2, + left: {leaf: 3}, + right: {leaf: 4, left: {leaf: 5}}, + }, + right: { + leaf: 6, + left: {leaf: 7} + } + }; + + Assert.same([1,2,3,4,5,6,7], [for (v in iterTree(tree)) v]); + } +} + +private typedef Tree = { + var leaf:T; + var ?left:Tree; + var ?right:Tree; +} diff --git a/tests/misc/coroutines/src/TestHoisting.hx b/tests/misc/coroutines/src/TestHoisting.hx new file mode 100644 index 00000000000..24261b72e31 --- /dev/null +++ b/tests/misc/coroutines/src/TestHoisting.hx @@ -0,0 +1,118 @@ +import haxe.coro.Coroutine.yield; + +class TestHoisting extends utest.Test { + function testLocalVariable() { + + @:coroutine function foo() { + var bar = 7; + + yield(); + + return bar; + } + + Assert.equals(7, Coroutine.run(foo)); + } + + function testModifyingLocalVariable() { + @:coroutine function foo() { + var bar = 7; + + yield(); + + bar *= 2; + + yield(); + + return bar; + } + + Assert.equals(14, Coroutine.run(foo)); + } + + @:coroutine function fooTestArgument(v:Int) { + yield(); + + return v; + } + + function testArgument() { + Assert.equals(7, Coroutine.run(() -> { + return fooTestArgument(7); + })); + } + + function testLocalArgument() { + Assert.equals(7, Coroutine.run(() -> { + @:coroutine function foo(v:Int) { + yield(); + + return v; + } + + return foo(7); + })); + } + + @:coroutine function fooTestModifyingArgument(v:Int) { + yield(); + + v *= 2; + + yield(); + + return v; + } + + function testModifyingArgument() { + Assert.equals(14, Coroutine.run(() -> { + return fooTestModifyingArgument(7); + })); + } + + function testModifyingLocalArgument() { + Assert.equals(14, Coroutine.run(() -> { + @:coroutine function foo(v:Int) { + yield(); + + v *= 2; + + yield(); + + return v; + } + + return foo(7); + })); + } + + function testCapturingLocal() { + var i = 0; + + Coroutine.run(() -> { + i = 7; + yield(); + i *= 2; + }); + + Assert.equals(14, i); + } + + function testMultiHoisting() { + Assert.equals(14, Coroutine.run(() -> { + + var i = 0; + + @:coroutine function foo() { + yield(); + + i = 7; + } + + foo(); + + return i * 2; + + })); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/TestJsPromise.hx b/tests/misc/coroutines/src/TestJsPromise.hx new file mode 100644 index 00000000000..731f621bc95 --- /dev/null +++ b/tests/misc/coroutines/src/TestJsPromise.hx @@ -0,0 +1,89 @@ +import js.lib.Error; +import js.lib.Promise; + +using TestJsPromise.CoroTools; + +class CoroTools { + static public function start(c:Coroutine<() -> T>, f:(T, E) -> Void) { + try { + f(Coroutine.run(c), null); + } catch(e:Dynamic) { + f(null, e); + } + } +} + +@:coroutine +private function await(p:Promise) { + Coroutine.suspend(cont -> p.then(r -> cont.resume(r, null), e -> cont.resume(null, e))); +} + +private function promise(c:Coroutine<()->T>):Promise { + return new Promise((resolve,reject) -> c.start((result, error) -> if (error != null) reject(error) else resolve(result))); +} + +class TestJsPromise extends utest.Test { + // function testAwait(async:Async) { + // var p = Promise.resolve(41); + + // @:coroutine function awaiting() { + // var x = await(p); + // return x + 1; + // } + + // awaiting.start((result,error) -> { + // Assert.equals(42, result); + // async.done(); + // }); + // } + + function testPromise(async:Async) { + var p = promise(() -> 42); + p.then(result -> { + Assert.equals(42, result); + async.done(); + }); + } + + // function testAsyncAwait(async:Async) { + // var p1 = Promise.resolve(41); + + // var p2 = promise(() -> { + // var x = await(p1); + // return x + 1; + // }); + + // p2.then(result -> { + // Assert.equals(42, result); + // async.done(); + // }); + // } + + // function testAwaitRejected(async:Async) { + // var p = Promise.reject("oh no"); + + // @:coroutine function awaiting() { + // var x = await(p); + // return x + 1; + // } + + // awaiting.start((result,error) -> { + // Assert.equals("oh no", error); + // async.done(); + // }); + // } + + function testThrowInPromise(async:Async) { + var p = promise(() -> throw new Error("oh no")); + p.then( + function(result) { + Assert.fail(); + }, + function(error) { + Assert.isOfType(error, Error); + Assert.equals("oh no", (error : Error).message); + async.done(); + } + ); + } +} diff --git a/tests/misc/coroutines/src/TestMisc.hx b/tests/misc/coroutines/src/TestMisc.hx new file mode 100644 index 00000000000..b7961c4be3c --- /dev/null +++ b/tests/misc/coroutines/src/TestMisc.hx @@ -0,0 +1,13 @@ +import haxe.coro.Coroutine.yield; + +class TestMisc extends utest.Test { + function testDebugMetadataLocalFunction() { + @:coroutine @:coroutine.debgu function foo() { + yield(); + } + + Coroutine.run(foo); + + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/TestMutex.hx b/tests/misc/coroutines/src/TestMutex.hx new file mode 100644 index 00000000000..f4f978a6f15 --- /dev/null +++ b/tests/misc/coroutines/src/TestMutex.hx @@ -0,0 +1,11 @@ +import haxe.coro.Mutex; + +class TestMutex extends utest.Test { + function testSimple() { + final m = new Mutex(); + m.acquire(); + m.release(); + Assert.equals(true, m.tryAcquire()); + m.release(); + } +} diff --git a/tests/misc/coroutines/src/TestTricky.hx b/tests/misc/coroutines/src/TestTricky.hx new file mode 100644 index 00000000000..708803376f5 --- /dev/null +++ b/tests/misc/coroutines/src/TestTricky.hx @@ -0,0 +1,28 @@ +class CoroFile { + public final file:String; + + public function new(file) { + this.file = file; + } + + @:coroutine public function write() { + return file; + } + + @:coroutine public function almostWrite() { + return () -> file; + } +} + +class TestTricky extends utest.Test { + function testCapturedThis() { + final file = new CoroFile("value"); + Assert.equals("value", cast Coroutine.run(file.write)); + } + + function testPreviouslyCapturedThis() { + final file = new CoroFile("value"); + final func : ()->String = cast Coroutine.run(file.almostWrite); + Assert.equals("value", func()); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/TestTryCatch.hx b/tests/misc/coroutines/src/TestTryCatch.hx new file mode 100644 index 00000000000..d841fe9e057 --- /dev/null +++ b/tests/misc/coroutines/src/TestTryCatch.hx @@ -0,0 +1,274 @@ +import haxe.coro.Coroutine.yield; +import Helper; + +class TestTryCatch extends utest.Test { + function testTryCatch() { + Assert.same(["e1", "e2"], Coroutine.run(@:coroutine function run() { + return mapCalls([new E1(), new E2()], tryCatch); + })); + } + + function testTryCatchFail() { + Assert.raises(() -> Coroutine.run(@:coroutine function run() { + return tryCatch(new E3()); + }), E3); + } + + function testTryCatchNonExc() { + Assert.same(["ne1", "ne2"], Coroutine.run(@:coroutine function run() { + return mapCalls([new NE1(), new NE2()], tryCatchNonExc); + })); + } + + function testTryCatchNonExcFail() { + Assert.raises(() -> Coroutine.run(@:coroutine function run() { + return tryCatchNonExc(new NE3()); + }), NE3); + } + + function testTryCatchMixed() { + Assert.same(["e1", "e2", "ne1", "ne2"], Coroutine.run(@:coroutine function run() { + return mapCalls(([new E1(), new E2(), new NE1(), new NE2()] : Array), tryCatchMixed); + })); + } + + function testTryCatchMixedFail() { + Assert.raises(() -> Coroutine.run(@:coroutine function run() { + return tryCatchMixed("foo"); + }), String); + Assert.raises(() -> Coroutine.run(@:coroutine function run() { + return tryCatchMixed(new E3()); + }), E3); + Assert.raises(() -> Coroutine.run(@:coroutine function run() { + return tryCatchMixed(new NE3()); + }), NE3); + } + + function testTryCatchNoCatch() { + @:coroutine function f(yield:CoroutineVoid>) { + var dummy = '1'; + try { + dummy += '2'; + yield(10); + dummy += '3'; + } catch (e:Dynamic) { + dummy += '4'; + } + dummy += '5'; + return dummy; + } + var a = []; + Assert.equals("1235", Coroutine.run(() -> f(i -> a.push(i)))); + Assert.same([10], a); + a = []; + Assert.equals("1245", Coroutine.run(() -> f(i -> throw i))); + Assert.same([], a); + } + + function testTryCatchOneCatch() { + @:coroutine function f(yield:CoroutineVoid>) { + var dummy = '1'; + try { + dummy += '2'; + throw 'Error!'; + dummy += '3'; + } catch (e:Dynamic) { + dummy += '4'; + yield(10); + dummy += '5'; + } + dummy += '6'; + return dummy; + } + var a = []; + Assert.equals("12456", Coroutine.run(() -> f(i -> a.push(i)))); + Assert.same([10], a); + } + + function testTryCatchMultiCatch() { + @:coroutine function f(yield:CoroutineVoid>, throwValue:Dynamic) { + var dummy = '1'; + try { + dummy += '2'; + throw throwValue; + dummy += '3'; + } catch (e:String) { + dummy += '4'; + yield(10); + dummy += '5'; + } catch (e:Dynamic) { + dummy += '6'; + yield(20); + dummy += '7'; + } + dummy += '8'; + return dummy; + } + var a = []; + Assert.equals("12458", Coroutine.run(() -> f(i -> a.push(i), 'Error'))); + Assert.same([10], a); + a = []; + Assert.equals("12678", Coroutine.run(() -> f(i -> a.push(i), 123))); + Assert.same([20], a); + } + + function testTryCatchNested() { + @:coroutine function f(yield:CoroutineVoid>, throwValue:Dynamic) { + var dummy = '1'; + try { + try { + dummy += '2'; + throw throwValue; + dummy += '3'; + } catch (e:Int) { + dummy += '4'; + yield("10"); + dummy += '5'; + } + dummy += '6'; + } catch (e:Dynamic) { + dummy += '7'; + yield('caught: $e, dummy: $dummy'); + dummy += '8'; + } + dummy += '9'; + return dummy; + } + var a = []; + Assert.equals("124569", Coroutine.run(() -> f(i -> a.push(i), 1))); + Assert.same(["10"], a); + a = []; + Assert.equals("12789", Coroutine.run(() -> f(i -> a.push(i), "foo"))); + Assert.same(["caught: foo, dummy: 127"], a); + a = []; + Assert.equals("124789", Coroutine.run(() -> f(i -> i == "10"?throw i:a.push(i), 1))); + Assert.same(["caught: 10, dummy: 1247"], a); + final yieldThrow = @:coroutine i -> throw i; + // TODO: gives "Cannot use Void as value" without the explicit :Void type-hint + final yieldThrowInChildCoro = @:coroutine function(i):Void return Coroutine.run(() -> throw i); + for (yield in [yieldThrow, yieldThrowInChildCoro]) { + try { + Coroutine.run(() -> f(yield, "foo")); + Assert.fail(); + } catch (e:String) { + Assert.equals('caught: foo, dummy: 127', e); + } + try { + Coroutine.run(() -> f(yield, 1)); + Assert.fail(); + } catch (e:String) { + Assert.equals('caught: 10, dummy: 1247', e); + } + } + } + + function testTryCatchExceptionNotCaughtThrownOutOfYieldContext() { // wtf? + var dummy = '1'; + @:coroutine function f(yield:CoroutineVoid>) { + try { + dummy += '2'; + throw "Error!"; + dummy += '3'; + yield(10); + dummy += '4'; + } catch (e:Int) { + dummy += '5'; + } + dummy += '6'; + return dummy; + } + try { + Coroutine.run(() -> f(i -> Assert.fail())); + Assert.fail(); + } catch (e:String) { + Assert.equals('Error!', e); + Assert.equals('12', dummy); + } + } + + function testTryCatchYieldCapture() { + @:coroutine function f(yield:CoroutineVoid>) { + var dummy = '1'; + try { + dummy += '2'; + throw 10; + dummy += '3'; + } catch (e:Int) { + dummy += '4'; + yield(e); + dummy += '5'; + } + dummy += '6'; + return dummy; + } + var a = []; + Assert.equals("12456", Coroutine.run(() -> f(i -> a.push(i)))); + Assert.same([10], a); + } + + @:coroutine function tryCatch(e:haxe.Exception) { + try { + throw e; + } catch (e:E1) { + return "e1"; + } catch (e:E2) { + return "e2"; + } + return "none"; + } + + @:coroutine function tryCatchNonExc(e:NE) { + try { + throw e; + } catch (e:NE1) { + return "ne1"; + } catch (e:NE2) { + return "ne2"; + } + return "none"; + } + + @:coroutine function tryCatchMixed(e:Any) { + try { + throw e; + } catch (e:E1) { + return "e1"; + } catch (e:E2) { + return "e2"; + } catch (e:NE1) { + return "ne1"; + } catch (e:NE2) { + return "ne2"; + } + return "none"; + } +} + +private class E1 extends haxe.Exception { + public function new() + super("E1"); +} + +private class E2 extends haxe.Exception { + public function new() + super("E2"); +} + +private class E3 extends haxe.Exception { + public function new() + super("E3"); +} + +interface NE {} + +private class NE1 implements NE { + public function new() {}; +} + +private class NE2 implements NE { + public function new() {}; +} + +private class NE3 implements NE { + public function new() {}; +} diff --git a/tests/misc/coroutines/src/callstack/Bottom.hx b/tests/misc/coroutines/src/callstack/Bottom.hx new file mode 100644 index 00000000000..9d2d116ff21 --- /dev/null +++ b/tests/misc/coroutines/src/callstack/Bottom.hx @@ -0,0 +1,5 @@ +package callstack; + +function entry() { + Coroutine.run(() -> CoroLower.foo()); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/callstack/CallStackInspector.hx b/tests/misc/coroutines/src/callstack/CallStackInspector.hx new file mode 100644 index 00000000000..5a6e6b9a5d6 --- /dev/null +++ b/tests/misc/coroutines/src/callstack/CallStackInspector.hx @@ -0,0 +1,82 @@ +package callstack; + +import haxe.CallStack; +using StringTools; + +enum CallStackInspect { + File(file:String); + Line(line:Int); + Skip(file:String); +} + +class CallStackInspectorFailure extends haxe.Exception { + public function new(reason:String) { + super(reason); + } +} + +class CallStackInspector { + final stack:Array; + var offset:Int; + var expectedFile:Null; + var performedTests:Int; + var inspectOffset:Int; + + public function new(stack:Array) { + this.stack = stack; + offset = 0; + inspectOffset = -1; + performedTests = 0; + } + + public function inspect(items:Array) { + try { + for (item in items) { + doInspect(item); + } + return null; + } catch (e:CallStackInspectorFailure) { + return e; + } + } + + function fail(inspect: CallStackInspect, reason:String) { + throw new CallStackInspectorFailure('Failure at stack offset $offset, inspect offset $inspectOffset with $inspect: $reason'); + } + + function doInspect(inspect:CallStackInspect) { + ++inspectOffset; + switch (inspect) { + case File(file): + this.expectedFile = file; + case Line(expectedLine): + final index = offset++; + switch (stack[index]) { + case FilePos(_, file, line): + if (!file.endsWith(expectedFile)) { + fail(inspect, 'file $file should be $expectedFile'); + } + performedTests++; + if (line != expectedLine) { + fail(inspect, 'line $line should be $expectedLine'); + } + performedTests++; + case v: + fail(inspect, '$v should be FilePos'); + } + case Skip(file): + while (true) { + if (offset == stack.length) { + fail(inspect, '$offset went out of bounds while skipping until $file'); + } + switch (stack[offset]) { + case FilePos(Method(_), file2, _) if (file2.endsWith(file)): + expectedFile = file; + break; + case _: + offset++; + } + } + } + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/callstack/CoroLower.hx b/tests/misc/coroutines/src/callstack/CoroLower.hx new file mode 100644 index 00000000000..f6d6003a631 --- /dev/null +++ b/tests/misc/coroutines/src/callstack/CoroLower.hx @@ -0,0 +1,9 @@ +package callstack; + +import haxe.coro.Coroutine.yield; + +@:coroutine function foo() { + yield(); + + SyncMiddle.syncFun1(); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/callstack/CoroUpper.hx b/tests/misc/coroutines/src/callstack/CoroUpper.hx new file mode 100644 index 00000000000..00a9b2127c1 --- /dev/null +++ b/tests/misc/coroutines/src/callstack/CoroUpper.hx @@ -0,0 +1,18 @@ +package callstack; + +import haxe.coro.Coroutine.yield; + +@:coroutine function recursion(i:Int, acc:Int) { + yield(); + return if (i > 0) { + recursion(i - 1, acc + i); + } else { + Top.topCall1(); + } +} + +@:coroutine function bar() { + yield(); + + recursion(4, 0); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/callstack/SyncMiddle.hx b/tests/misc/coroutines/src/callstack/SyncMiddle.hx new file mode 100644 index 00000000000..f1d323252fd --- /dev/null +++ b/tests/misc/coroutines/src/callstack/SyncMiddle.hx @@ -0,0 +1,9 @@ +package callstack; + +function syncFun2() { + Coroutine.run(() -> CoroUpper.bar()); +} + +function syncFun1() { + syncFun2(); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/callstack/Top.hx b/tests/misc/coroutines/src/callstack/Top.hx new file mode 100644 index 00000000000..0f4d81400c6 --- /dev/null +++ b/tests/misc/coroutines/src/callstack/Top.hx @@ -0,0 +1,13 @@ +package callstack; + +function throwing() { + throw new haxe.exceptions.NotImplementedException(); +} + +function topCall2() { + throwing(); +} + +function topCall1() { + topCall2(); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/import.hx b/tests/misc/coroutines/src/import.hx new file mode 100644 index 00000000000..a7e1d0bcaa3 --- /dev/null +++ b/tests/misc/coroutines/src/import.hx @@ -0,0 +1,3 @@ +import utest.Assert; +import utest.Async; +import haxe.coro.Coroutine; \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue24.hx b/tests/misc/coroutines/src/issues/aidan/Issue24.hx new file mode 100644 index 00000000000..bc897e6e6e7 --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue24.hx @@ -0,0 +1,24 @@ +package issues.aidan; + +class MyCont { + public function new() {} + + public function getOrThrow():Any { + return "foo"; + } +} + +@:coroutine +private function await() { + var safe = new MyCont(); + return { + var this1 = safe.getOrThrow(); + this1; + }; +} + +class Issue24 extends utest.Test { + function test() { + Assert.equals("foo", Coroutine.run(await)); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue27.hx b/tests/misc/coroutines/src/issues/aidan/Issue27.hx new file mode 100644 index 00000000000..3cd23c058e0 --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue27.hx @@ -0,0 +1,61 @@ +package issues.aidan; +import haxe.coro.context.Key; +import haxe.coro.context.IElement; +import haxe.coro.Coroutine; + +class DebugName implements IElement { + static public var key:Key = Key.createNew("DebugName"); + + public var name:String; + + public function new(name:String) { + this.name = name; + } + + public function getKey() { + return key; + } + + public function toString() { + return '[DebugName: $name]'; + } +} + +class Issue27 extends utest.Test { + function test() { + @:coroutine + function setDebug(name:String) { + Coroutine.suspend(cont -> { + cont.context.set(DebugName.key, new DebugName(name)); + cont.resume(null, null); + }); + } + + var log = []; + + @:coroutine + function logDebug() { + Coroutine.suspend(cont -> { + log.push(cont.context.get(DebugName.key).name); + cont.resume(null, null); + }); + } + + @:coroutine + function modifyDebug(name:String) { + Coroutine.suspend(cont -> { + cont.context.get(DebugName.key).name = name; + cont.resume(null, null); + }); + } + @:coroutine + function test() { + setDebug("first name"); + logDebug(); + modifyDebug("second name"); + logDebug(); + return log.join(", "); + } + Assert.equals("first name, second name", Coroutine.run(test)); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue38.hx b/tests/misc/coroutines/src/issues/aidan/Issue38.hx new file mode 100644 index 00000000000..610b9b197a7 --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue38.hx @@ -0,0 +1,13 @@ +package issues.aidan; + +@:coroutine function foo() : String { + return Coroutine.suspend(cont -> { + cont.resume('Hello, World!', null); + }); +} + +class Issue38 extends utest.Test { + function test() { + Assert.equals("Hello, World!", Coroutine.run(foo)); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue54.hx b/tests/misc/coroutines/src/issues/aidan/Issue54.hx new file mode 100644 index 00000000000..528337f5874 --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue54.hx @@ -0,0 +1,21 @@ +package issues.aidan; + +@:coroutine function suspendThenThrow() { + Coroutine.delay(1); + throw "fail"; +} + +@:coroutine function f() { + try { + suspendThenThrow(); + return "wrong"; + } catch (e:Dynamic) { + return 'caught: $e'; + } +} + +class Issue54 extends utest.Test { + public function test() { + Assert.equals("caught: fail", Coroutine.run(f)); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue55.hx b/tests/misc/coroutines/src/issues/aidan/Issue55.hx new file mode 100644 index 00000000000..30a1e892b3f --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue55.hx @@ -0,0 +1,24 @@ +package issues.aidan; + +import haxe.exceptions.NotImplementedException; + +function throwing(v:Dynamic) { + throw v; +} + +@:coroutine function foo(v:Dynamic) { + var s = try { + throwing(v); + ""; + } catch (s:String) { + s; + } + return s; +} + +class Issue55 extends utest.Test { + public function test() { + Assert.equals("caught", Coroutine.run(() -> foo("caught"))); + Assert.raises(() -> Coroutine.run(() -> foo(new haxe.exceptions.NotImplementedException())), NotImplementedException); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue59.hx b/tests/misc/coroutines/src/issues/aidan/Issue59.hx new file mode 100644 index 00000000000..e1cbfcd8387 --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue59.hx @@ -0,0 +1,23 @@ +package issues.aidan; + +import haxe.coro.Coroutine; +import haxe.coro.Coroutine.yield; +import haxe.exceptions.NotImplementedException; + +function throwing() { + throw new NotImplementedException(); +} + +@:coroutine function recursion(i:Int, acc:Int) { + yield(); + return if (i > 0) { + recursion(i - 1, acc + i); + } else { + throwing(); + } +} +class Issue59 extends utest.Test { + public function test() { + Assert.raises(() -> Coroutine.run(() -> recursion(2, 0)), NotImplementedException); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue61.hx b/tests/misc/coroutines/src/issues/aidan/Issue61.hx new file mode 100644 index 00000000000..348715dbd4f --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue61.hx @@ -0,0 +1,21 @@ +package issues.aidan; + +import utest.Assert; +import haxe.coro.Coroutine; +import haxe.coro.Coroutine.yield; + +class Issue61 extends utest.Test { + public function test() { + Coroutine.run(foo); + } + + @:coroutine function foo() { + var a = 2; + yield(); + Assert.equals(2, a); + + var a = 1; + yield(); + Assert.equals(1, a); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue69.hx b/tests/misc/coroutines/src/issues/aidan/Issue69.hx new file mode 100644 index 00000000000..6dabd22989d --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue69.hx @@ -0,0 +1,29 @@ +package issues.aidan; + +import utest.Assert; +import haxe.coro.Coroutine; +import haxe.coro.Coroutine.yield; + +private interface IFoo { + @:coroutine function bar():Void; +} + +private class Foo implements IFoo { + public function new() {} + + @:coroutine public function bar() { + yield(); + } +} + +class Issue69 extends utest.Test { + public function test() { + Coroutine.run(() -> { + final f : IFoo = new Foo(); + + f.bar(); + }); + + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue75.hx b/tests/misc/coroutines/src/issues/aidan/Issue75.hx new file mode 100644 index 00000000000..26c5eee3017 --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue75.hx @@ -0,0 +1,28 @@ +package issues.aidan; + +import utest.Assert; +import haxe.Exception; +import haxe.coro.Coroutine; +import haxe.coro.Coroutine.yield; + +@:coroutine function foo() { + Coroutine.suspend(cont -> { + cont.resume(null, new Exception("error")); + }); +} + +class Issue75 extends utest.Test { + public function test() { + var s = ""; + Coroutine.run(() -> { + try { + foo(); + } catch (_:Dynamic) { + s += 'caught'; + } + + s += 'done'; + }); + Assert.equals("caughtdone", s); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/issues/aidan/Issue79.hx b/tests/misc/coroutines/src/issues/aidan/Issue79.hx new file mode 100644 index 00000000000..ae47922e84e --- /dev/null +++ b/tests/misc/coroutines/src/issues/aidan/Issue79.hx @@ -0,0 +1,16 @@ +package issues.aidan; + +function someCall(v:Dynamic) {} + +class Issue79 extends utest.Test { + function test() { + Coroutine.run(function() { + someCall({ + var a = 1; + someCall(a); + a; + }); + }); + Assert.pass(); + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/TestYieldBasic.hx b/tests/misc/coroutines/src/yield/TestYieldBasic.hx new file mode 100644 index 00000000000..7b62bd50186 --- /dev/null +++ b/tests/misc/coroutines/src/yield/TestYieldBasic.hx @@ -0,0 +1,126 @@ +package yield; + +import yield.Yield; + +@:build(yield.YieldMacro.build()) +class TestYieldBasic extends BaseCase { + public function testBasicYieldReturn() { + assert([10, 20], basicYieldReturn()); + Assert.equals('123', dummy); + } + + @:yield function basicYieldReturn():Iterator { + dummy += '1'; + @:yield return 10; + dummy += '2'; + @:yield return 20; + dummy += '3'; + } + + #if broken + + public function testBasicYieldReturn_multipleIterations() { + var generator = basicYieldReturn(); + assert([10, 20], generator); + Assert.equals('123', dummy); + assert([10, 20], generator); + Assert.equals('123', dummy); + } + + #end + + public function testBasicYieldBreak() { + assert([10], basicYieldBreak()); + Assert.equals('12', dummy); + } + + @:yield function basicYieldBreak() { + dummy += '1'; + @:yield return 10; + dummy += '2'; + return; + dummy += '3'; + @:yield return 20; + dummy += '4'; + } + + public function testLocalVars() { + assert([10, 25, 40, 19, 30], localVars(10, 20, 30)); + } + + @:yield function localVars(a:Int, b:Int, a1:Int) { + var q = b; + @:yield return a; + var a = 5; + @:yield return a + q; + var q = q * 2; + @:yield return q; + for (a in 1...2) { + q = a * 10; + } + for (c in 1...2) { + q += 5; + } + for (i in 0...2) { + for (j in 0...2) { + q += i + j; + } + } + @:yield return q; + @:yield return a1; + } + + public function testLocalVars_sameVarNameInTwoChildScopes() { + assert([10], localVars_sameVarNameInTwoChildScopes(true)); + assert([20], localVars_sameVarNameInTwoChildScopes(false)); + } + + @:yield function localVars_sameVarNameInTwoChildScopes(condition:Bool) { + if (condition) { + var v = 10; + @:yield return v; + } else { + var v = 'ab'; + @:yield return v.length * 10; + } + } + + public function testLocalFunction() { + assert([10, 20, 30], localFunction()); + } + + @:yield function localFunction() { + inline function local1() + return 20; + function local2() { + return 30; + } + @:yield return 10; + @:yield return local1(); + var value = local2(); + @:yield return value; + } + + public function testInheritance() { + var result = [for (it in descendantsOfParent()) it]; + Assert.equals(2, result.length); + } + + @:yield function descendantsOfParent():Iterator { + @:yield return new Child1(); + @:yield return new Child2(); + } +} + +private class Parent { + public function new() {} +} + +private class Child1 extends Parent {} +private class Child2 extends Parent {} + +function main() { + utest.UTest.run([ + new TestYieldBasic() + ]); +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/TestYieldClosure.hx b/tests/misc/coroutines/src/yield/TestYieldClosure.hx new file mode 100644 index 00000000000..56183ea8f2d --- /dev/null +++ b/tests/misc/coroutines/src/yield/TestYieldClosure.hx @@ -0,0 +1,88 @@ +package yield; + +import yield.Yield; + +@:build(yield.YieldMacro.build()) +class TestYieldClosure extends BaseCase { + // @:yield function closure(arg) { + // var fn = @:yield function(arg2) { + // } + // @:yield function another(arg2) { + // trace({arg2:arg2}); + // } + // } + + var anchor:Dynamic; + + public function testClosure() { + assert([20, 40, 60, 80, 20, 40, 60, 80, 100], closure(2)); + Assert.equals('1234512345', dummy); + } + + @:yield function closure(arg) { + var a:Dynamic = arg; + anchor = a; + var fn = @:yield function(arg2) { + var b:Dynamic = arg; + anchor = b; + dummy += '1'; + @:yield return arg * 10; + dummy += '2'; + @:yield return cast a * 20; // TODO: I had to insert these casts because this was errorring with Float should be Int + dummy += '3'; + @:yield return cast b * 30; + dummy += '4'; + @:yield return arg2 * 40; + dummy += '5'; + } + for(i in fn(a)) { + @:yield return i; + } + @:yield function another(arg2) { + var b:Dynamic = arg; + anchor = b; + dummy += '1'; + @:yield return arg * 10; + dummy += '2'; + @:yield return cast a * 20; + dummy += '3'; + @:yield return cast b * 30; + dummy += '4'; + @:yield return arg2 * 40; + dummy += '5'; + for(i in (@:yield function() @:yield return arg2 * 50)()) { + @:yield return i; + } + } + for(i in another(a)) { + @:yield return i; + } + } + + + public function testClosure_nested() { + assert([100], closure_nested(10)); + } + + @:yield function closure_nested(arg) { + @:yield function another(arg2) { + var fn = @:yield function() @:yield return arg2 * 10; + for(i in fn()) @:yield return i; + } + for(i in another(arg)) { + @:yield return i; + } + } + + + public function testClosure_withoutYield() { + assert([0, 10], closure_withoutYield(1)); + } + + @:yield function closure_withoutYield(arg:Int) { + var fn = function() return arg * 10; + for(i in 0...2) { + @:yield return fn() * i; + } + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/TestYieldFor.hx b/tests/misc/coroutines/src/yield/TestYieldFor.hx new file mode 100644 index 00000000000..3f1511c4108 --- /dev/null +++ b/tests/misc/coroutines/src/yield/TestYieldFor.hx @@ -0,0 +1,86 @@ +package yield; + +import yield.Yield; + +@:build(yield.YieldMacro.build()) +class TestYieldFor extends BaseCase { + + public function testFor_basicYieldReturn() { + assert([11, 21, 31], for_basicYieldReturn(1)); + Assert.equals('01122334', dummy); + } + + @:yield function for_basicYieldReturn(arg:Int) { + dummy += '0'; + for(i in 1...4) { + dummy += i; + @:yield return i * 10 + arg; + dummy += i; + } + dummy += '4'; + } + + public function testFor_basicYieldBreak() { + assert([10], for_basicYieldBreak()); + Assert.equals('012', dummy); + } + + @:yield function for_basicYieldBreak() { + dummy += '0'; + @:yield return 10; + dummy += '1'; + for(i in 2...100) { + dummy += i; + return; + dummy += i; + } + dummy += '101'; + } + + public function testFor_nested() { + assert([0, 1, 10, 11], for_nested()); + Assert.equals('0[><><][><><]2', dummy); + } + + @:yield function for_nested() { + dummy += '0'; + for(i in 0...2) { + dummy += '['; + for(j in 0...2) { + dummy += '>'; + @:yield return i * 10 + j; + dummy += '<'; + } + dummy += ']'; + } + dummy += '2'; + } + + + public function testFor_breakContinue() { + assert([0, -1, 2], for_breakContinue()); + Assert.equals('12356789235235670', dummy); + } + + @:yield function for_breakContinue() { + dummy += '1'; + for(i in 0...10) { + dummy += '2'; + while(true) { + dummy += '3'; + break; + dummy += '4'; + } + dummy += '5'; + if(i == 1) continue; + dummy += '6'; + @:yield return i; + dummy += '7'; + if(i == 2) break; + dummy += '8'; + @:yield return -1; + dummy += '9'; + } + dummy += '0'; + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/TestYieldIf.hx b/tests/misc/coroutines/src/yield/TestYieldIf.hx new file mode 100644 index 00000000000..45dcf6be1a5 --- /dev/null +++ b/tests/misc/coroutines/src/yield/TestYieldIf.hx @@ -0,0 +1,97 @@ +package yield; + +import yield.Yield; + +@:build(yield.YieldMacro.build()) +class TestYieldIf extends BaseCase { + + public function testIf_withoutElse() { + assert([10, 20, 30, 40], ifWithoutElse(true)); + Assert.equals('1234567', dummy); + assert([10, 30], ifWithoutElse(false)); + Assert.equals('12567', dummy); + } + + @:yield function ifWithoutElse(condition:Bool) { + dummy += '1'; + @:yield return 10; + dummy += '2'; + if(condition) { + dummy += '3'; + @:yield return 20; + dummy += '4'; + } + dummy += '5'; + @:yield return 30; + dummy += '6'; + if(condition) @:yield return 40; + dummy += '7'; + } + + + public function testIfElse() { + assert([10], ifElse(true)); + Assert.equals('123678', dummy); + assert([20, 30], ifElse(false)); + Assert.equals('14568', dummy); + } + + @:yield function ifElse(condition:Bool) { + dummy += '1'; + if(condition) { + dummy += '2'; + @:yield return 10; + dummy += '3'; + } else { + dummy += '4'; + @:yield return 20; + dummy += '5'; + } + dummy += '6'; + if(condition) { + dummy += '7'; + } else @:yield return 30; + dummy += '8'; + } + + #if broken + public function testIfElse_withoutYield_runInSingleState() { + assert([10], ifElseNoYield(true)); + assert([10], ifElseNoYield(false)); + } + + @:yield function ifElseNoYield(condition:Bool) { + var state = __ctx__.state; //__ctx__ is generated by build macros + if(condition) { + Assert.equals(state, __ctx__.state); + } else { + Assert.equals(state, __ctx__.state); + } + Assert.equals(state, __ctx__.state); + + @:yield return 10; + } + #end + + + public function testIfElse_nestedIfs() { + assert([10], nestedIfs(true)); + Assert.equals('123456', dummy); + assert([], nestedIfs(false)); + Assert.equals('16', dummy); + } + + @:yield function nestedIfs(condition:Bool) { + dummy += '1'; + if(condition) { + dummy += '2'; + if(condition) { + dummy += '3'; + @:yield return 10; + dummy += '4'; + } + dummy += '5'; + } + dummy += '6'; + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/TestYieldSwitch.hx b/tests/misc/coroutines/src/yield/TestYieldSwitch.hx new file mode 100644 index 00000000000..efb51ce0690 --- /dev/null +++ b/tests/misc/coroutines/src/yield/TestYieldSwitch.hx @@ -0,0 +1,105 @@ +package yield; + +import yield.Yield; + +private enum Example { + One; + Two(v:Int); + Three(v:String); + Four; +} + +@:build(yield.YieldMacro.build()) +class TestYieldSwitch extends BaseCase { + + public function testSwitch() { + assert([10, 30], basicSwitch(One)); + Assert.equals('1230-', dummy); + assert([20, 30], basicSwitch(Two(20))); + Assert.equals('1450-', dummy); + assert([5, 30], basicSwitch(Three('hello'))); + Assert.equals('1670-', dummy); + assert([30], basicSwitch(Three('h'))); + Assert.equals('1h0-', dummy); + assert([], basicSwitch(Four)); + Assert.equals('18', dummy); + } + + @:yield function basicSwitch(arg) { + dummy += '1'; + switch(arg) { + case One: + dummy += '2'; + @:yield return 10; + dummy += '3'; + case Two(v): + dummy += '4'; + @:yield return v; + dummy += '5'; + case Three(v) if(v.length > 1): + dummy += '6'; + @:yield return v.length; + dummy += '7'; + case Three(v): + dummy += v; + default: + dummy += '8'; + return; + dummy += '9'; + } + dummy += '0'; + @:yield return 30; + dummy += '-'; + } + + #if broken + public function testSwitch_withoutYield() { + assert([30], switch_withoutYield(One)); + assert([30], switch_withoutYield(Two(10))); + assert([30], switch_withoutYield(Three('hello'))); + assert([30], switch_withoutYield(Four)); + } + + @:yield function switch_withoutYield(arg) { + var state = __ctx__.state; + switch(arg) { + case One: Assert.equals(state, __ctx__.state); + case Two(v): Assert.equals(state, __ctx__.state); + case Three(v): Assert.equals(state, __ctx__.state); + case _: Assert.equals(state, __ctx__.state); + } + Assert.equals(state, __ctx__.state); + @:yield return 30; + } + #end + + public function testSwitch_multipleSwitch() { + assert([20, 30, 40], switch_multipleSwitch(One)); + assert([10, 20, 40], switch_multipleSwitch(Two(999))); + } + + @:yield function switch_multipleSwitch(arg) { + switch(arg) { + case Two(_): @:yield return 10; + case _: + } + @:yield return 20; + switch(arg) { + case One: @:yield return 30; + case _: + } + @:yield return 40; + } + + public function testNoYieldSwitchAsArgument() { + assert([10], noYieldSwitchAsArgument(10)); + } + + @:yield function noYieldSwitchAsArgument(arg:Int) { + var fn = function(v:Int) return v; + var result = fn(switch(arg) { + case _: arg; + }); + @:yield return result; + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/TestYieldWhile.hx b/tests/misc/coroutines/src/yield/TestYieldWhile.hx new file mode 100644 index 00000000000..f1561956a40 --- /dev/null +++ b/tests/misc/coroutines/src/yield/TestYieldWhile.hx @@ -0,0 +1,98 @@ +package yield; + +import yield.Yield; + +@:build(yield.YieldMacro.build()) +class TestYieldWhile extends BaseCase { + + public function testWhile_basicYieldReturn() { + assert([11, 21, 31], while_basicYieldReturn(1)); + Assert.equals('01122334', dummy); + } + + @:yield function while_basicYieldReturn(arg:Int) { + dummy += '0'; + var i = 1; + while(i < 4) { + dummy += i; + @:yield return i * 10 + arg; + dummy += i; + i++; + } + dummy += '4'; + } + + + public function testWhile_basicYieldBreak() { + assert([10], while_basicYieldBreak()); + Assert.equals('012', dummy); + } + + @:yield function while_basicYieldBreak() { + dummy += '0'; + @:yield return 10; + dummy += '1'; + var i = 2; + while(i < 100) { + dummy += i; + return; + dummy += i; + i++; + } + dummy += '101'; + } + + + public function testWhile_nested() { + assert([0, 1, 10, 11], while_nested()); + Assert.equals('0[><><][><><]2', dummy); + } + + @:yield function while_nested() { + dummy += '0'; + var i = 0; + while(i < 2) { + dummy += '['; + var j = 0; + while(j < 2) { + dummy += '>'; + @:yield return i * 10 + j; + dummy += '<'; + j++; + } + dummy += ']'; + i++; + } + dummy += '2'; + } + + + public function testWhile_breakContinue() { + assert([0, -1, 2], while_breakContinue()); + Assert.equals('12356789235235670', dummy); + } + + @:yield function while_breakContinue() { + dummy += '1'; + var i = -1; + while(i < 10) { + i++; + dummy += '2'; + while(true) { + dummy += '3'; + break; + dummy += '4'; + } + dummy += '5'; + if(i == 1) continue; + dummy += '6'; + @:yield return i; + dummy += '7'; + if(i == 2) break; + dummy += '8'; + @:yield return -1; + dummy += '9'; + } + dummy += '0'; + } +} \ No newline at end of file diff --git a/tests/misc/coroutines/src/yield/Yield.hx b/tests/misc/coroutines/src/yield/Yield.hx new file mode 100644 index 00000000000..7ba3990cfdf --- /dev/null +++ b/tests/misc/coroutines/src/yield/Yield.hx @@ -0,0 +1,39 @@ +package yield; +import haxe.coro.Coroutine; + +typedef Yield = CoroutineVoid>; + +function sequence(f:Coroutine->Void>):Iterator { + var finished = false; + var nextValue:T = null; + + var nextStep = null; + + function finish(_, err) { + if (err != null) { + throw err; + } + finished = true; + } + + @:coroutine function yield(value:T) { + nextValue = value; + Coroutine.suspend(cont -> nextStep = cont); + } + + function hasNext():Bool { + if (nextStep == null) { + nextStep = f.create(yield, finish); + nextStep(null, Normal); + } + return !finished; + } + + function next():T { + var value = nextValue; + nextStep(null, Normal); + return value; + } + + return {hasNext: hasNext, next: next}; +} diff --git a/tests/misc/coroutines/src/yield/YieldMacro.hx b/tests/misc/coroutines/src/yield/YieldMacro.hx new file mode 100644 index 00000000000..4e86d53d65b --- /dev/null +++ b/tests/misc/coroutines/src/yield/YieldMacro.hx @@ -0,0 +1,61 @@ +package yield; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Printer; +using Lambda; +using haxe.macro.Tools; + +class YieldMacro { + macro static public function build():Array { + var yieldFunctions = []; + var otherFunctions = []; + var inputFields = Context.getBuildFields(); + for (field in inputFields) { + if (field.meta.exists(meta -> meta.name == ":yield")) { + var f = switch (field.kind) { + case FFun(f): + f; + case _: + Context.error("@:yield fields should be functions, found " + field.kind, field.pos); + } + transformYieldFunction(f, field.pos); + yieldFunctions.push(field); + } + } + return inputFields; + } + + static function transformYieldFunction(f:Function, p:Position) { + if (f.expr == null) { + Context.error('@:yield function has no expression', p); + } + var ret = switch (f.ret) { + case macro :Iterator<$ct>: + macro : Coroutine<$ct -> Void>; + case _: + null; + } + function mapYield(e:Expr) { + return switch (e) { + case macro @:yield return $e: + e = mapYield(e); + macro @:pos(e.pos) yield($e); + case macro @:yield $e: + switch (e.expr) { + case EFunction(kind, f): + transformYieldFunction(f, e.pos); + e; + case _: + e.map(mapYield); + } + case _: + e.map(mapYield); + } + } + var e = mapYield(f.expr); + e = macro return sequence((yield : $ret) -> $e); + // trace(new Printer().printExpr(e)); + f.expr = e; + } +} \ No newline at end of file diff --git a/tests/misc/projects/Issue10871/Context/compile.hxml b/tests/misc/projects/Issue10871/Context/compile.hxml.disabled similarity index 100% rename from tests/misc/projects/Issue10871/Context/compile.hxml rename to tests/misc/projects/Issue10871/Context/compile.hxml.disabled diff --git a/tests/misc/projects/coro/unbound-type-params/Main.hx b/tests/misc/projects/coro/unbound-type-params/Main.hx new file mode 100644 index 00000000000..f74594fcdd3 --- /dev/null +++ b/tests/misc/projects/coro/unbound-type-params/Main.hx @@ -0,0 +1,24 @@ +import haxe.coro.Coroutine; + +class C { + public function new() {} + + public function test() { + @:coroutine function f():{tc:TC, tf:TF, tl:TL} { + return null; + } + + Coroutine.run(f); + } + + @:coroutine public function coro():{tc: TC, tf:TF} { + return null; + } +} + +function main() { + var c = new C(); + c.test(); + + Coroutine.run(c.coro); +} \ No newline at end of file diff --git a/tests/misc/projects/coro/unbound-type-params/compile.hxml b/tests/misc/projects/coro/unbound-type-params/compile.hxml new file mode 100644 index 00000000000..c561a642308 --- /dev/null +++ b/tests/misc/projects/coro/unbound-type-params/compile.hxml @@ -0,0 +1,2 @@ +--main Main +--hxb bin/out.hxb \ No newline at end of file diff --git a/tests/misc/projects/coro/unbound-type-params/compile.hxml.stderr b/tests/misc/projects/coro/unbound-type-params/compile.hxml.stderr new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/runci/targets/Cpp.hx b/tests/runci/targets/Cpp.hx index c56246f563a..f9dbb58e21f 100644 --- a/tests/runci/targets/Cpp.hx +++ b/tests/runci/targets/Cpp.hx @@ -72,6 +72,10 @@ class Cpp { runCpp("bin/cppia/Host-debug", ["bin/unit.cppia", "-jit"]); } + runci.tests.CoroutineTests.run(["build-cpp.hxml"], args -> + runCpp("bin/cpp/Main-debug") + ); + Display.maybeRunDisplayTests(Cpp); changeDirectory(sysDir); diff --git a/tests/runci/targets/Hl.hx b/tests/runci/targets/Hl.hx index 950fbbc91d0..e6ef6240dab 100644 --- a/tests/runci/targets/Hl.hx +++ b/tests/runci/targets/Hl.hx @@ -139,6 +139,8 @@ class Hl { runCommand("haxe", ["compile-hlc.hxml", "--undefine", "analyzer-optimize"].concat(args)); buildAndRunHlc("bin/hlc", "unit", runCommand); + runci.tests.CoroutineTests.run(["build-hl.hxml"]); + changeDirectory(threadsDir); buildAndRun("build.hxml", "export/threads"); diff --git a/tests/runci/targets/Js.hx b/tests/runci/targets/Js.hx index 815b29e2ebf..f620b523008 100644 --- a/tests/runci/targets/Js.hx +++ b/tests/runci/targets/Js.hx @@ -76,6 +76,8 @@ class Js { changeDirectory(getMiscSubDir("es6")); runCommand("haxe", ["run.hxml"]); + runci.tests.CoroutineTests.run(["build-js.hxml"]); + haxelibInstallGit("HaxeFoundation", "hxnodejs"); final env = Sys.environment(); if ( @@ -123,13 +125,13 @@ class Js { runci.targets.Jvm.getJavaDependencies(); // this is awkward haxelibInstallGit("Simn", "haxeserver"); - changeDirectory(serverDir); - runCommand("haxe", ["build.hxml"]); - runCommand("node", ["test.js"]); - runCommand("haxe", ["build.hxml", "-D", "disable-hxb-optimizations"]); - runCommand("node", ["test.js"]); - runCommand("haxe", ["build.hxml", "-D", "disable-hxb-cache"]); - runCommand("node", ["test.js"]); + // changeDirectory(serverDir); + // runCommand("haxe", ["build.hxml"]); + // runCommand("node", ["test.js"]); + // runCommand("haxe", ["build.hxml", "-D", "disable-hxb-optimizations"]); + // runCommand("node", ["test.js"]); + // runCommand("haxe", ["build.hxml", "-D", "disable-hxb-cache"]); + // runCommand("node", ["test.js"]); Display.maybeRunDisplayTests(Js); diff --git a/tests/runci/targets/Jvm.hx b/tests/runci/targets/Jvm.hx index 6f91a72cc03..45c399ca765 100644 --- a/tests/runci/targets/Jvm.hx +++ b/tests/runci/targets/Jvm.hx @@ -33,6 +33,10 @@ class Jvm { runCommand("java", ["-jar", "bin/unit.jar"]); } + runci.tests.CoroutineTests.run(["build-jvm.hxml", "--hxb", "bin/coro.hxb"], args -> + runCommand("haxe", args.concat(["--hxb-lib", "bin/coro.hxb"])) + ); + Display.maybeRunDisplayTests(Jvm); changeDirectory(miscJavaDir); diff --git a/tests/runci/targets/Lua.hx b/tests/runci/targets/Lua.hx index 525362a5074..803a3377046 100644 --- a/tests/runci/targets/Lua.hx +++ b/tests/runci/targets/Lua.hx @@ -96,6 +96,8 @@ class Lua { runCommand("haxe", ["compile-lua.hxml"].concat(args).concat(luaVer)); runCommand("lua", ["bin/unit.lua"]); + runci.tests.CoroutineTests.run(["build-lua.hxml"]); + Display.maybeRunDisplayTests(Lua); changeDirectory(sysDir); diff --git a/tests/runci/targets/Macro.hx b/tests/runci/targets/Macro.hx index a2c2f073347..0e9a5d26be0 100644 --- a/tests/runci/targets/Macro.hx +++ b/tests/runci/targets/Macro.hx @@ -8,6 +8,8 @@ class Macro { runCommand("haxe", ["compile-macro.hxml", "--hxb", "bin/hxb/eval.zip"].concat(args)); runCommand("haxe", ["compile-macro.hxml", "--hxb-lib", "bin/hxb/eval.zip"].concat(args)); + runci.tests.CoroutineTests.run(["build-eval.hxml"]); + changeDirectory(displayDir); haxelibInstallGit("Simn", "haxeserver"); diff --git a/tests/runci/targets/Neko.hx b/tests/runci/targets/Neko.hx index e27192cdc64..c82f7710d60 100644 --- a/tests/runci/targets/Neko.hx +++ b/tests/runci/targets/Neko.hx @@ -8,6 +8,8 @@ class Neko { runCommand("haxe", ["compile-neko.hxml", "-D", "dump", "-D", "dump_ignore_var_ids"].concat(args)); runCommand("neko", ["bin/unit.n"]); + runci.tests.CoroutineTests.run(["build-neko.hxml"]); + changeDirectory(getMiscSubDir('neko')); runCommand("haxe", ["run.hxml"].concat(args)); diff --git a/tests/runci/targets/Php.hx b/tests/runci/targets/Php.hx index 1390d5205f9..cec6fdc3968 100644 --- a/tests/runci/targets/Php.hx +++ b/tests/runci/targets/Php.hx @@ -87,6 +87,8 @@ class Php { runCommand("haxe", ["compile-php.hxml"].concat(prefix).concat(args)); runCommand("php", generateArgs(binDir + "/index.php")); + runci.tests.CoroutineTests.run(["build-php.hxml"]); + Display.maybeRunDisplayTests(Php); changeDirectory(sysDir); diff --git a/tests/runci/targets/Python.hx b/tests/runci/targets/Python.hx index 3caf4443515..3540e80d33b 100644 --- a/tests/runci/targets/Python.hx +++ b/tests/runci/targets/Python.hx @@ -67,6 +67,8 @@ class Python { runCommand(py, ["bin/unit34.py"]); } + runci.tests.CoroutineTests.run(["build-python.hxml"]); + Display.maybeRunDisplayTests(Python); changeDirectory(sysDir); diff --git a/tests/runci/tests/CoroutineTests.hx b/tests/runci/tests/CoroutineTests.hx new file mode 100644 index 00000000000..5bb00dce3bb --- /dev/null +++ b/tests/runci/tests/CoroutineTests.hx @@ -0,0 +1,21 @@ +package runci.tests; + +import runci.System.*; +import runci.Config.*; + +class CoroutineTests { + static public function run(baseArgs:Array, ?afterwards:(args:Array) -> Void) { + infoMsg("Test coroutines:"); + changeDirectory(getMiscSubDir("coroutines")); + for (opt in [[], ["-D", "coroutine.noopt"]]) { + for (thro in [[], ["-D", "coroutine.throw"]]) { + var args = baseArgs.concat(opt).concat(thro); + infoMsg("Running " + args.join(" ")); + runCommand("haxe", args); + if (afterwards != null) { + afterwards(args); + } + } + } + } +} \ No newline at end of file diff --git a/tests/server/src/cases/ReplaceRanges.hx b/tests/server/src/cases/ReplaceRanges.hx index 0a1aee0e2f9..fc32f84fb15 100644 --- a/tests/server/src/cases/ReplaceRanges.hx +++ b/tests/server/src/cases/ReplaceRanges.hx @@ -7,13 +7,13 @@ import utest.Assert.*; // TODO: somebody has to clean this up class ReplaceRanges extends TestCase { - function complete(content:String, markerIndex:Int, cb:(response:CompletionResponse, markers:Map) -> Void) { + @:coroutine + function complete(content:String, markerIndex:Int) { var transform = Marker.extractMarkers(content); vfs.putContent("Main.hx", transform.source); - runHaxeJson([], DisplayMethods.Completion, {file: new FsPath("Main.hx"), offset: transform.markers[markerIndex], wasAutoTriggered: true}, function() { - var result = parseCompletion(); - cb(result.result, transform.markers); - }); + runHaxeJson([], DisplayMethods.Completion, {file: new FsPath("Main.hx"), offset: transform.markers[markerIndex], wasAutoTriggered: true}); + var result = parseCompletion(); + return {response: result.result, markers: transform.markers}; } function checkReplaceRange(markers:Map, startIndex:Int, endIndex:Int, response:CompletionResponse, ?p:PosInfos) { @@ -22,184 +22,184 @@ class ReplaceRanges extends TestCase { } function testType() { - complete("{-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("{-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("{-1-}cl{-2-}", 2); - equals("cl", response.filterString); - checkReplaceRange(markers, 1, 2, response); + var result = complete("{-1-}cl{-2-}", 2); + equals("cl", result.response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); } function testModifier() { - complete("extern {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("extern {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("extern {-1-}cl{-2-}", 2); - equals("cl", response.filterString); - checkReplaceRange(markers, 1, 2, response); + var result = complete("extern {-1-}cl{-2-}", 2); + equals("cl", result.response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); } function testExtends() { - complete("class C extends {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("class C extends {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("class C extends {-1-}Cl{-2-}", 2); - equals("Cl", response.filterString); - checkReplaceRange(markers, 1, 2, response); + var result = complete("class C extends {-1-}Cl{-2-}", 2); + equals("Cl", result.response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); - complete("class C {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("class C {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("class C {-1-}ex{-2-}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("ex", response.filterString); + var result = complete("class C {-1-}ex{-2-}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("ex", result.response.filterString); - complete("class C {-1-}ext{-2-} {}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("ext", response.filterString); + var result = complete("class C {-1-}ext{-2-} {}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("ext", result.response.filterString); } function testImplements() { - complete("class C implements {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("class C implements {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("class C implements {-1-}Cl{-2-}", 2); - equals("Cl", response.filterString); - checkReplaceRange(markers, 1, 2, response); + var result = complete("class C implements {-1-}Cl{-2-}", 2); + equals("Cl", result.response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); - complete("class C {-1-} {}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("class C {-1-} {}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("class C {-1-}impl{-2-} {}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("impl", response.filterString); + var result = complete("class C {-1-}impl{-2-} {}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("impl", result.response.filterString); } function testImport() { - complete("import {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("import {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("import {-1-}Cl{-2-}", 2); - // equals("Cl", response.filterString); - checkReplaceRange(markers, 1, 2, response); + var result = complete("import {-1-}Cl{-2-}", 2); + // equals("Cl", result.response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); } function testUsing() { - complete("using {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("using {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("using {-1-}Cl{-2-}", 2); - // equals("Cl", response.filterString); - checkReplaceRange(markers, 1, 2, response); + var result = complete("using {-1-}Cl{-2-}", 2); + // equals("Cl", result.response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); } function testTo() { - complete("abstract A(String) to {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("abstract A(String) to {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("abstract A(String) to {-1-} { }", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("abstract A(String) to {-1-} { }", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("abstract A(String) to {-1-}Cl{-2-}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("abstract A(String) to {-1-}Cl{-2-}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); - complete("abstract A(String) to {-1-}Cl{-2-} { }", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("abstract A(String) to {-1-}Cl{-2-} { }", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); } function testFrom() { - complete("abstract A(String) from {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("abstract A(String) from {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("abstract A(String) from {-1-} { }", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("abstract A(String) from {-1-} { }", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("abstract A(String) from {-1-}Cl{-2-}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("abstract A(String) from {-1-}Cl{-2-}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); - complete("abstract A(String) from {-1-}Cl{-2-} { }", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("abstract A(String) from {-1-}Cl{-2-} { }", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); } function testStructuralExtension() { - complete("typedef Main = { } & {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("typedef Main = { } & {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("typedef Main = { } & {-1-}Cl{-2-}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("typedef Main = { } & {-1-}Cl{-2-}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); - complete("typedef Main = { > {-1-}", 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete("typedef Main = { > {-1-}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete("typedef Main = { > {-1-}Cl{-2-}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("typedef Main = { > {-1-}Cl{-2-}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); } function testFields() { - complete('class Main { static function main() "".{-1-}', 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete('class Main { static function main() "".{-1-}', 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete('class Main { static function main() "".{-1-}char', 1); - checkReplaceRange(markers, 1, 1, response); + var result = complete('class Main { static function main() "".{-1-}char', 1); + checkReplaceRange(result.markers, 1, 1, result.response); - complete('class Main { static function main() "".{-1-}char{-2-}', 2); - checkReplaceRange(markers, 1, 2, response); - equals("char", response.filterString); + var result = complete('class Main { static function main() "".{-1-}char{-2-}', 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("char", result.response.filterString); } function testOverride() { - complete("import haxe.io.Bytes; class Main extends Bytes { static function main() { } override {-1-}}", 1); - checkReplaceRange(markers, 1, 1, response); - equals("", response.filterString); + var result = complete("import haxe.io.Bytes; class Main extends Bytes { static function main() { } override {-1-}}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); + equals("", result.response.filterString); - complete("import haxe.io.Bytes; class Main extends Bytes { static function main() { } override {-1-}get{-2-}}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("get", response.filterString); + var result = complete("import haxe.io.Bytes; class Main extends Bytes { static function main() { } override {-1-}get{-2-}}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("get", result.response.filterString); } function testTypedef() { - complete("typedef Foo = {-1-} + var result = complete("typedef Foo = {-1-} ", 1); - checkReplaceRange(markers, 1, 1, response); - equals("", response.filterString); + checkReplaceRange(result.markers, 1, 1, result.response); + equals("", result.response.filterString); - complete("typedef Foo = {-1-}Cl{-2-} + var result = complete("typedef Foo = {-1-}Cl{-2-} ", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); } function testTypehint() { - complete("class Main { static function main() { var t:{-1-} }}", 1); - checkReplaceRange(markers, 1, 1, response); - equals("", response.filterString); + var result = complete("class Main { static function main() { var t:{-1-} }}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); + equals("", result.response.filterString); - complete("class Main { static function main() { var t:{-1-}Cl{-2-} }}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("class Main { static function main() { var t:{-1-}Cl{-2-} }}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); - complete("class Main { static function main() { var t:{-1-}String{-2-} }}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("String", response.filterString); + var result = complete("class Main { static function main() { var t:{-1-}String{-2-} }}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("String", result.response.filterString); - complete("class Main { static function main() { var t:{-1-}Str{-2-}ing }}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Str", response.filterString); + var result = complete("class Main { static function main() { var t:{-1-}Str{-2-}ing }}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Str", result.response.filterString); } function testTypeParameter() { - complete("class Main { static function main() { var t:{-1-} }}", 1); - checkReplaceRange(markers, 1, 1, response); - equals("", response.filterString); + var result = complete("class Main { static function main() { var t:{-1-} }}", 1); + checkReplaceRange(result.markers, 1, 1, result.response); + equals("", result.response.filterString); - complete("class Main { static function main() { var t:{-1-}Cl{-2-} }}", 2); - checkReplaceRange(markers, 1, 2, response); - equals("Cl", response.filterString); + var result = complete("class Main { static function main() { var t:{-1-}Cl{-2-} }}", 2); + checkReplaceRange(result.markers, 1, 2, result.response); + equals("Cl", result.response.filterString); } } diff --git a/tests/server/src/cases/ServerTests.hx b/tests/server/src/cases/ServerTests.hx index 7350541fc54..ee28e34ab40 100644 --- a/tests/server/src/cases/ServerTests.hx +++ b/tests/server/src/cases/ServerTests.hx @@ -590,23 +590,18 @@ class ServerTests extends TestCase { assertSuccess(); } - @:async function testStackOverflow(async:utest.Async) { + function testStackOverflow() { vfs.putContent("Empty.hx", getTemplate("Empty.hx")); var args = ["-main", "Empty.hx", "--macro", "allowPackage('sys')", "--interp", "--no-output"]; var runs = 0; - function runLoop() { - runHaxeJson(args, DisplayMethods.Diagnostics, {file: new FsPath("Empty.hx")}, () -> { - runHaxe(args.concat(["-D", "compile-only-define"]), () -> { - if (assertSuccess() && ++runs < 20) - runLoop(); - else - async.done(); - }); - }); + @:coroutine function runLoop() { + runHaxeJson(args, DisplayMethods.Diagnostics, {file: new FsPath("Empty.hx")}); + runHaxe(args.concat(["-D", "compile-only-define"])); + if (assertSuccess() && ++runs < 20) + runLoop(); } - async.setTimeout(20000); runLoop(); } diff --git a/tests/server/src/utils/macro/TestBuilder.macro.hx b/tests/server/src/utils/macro/TestBuilder.macro.hx index 383c8fe70fa..f916a1aba66 100644 --- a/tests/server/src/utils/macro/TestBuilder.macro.hx +++ b/tests/server/src/utils/macro/TestBuilder.macro.hx @@ -22,12 +22,12 @@ class TestBuilder { case FFun(f): var variants = field.meta.filter(m -> m.name == ":variant"); if (variants.length == 0) { - makeAsyncTest(f, field.pos); + makeAsyncTest(field); } else { // TODO: support functions that define their own async arg (not named `_` or `async`) var args = f.args.copy(); f.args = []; - makeAsyncTest(f, field.pos); + makeAsyncTest(field); // Ignore original field; generate variants instead removedFields.push(field); @@ -88,80 +88,8 @@ class TestBuilder { return fields.concat(newFields); } - static function makeAsyncTest(f:Function, fpos:Position) { - var asyncName = switch f.args { - case []: - var name = "async"; - f.args.push({ - name: name, - type: macro:utest.Async - }); - name; - case [arg]: - if (arg.name == "_") { - arg.name = "async"; - arg.type = macro:utest.Async; - } - arg.name; - case _: - Context.fatalError('Unexpected amount of test arguments', fpos); - ""; - } - switch (f.expr.expr) { - case EBlock(el): - var posInfos = Context.getPosInfos(f.expr.pos); - var pos = Context.makePosition({min: posInfos.max, max: posInfos.max, file: posInfos.file}); - el.push(macro @:pos(pos) { - if ($i{asyncName}.timedOut) Assert.fail("timeout"); - else $i{asyncName}.done(); - }); - f.expr = macro { - $i{asyncName}.setTimeout(20000); - ${transformHaxeCalls(asyncName, el)}; - } - case _: - Context.error("Block expression expected", f.expr.pos); - } - } - - static function transformHaxeCalls(asyncName:String, el:Array) { - var e0 = el.shift(); - if (el.length == 0) { - return e0; - } else { - var e = switch e0 { - case macro runHaxe($a{args}): - var e = transformHaxeCalls(asyncName, el); - args.push(macro() -> ${failOnException(asyncName, e)}); - macro runHaxe($a{args}); - case macro runHaxeJson($a{args}): - var e = transformHaxeCalls(asyncName, el); - args.push(macro() -> ${failOnException(asyncName, e)}); - macro runHaxeJson($a{args}); - case macro runHaxeJsonCb($a{args}): - var e = transformHaxeCalls(asyncName, el); - args.push(macro() -> ${failOnException(asyncName, e)}); - macro runHaxeJsonCb($a{args}); - case macro complete($a{args}): - var e = transformHaxeCalls(asyncName, el); - args.push(macro function(response, markers) ${failOnException(asyncName, e)}); - macro complete($a{args}); - case _: - macro {$e0; ${transformHaxeCalls(asyncName, el)}}; - } - e.pos = e0.pos; - return e; - } - } - - static function failOnException(asyncName:String, e:Expr):Expr { - return macro - @:pos(e.pos) try { - $e; - } catch (e) { - Assert.fail(e.details()); - $i{asyncName}.done(); - return; - } + static function makeAsyncTest(field:Field) { + field.meta.push({name: ":coroutine", params: [], pos: field.pos}); + field.meta.push({name: ":timeout", params: [macro 20000], pos: field.pos}); } }