diff --git a/document/core/appendix/changes.rst b/document/core/appendix/changes.rst index 2aabac110e..be21696272 100644 --- a/document/core/appendix/changes.rst +++ b/document/core/appendix/changes.rst @@ -160,3 +160,13 @@ Added vector type and instructions that manipulate multiple numeric values in pa Release 3.0 ~~~~~~~~~~~ +Tail calls +.......... + +Added instructions to perform tail calls [#proposal-tailcall]_. + +* New :ref:`control instructions `: :math:`RETURNCALL` and :math:`RETURNCALLINDIRECT` + + +.. [#proposal-tailcall] + https://github.com/WebAssembly/spec/tree/main/proposals/tail-call/ diff --git a/document/core/appendix/index-instructions.py b/document/core/appendix/index-instructions.py index c3ffc2a324..9f601fe36f 100755 --- a/document/core/appendix/index-instructions.py +++ b/document/core/appendix/index-instructions.py @@ -87,8 +87,8 @@ def Instruction(name, opcode, type=None, validation=None, execution=None, operat Instruction(r'\RETURN', r'\hex{0F}', r'[t_1^\ast~t^\ast] \to [t_2^\ast]', r'valid-return', r'exec-return'), Instruction(r'\CALL~x', r'\hex{10}', r'[t_1^\ast] \to [t_2^\ast]', r'valid-call', r'exec-call'), Instruction(r'\CALLINDIRECT~x~y', r'\hex{11}', r'[t_1^\ast~\I32] \to [t_2^\ast]', r'valid-call_indirect', r'exec-call_indirect'), - Instruction(None, r'\hex{12}'), - Instruction(None, r'\hex{13}'), + Instruction(r'\RETURNCALL~x', r'\hex{12}', r'[t_1^\ast] \to [t_2^\ast]', r'valid-return_call', r'exec-return_call'), + Instruction(r'\RETURNCALLINDIRECT~x~y', r'\hex{13}', r'[t_1^\ast~\I32] \to [t_2^\ast]', r'valid-return_call_indirect', r'exec-return_call_indirect'), Instruction(None, r'\hex{14}'), Instruction(None, r'\hex{15}'), Instruction(None, r'\hex{16}'), diff --git a/document/core/binary/instructions.rst b/document/core/binary/instructions.rst index e13a03236e..d07df8e1c3 100644 --- a/document/core/binary/instructions.rst +++ b/document/core/binary/instructions.rst @@ -37,6 +37,8 @@ Control Instructions .. _binary-return: .. _binary-call: .. _binary-call_indirect: +.. _binary-return_call: +.. _binary-return_call_indirect: .. math:: \begin{array}{llcllll} @@ -63,6 +65,8 @@ Control Instructions \hex{0F} &\Rightarrow& \RETURN \\ &&|& \hex{10}~~x{:}\Bfuncidx &\Rightarrow& \CALL~x \\ &&|& \hex{11}~~y{:}\Btypeidx~~x{:}\Btableidx &\Rightarrow& \CALLINDIRECT~x~y \\ + \hex{12}~~x{:}\Bfuncidx &\Rightarrow& \RETURNCALL~x \\ &&|& + \hex{13}~~y{:}\Btypeidx~~x{:}\Btableidx &\Rightarrow& \RETURNCALLINDIRECT~x~y \\ \end{array} .. note:: diff --git a/document/core/conf.py b/document/core/conf.py index 06478c7c63..17174572a7 100644 --- a/document/core/conf.py +++ b/document/core/conf.py @@ -66,10 +66,10 @@ logo = 'static/webassembly.png' # The name of the GitHub repository this resides in -repo = 'spec' +repo = 'tail-call' # The name of the proposal it represents, if any -proposal = '' +proposal = 'tail calls' # The draft version string (clear out for release cuts) draft = ' (Draft ' + date.today().strftime("%Y-%m-%d") + ')' diff --git a/document/core/exec/instructions.rst b/document/core/exec/instructions.rst index ccb27bfd31..76bf9b7d54 100644 --- a/document/core/exec/instructions.rst +++ b/document/core/exec/instructions.rst @@ -2806,7 +2806,7 @@ Control Instructions .. math:: ~\\[-1ex] \begin{array}{lcl@{\qquad}l} - \FRAME_n\{F\}~\XB^k[\val^n~\RETURN]~\END &\stepto& \val^n + \FRAME_n\{F\}~B^\ast[\val^n~\RETURN]~\END &\stepto& \val^n \end{array} @@ -2900,6 +2900,84 @@ Control Instructions \end{array} +.. _exec-return_call: + +:math:`\RETURNCALL~x` +..................... + +.. todo: find a way to reuse call/call_indirect prose for tail call versions + +1. Let :math:`F` be the :ref:`current ` :ref:`frame `. + +2. Assert: due to :ref:`validation `, :math:`F.\AMODULE.\MIFUNCS[x]` exists. + +3. Let :math:`a` be the :ref:`function address ` :math:`F.\AMODULE.\MIFUNCS[x]`. + +4. :ref:`Tail-invoke ` the function instance at address :math:`a`. + + +.. math:: + \begin{array}{lcl@{\qquad}l} + (\RETURNCALL~x) &\stepto& (\RETURNINVOKE~a) + & (\iff (\CALL~x) \stepto (\INVOKE~a)) + \end{array} + + +.. _exec-return_call_indirect: + +:math:`\RETURNCALLINDIRECT~x~y` +............................... + +1. Let :math:`F` be the :ref:`current ` :ref:`frame `. + +2. Assert: due to :ref:`validation `, :math:`F.\AMODULE.\MITABLES[x]` exists. + +3. Let :math:`\X{ta}` be the :ref:`table address ` :math:`F.\AMODULE.\MITABLES[x]`. + +4. Assert: due to :ref:`validation `, :math:`S.\STABLES[\X{ta}]` exists. + +5. Let :math:`\X{tab}` be the :ref:`table instance ` :math:`S.\STABLES[\X{ta}]`. + +6. Assert: due to :ref:`validation `, :math:`F.\AMODULE.\MITYPES[y]` exists. + +7. Let :math:`\X{ft}_{\F{expect}}` be the :ref:`function type ` :math:`F.\AMODULE.\MITYPES[y]`. + +8. Assert: due to :ref:`validation `, a value with :ref:`value type ` |I32| is on the top of the stack. + +9. Pop the value :math:`\I32.\CONST~i` from the stack. + +10. If :math:`i` is not smaller than the length of :math:`\X{tab}.\TIELEM`, then: + + a. Trap. + +11. If :math:`\X{tab}.\TIELEM[i]` is uninitialized, then: + + a. Trap. + +12. Let :math:`a` be the :ref:`function address ` :math:`\X{tab}.\TIELEM[i]`. + +13. Assert: due to :ref:`validation `, :math:`S.\SFUNCS[a]` exists. + +14. Let :math:`\X{f}` be the :ref:`function instance ` :math:`S.\SFUNCS[a]`. + +15. Let :math:`\X{ft}_{\F{actual}}` be the :ref:`function type ` :math:`\X{f}.\FITYPE`. + +16. If :math:`\X{ft}_{\F{actual}}` and :math:`\X{ft}_{\F{expect}}` differ, then: + + a. Trap. + +17. :ref:`Tail-invoke ` the function instance at address :math:`a`. + + +.. math:: + \begin{array}{lcl@{\qquad}l} + \val~(\RETURNCALLINDIRECT~x~y) &\stepto& (\RETURNINVOKE~a) + & (\iff \val~(\CALLINDIRECT~x~y) \stepto (\INVOKE~a)) \\ + \val~(\RETURNCALLINDIRECT~x~y) &\stepto& \TRAP + & (\iff \val~(\CALLINDIRECT~x~y) \stepto \TRAP) \\ + \end{array} + + .. index:: instruction, instruction sequence, block .. _exec-instr-seq: @@ -3005,6 +3083,42 @@ Invocation of :ref:`function address ` :math:`a` \end{array} +.. _exec-return-invoke: + +Tail-invocation of :ref:`function address ` :math:`a` +...................................................................... + +1. Assert: due to :ref:`validation `, :math:`S.\SFUNCS[a]` exists. + +2. Let :math:`[t_1^n] \to [t_2^m]` be the :ref:`function type ` :math:`S.\SFUNCS[a].\FITYPE`. + +3. Assert: due to :ref:`validation `, there are at least :math:`n` values on the top of the stack. + +4. Pop the results :math:`\val^n` from the stack. + +5. Assert: due to :ref:`validation `, the stack contains at least one :ref:`frame `. + +6. While the top of the stack is not a frame, do: + + a. Pop the top element from the stack. + +7. Assert: the top of the stack is a frame. + +8. Pop the frame from the stack. + +9. Push :math:`\val^n` to the stack. + +10. :ref:`Invoke ` the function instance at address :math:`a`. + +.. math:: + ~\\[-1ex] + \begin{array}{lcl@{\qquad}l} + S; \FRAME_m\{F\}~B^\ast[\val^n~(\RETURNINVOKE~a)]~\END &\stepto& + \val^n~(\INVOKE~a) + & (\iff S.\SFUNCS[a].\FITYPE = [t_1^n] \to [t_2^m]) + \end{array} + + .. _exec-invoke-exit: Returning from a function diff --git a/document/core/exec/runtime.rst b/document/core/exec/runtime.rst index 6c53a4e965..8e165e927b 100644 --- a/document/core/exec/runtime.rst +++ b/document/core/exec/runtime.rst @@ -533,6 +533,7 @@ In order to express the reduction of :ref:`traps `, :ref:`calls `, identified by its :ref:`address `. It unifies the handling of different forms of calls. +Analogously, |RETURNINVOKE| represents the imminent tail invocation of a function instance. The |LABEL| and |FRAME| instructions model :ref:`labels ` and :ref:`frames ` :ref:`"on the stack" `. Moreover, the administrative syntax maintains the nesting structure of the original :ref:`structured control instruction ` or :ref:`function body ` and their :ref:`instruction sequences ` with an |END| marker. diff --git a/document/core/syntax/instructions.rst b/document/core/syntax/instructions.rst index ddb51d7ee6..350a8121ed 100644 --- a/document/core/syntax/instructions.rst +++ b/document/core/syntax/instructions.rst @@ -652,6 +652,8 @@ Instructions in this group affect the flow of control. \RETURN \\&&|& \CALL~\funcidx \\&&|& \CALLINDIRECT~\tableidx~\typeidx \\ + \RETURNCALL~\funcidx \\&&|& + \RETURNCALLINDIRECT~\tableidx~\typeidx \\ \end{array} The |NOP| instruction does nothing. @@ -698,6 +700,10 @@ The |CALLINDIRECT| instruction calls a function indirectly through an operand in Since it may contain functions of heterogeneous type, the callee is dynamically checked against the :ref:`function type ` indexed by the instruction's second immediate, and the call is aborted with a :ref:`trap ` if it does not match. +The |RETURNCALL| and |RETURNCALLINDIRECT| instructions are *tail-call* variants of the previous ones. +That is, they first return from the current function before actually performing the respective call. +It is guaranteed that no sequence of nested calls using only these instructions can cause resource exhaustion due to hitting an :ref:`implementation's limit ` on the number of active calls. + .. index:: ! expression, constant, global, offset, element, data, instruction pair: abstract syntax; expression diff --git a/document/core/text/instructions.rst b/document/core/text/instructions.rst index 0e8c577de4..9c08fd1f11 100644 --- a/document/core/text/instructions.rst +++ b/document/core/text/instructions.rst @@ -103,6 +103,8 @@ However, the special case of a type use that is syntactically empty or consists .. _text-return: .. _text-call: .. _text-call_indirect: +.. _text-return_call: +.. _text-return_call_indirect: All other control instruction are represented verbatim. @@ -118,6 +120,9 @@ All other control instruction are represented verbatim. \text{return} &\Rightarrow& \RETURN \\ &&|& \text{call}~~x{:}\Tfuncidx_I &\Rightarrow& \CALL~x \\ &&|& \text{call\_indirect}~~x{:}\Ttableidx~~y,I'{:}\Ttypeuse_I &\Rightarrow& \CALLINDIRECT~x~y + & (\iff I' = \{\ILOCALS~(\epsilon)^\ast\}) \\&&|& + \text{return\_call}~~x{:}\Tfuncidx_I &\Rightarrow& \RETURNCALL~x \\ &&|& + \text{return\_call\_indirect}~~x{:}\Ttableidx~~y,I'{:}\Ttypeuse_I &\Rightarrow& \RETURNCALLINDIRECT~x~y & (\iff I' = \{\ILOCALS~(\epsilon)^\ast\}) \\ \end{array} @@ -138,14 +143,17 @@ The :math:`\text{else}` keyword of an :math:`\text{if}` instruction can be omitt \text{if}~~\Tlabel~~\Tblocktype~~\Tinstr^\ast~~\text{else}~~\text{end} \end{array} -Also, for backwards compatibility, the table index to :math:`\text{call\_indirect}` can be omitted, defaulting to :math:`0`. +Also, for backwards compatibility, the table index to :math:`\text{call\_indirect}` and :math:`\text{return\_call\_indirect}` can be omitted, defaulting to :math:`0`. .. math:: \begin{array}{llclll} \production{plain instruction} & \text{call\_indirect}~~\Ttypeuse &\equiv& - \text{call\_indirect}~~0~~\Ttypeuse + \text{call\_indirect}~~0~~\Ttypeuse \\ + \text{return\_call\_indirect}~~\Ttypeuse + &\equiv& + \text{return\_call\_indirect}~~0~~\Ttypeuse \\ \end{array} diff --git a/document/core/util/macros.def b/document/core/util/macros.def index df4af62f83..d61b56c090 100644 --- a/document/core/util/macros.def +++ b/document/core/util/macros.def @@ -368,6 +368,8 @@ .. |RETURN| mathdef:: \xref{syntax/instructions}{syntax-instr-control}{\K{return}} .. |CALL| mathdef:: \xref{syntax/instructions}{syntax-instr-control}{\K{call}} .. |CALLINDIRECT| mathdef:: \xref{syntax/instructions}{syntax-instr-control}{\K{call\_indirect}} +.. |RETURNCALL| mathdef:: \xref{syntax/instructions}{syntax-instr-control}{\K{return\_call}} +.. |RETURNCALLINDIRECT| mathdef:: \xref{syntax/instructions}{syntax-instr-control}{\K{return\_call\_indirect}} .. |DROP| mathdef:: \xref{syntax/instructions}{syntax-instr-parametric}{\K{drop}} .. |SELECT| mathdef:: \xref{syntax/instructions}{syntax-instr-parametric}{\K{select}} @@ -1068,6 +1070,7 @@ .. |REFEXTERNADDR| mathdef:: \xref{exec/runtime}{syntax-ref.extern}{\K{ref{.}extern}} .. |TRAP| mathdef:: \xref{exec/runtime}{syntax-trap}{\K{trap}} .. |INVOKE| mathdef:: \xref{exec/runtime}{syntax-invoke}{\K{invoke}} +.. |RETURNINVOKE| mathdef:: \xref{exec/runtime}{syntax-return_invoke}{\K{return\_invoke}} .. Values & Results, non-terminals diff --git a/document/core/valid/instructions.rst b/document/core/valid/instructions.rst index afcda69c01..5ef57ca1d6 100644 --- a/document/core/valid/instructions.rst +++ b/document/core/valid/instructions.rst @@ -1511,6 +1511,71 @@ Control Instructions } +.. _valid-return_call: + +:math:`\RETURNCALL~x` +..................... + +* The return type :math:`C.\CRETURN` must not be absent in the context. + +* The function :math:`C.\CFUNCS[x]` must be defined in the context. + +* Let :math:`[t_1^\ast] \to [t_2^\ast]` be the :ref:`function type ` :math:`C.\CFUNCS[x]`. + +* The :ref:`result type ` :math:`[t_2^\ast]` must be the same as :math:`C.\CRETURN`. + +* Then the instruction is valid with type :math:`[t_3^\ast~t_1^\ast] \to [t_4^\ast]`, for any sequences of :ref:`value types ` :math:`t_3^\ast` and :math:`t_4^\ast`. + +.. math:: + \frac{ + C.\CFUNCS[x] = [t_1^\ast] \to [t_2^\ast] + \qquad + C.\CRETURN = [t_2^\ast] + }{ + C \vdashinstr \RETURNCALL~x : [t_3^\ast~t_1^\ast] \to [t_4^\ast] + } + +.. note:: + The |RETURNCALL| instruction is :ref:`stack-polymorphic `. + + +.. _valid-return_call_indirect: + +:math:`\RETURNCALLINDIRECT~x~y` +............................... + +* The return type :math:`C.\CRETURN` must not be empty in the context. + +* The table :math:`C.\CTABLES[x]` must be defined in the context. + +* Let :math:`\limits~\reftype` be the :ref:`table type ` :math:`C.\CTABLES[x]`. + +* The :ref:`reference type ` :math:`\reftype` must be |FUNCREF|. + +* The type :math:`C.\CTYPES[y]` must be defined in the context. + +* Let :math:`[t_1^\ast] \to [t_2^\ast]` be the :ref:`function type ` :math:`C.\CTYPES[y]`. + +* The :ref:`result type ` :math:`[t_2^\ast]` must be the same as :math:`C.\CRETURN`. + +* Then the instruction is valid with type :math:`[t_3^\ast~t_1^\ast~\I32] \to [t_4^\ast]`, for any sequences of :ref:`value types ` :math:`t_3^\ast` and :math:`t_4^\ast`. + + +.. math:: + \frac{ + C.\CTABLES[x] = \limits~\FUNCREF + \qquad + C.\CTYPES[y] = [t_1^\ast] \to [t_2^\ast] + \qquad + C.\CRETURN = [t_2^\ast] + }{ + C \vdashinstr \RETURNCALLINDIRECT~x~y : [t_3^\ast~t_1^\ast~\I32] \to [t_4^\ast] + } + +.. note:: + The |RETURNCALLINDIRECT| instruction is :ref:`stack-polymorphic `. + + .. index:: instruction, instruction sequence .. _valid-instr-seq: diff --git a/interpreter/README.md b/interpreter/README.md index 1df7ff715a..a4a658a7fb 100644 --- a/interpreter/README.md +++ b/interpreter/README.md @@ -234,6 +234,8 @@ op: br_if br_table + return + return_call + return_call_indirect call call_indirect ? drop diff --git a/interpreter/binary/decode.ml b/interpreter/binary/decode.ml index fa3a0ef9e5..287645ba3f 100644 --- a/interpreter/binary/decode.ml +++ b/interpreter/binary/decode.ml @@ -278,8 +278,13 @@ let rec instr s = let y = at var s in let x = at var s in call_indirect x y + | 0x12 -> return_call (at var s) + | 0x13 -> + let y = at var s in + let x = at var s in + return_call_indirect x y - | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 | 0x18 | 0x19 as b -> illegal s pos b + | 0x14 | 0x15 | 0x16 | 0x17 | 0x18 | 0x19 as b -> illegal s pos b | 0x1a -> drop | 0x1b -> select None diff --git a/interpreter/binary/encode.ml b/interpreter/binary/encode.ml index f5665bb1cd..c76f1fde08 100644 --- a/interpreter/binary/encode.ml +++ b/interpreter/binary/encode.ml @@ -173,6 +173,8 @@ struct | Return -> op 0x0f | Call x -> op 0x10; var x | CallIndirect (x, y) -> op 0x11; var y; var x + | ReturnCall x -> op 0x12; var x + | ReturnCallIndirect (x, y) -> op 0x13; var y; var x | Drop -> op 0x1a | Select None -> op 0x1b diff --git a/interpreter/exec/eval.ml b/interpreter/exec/eval.ml index 90221c4cdf..a6803fcd01 100644 --- a/interpreter/exec/eval.ml +++ b/interpreter/exec/eval.ml @@ -61,6 +61,7 @@ and admin_instr' = | Invoke of func_inst | Trapping of string | Returning of value stack + | ReturningInvoke of value stack * func_inst | Breaking of int32 * value stack | Label of int32 * instr list * code | Frame of int32 * frame * code @@ -205,6 +206,21 @@ let rec step (c : config) : config = else vs, [Invoke func @@ e.at] + | ReturnCall x, vs -> + (match (step {c with code = (vs, [Plain (Call x) @@ e.at])}).code with + | vs', [{it = Invoke a; at}] -> vs', [ReturningInvoke (vs', a) @@ at] + | _ -> assert false + ) + + | ReturnCallIndirect (x, y), vs -> + (match + (step {c with code = (vs, [Plain (CallIndirect (x, y)) @@ e.at])}).code + with + | vs', [{it = Invoke a; at}] -> vs', [ReturningInvoke (vs', a) @@ at] + | vs', [{it = Trapping s; at}] -> vs', [Trapping s @@ at] + | _ -> assert false + ) + | Drop, v :: vs' -> vs', [] @@ -594,13 +610,14 @@ let rec step (c : config) : config = | Refer r, vs -> Ref r :: vs, [] - | Trapping msg, vs -> + | Trapping _, vs -> assert false - | Returning vs', vs -> + | Returning _, vs + | ReturningInvoke _, vs -> Crash.error e.at "undefined frame" - | Breaking (k, vs'), vs -> + | Breaking _, vs -> Crash.error e.at "undefined label" | Label (n, es0, (vs', [])), vs -> @@ -612,6 +629,9 @@ let rec step (c : config) : config = | Label (n, es0, (vs', {it = Returning vs0; at} :: es')), vs -> vs, [Returning vs0 @@ at] + | Label (n, es0, (vs', {it = ReturningInvoke (vs0, f); at} :: es')), vs -> + vs, [ReturningInvoke (vs0, f) @@ at] + | Label (n, es0, (vs', {it = Breaking (0l, vs0); at} :: es')), vs -> take n vs0 e.at @ vs, List.map plain es0 @@ -631,6 +651,10 @@ let rec step (c : config) : config = | Frame (n, frame', (vs', {it = Returning vs0; at} :: es')), vs -> take n vs0 e.at @ vs, [] + | Frame (n, frame', (vs', {it = ReturningInvoke (vs0, f); at} :: es')), vs -> + let FuncType (ins, out) = Func.type_of f in + take (Lib.List32.length ins) vs0 e.at @ vs, [Invoke f @@ at] + | Frame (n, frame', code'), vs -> let c' = step {frame = frame'; code = code'; budget = c.budget - 1} in vs, [Frame (n, c'.frame, c'.code) @@ e.at] diff --git a/interpreter/syntax/ast.ml b/interpreter/syntax/ast.ml index a6eb0d9cc1..42c2fb6736 100644 --- a/interpreter/syntax/ast.ml +++ b/interpreter/syntax/ast.ml @@ -149,6 +149,8 @@ and instr' = | Return (* break from function body *) | Call of var (* call function *) | CallIndirect of var * var (* call function through table *) + | ReturnCall of var (* tail-call function *) + | ReturnCallIndirect of var * var (* tail-call function through table *) | LocalGet of var (* read local variable *) | LocalSet of var (* write local variable *) | LocalTee of var (* write local variable and keep value *) diff --git a/interpreter/syntax/free.ml b/interpreter/syntax/free.ml index 8e1a37a458..dfddd764e6 100644 --- a/interpreter/syntax/free.ml +++ b/interpreter/syntax/free.ml @@ -75,8 +75,9 @@ let rec instr (e : instr) = | Br x | BrIf x -> labels (var x) | BrTable (xs, x) -> list (fun x -> labels (var x)) (x::xs) | Return -> empty - | Call x -> funcs (var x) - | CallIndirect (x, y) -> tables (var x) ++ types (var y) + | Call x | ReturnCall x -> funcs (var x) + | CallIndirect (x, y) | ReturnCallIndirect (x, y) -> + tables (var x) ++ types (var y) | LocalGet x | LocalSet x | LocalTee x -> locals (var x) | GlobalGet x | GlobalSet x -> globals (var x) | TableGet x | TableSet x | TableSize x | TableGrow x | TableFill x -> diff --git a/interpreter/syntax/operators.ml b/interpreter/syntax/operators.ml index 296bb1e77e..685eeafa96 100644 --- a/interpreter/syntax/operators.ml +++ b/interpreter/syntax/operators.ml @@ -27,6 +27,8 @@ let br_table xs x = BrTable (xs, x) let return = Return let call x = Call x let call_indirect x y = CallIndirect (x, y) +let return_call x = ReturnCall x +let return_call_indirect x y = ReturnCallIndirect (x, y) let local_get x = LocalGet x let local_set x = LocalSet x diff --git a/interpreter/text/arrange.ml b/interpreter/text/arrange.ml index dc56743eb6..1cd7b80636 100644 --- a/interpreter/text/arrange.ml +++ b/interpreter/text/arrange.ml @@ -453,6 +453,9 @@ let rec instr e = | Call x -> "call " ^ var x, [] | CallIndirect (x, y) -> "call_indirect " ^ var x, [Node ("type " ^ var y, [])] + | ReturnCall x -> "return_call " ^ var x, [] + | ReturnCallIndirect (x, y) -> + "return_call_indirect " ^ var x, [Node ("type " ^ var y, [])] | LocalGet x -> "local.get " ^ var x, [] | LocalSet x -> "local.set " ^ var x, [] | LocalTee x -> "local.tee " ^ var x, [] diff --git a/interpreter/text/lexer.mll b/interpreter/text/lexer.mll index d9a12b5d21..46b4e4ddf3 100644 --- a/interpreter/text/lexer.mll +++ b/interpreter/text/lexer.mll @@ -168,6 +168,8 @@ rule token = parse | "select" -> SELECT | "call" -> CALL | "call_indirect" -> CALL_INDIRECT + | "return_call" -> RETURN_CALL + | "return_call_indirect" -> RETURN_CALL_INDIRECT | "local.get" -> LOCAL_GET | "local.set" -> LOCAL_SET diff --git a/interpreter/text/parser.mly b/interpreter/text/parser.mly index e29be3ae3b..7129d0d3f3 100644 --- a/interpreter/text/parser.mly +++ b/interpreter/text/parser.mly @@ -212,7 +212,7 @@ let inline_type_explicit (c : context) x ft at = %token FUNCREF EXTERNREF EXTERN MUT %token UNREACHABLE NOP DROP SELECT %token BLOCK END IF THEN ELSE LOOP BR BR_IF BR_TABLE -%token CALL CALL_INDIRECT RETURN +%token CALL CALL_INDIRECT RETURN RETURN_CALL RETURN_CALL_INDIRECT %token LOCAL_GET LOCAL_SET LOCAL_TEE GLOBAL_GET GLOBAL_SET %token TABLE_GET TABLE_SET %token TABLE_SIZE TABLE_GROW TABLE_FILL TABLE_COPY TABLE_INIT ELEM_DROP @@ -388,6 +388,7 @@ plain_instr : br_table xs x } | RETURN { fun c -> return } | CALL var { fun c -> call ($2 c func) } + | RETURN_CALL var { fun c -> return_call ($2 c func) } | LOCAL_GET var { fun c -> local_get ($2 c local) } | LOCAL_SET var { fun c -> local_set ($2 c local) } | LOCAL_TEE var { fun c -> local_tee ($2 c local) } @@ -468,6 +469,14 @@ call_instr_instr_list : { let at1 = ati 1 in fun c -> let x, es = $2 c in (call_indirect (0l @@ at1) x @@ at1) :: es } + | RETURN_CALL_INDIRECT var call_instr_type_instr_list + { let at1 = ati 1 in + fun c -> let x, es = $3 c in + (return_call_indirect ($2 c table) x @@ at1) :: es } + | RETURN_CALL_INDIRECT call_instr_type_instr_list /* Sugar */ + { let at1 = ati 1 in + fun c -> let x, es = $2 c in + (return_call_indirect (0l @@ at1) x @@ at1) :: es } call_instr_type_instr_list : | type_use call_instr_params_instr_list @@ -547,6 +556,11 @@ expr1 : /* Sugar */ | CALL_INDIRECT call_expr_type /* Sugar */ { let at1 = ati 1 in fun c -> let x, es = $2 c in es, call_indirect (0l @@ at1) x } + | RETURN_CALL_INDIRECT var call_expr_type + { fun c -> let x, es = $3 c in es, return_call_indirect ($2 c table) x } + | RETURN_CALL_INDIRECT call_expr_type /* Sugar */ + { let at1 = ati 1 in + fun c -> let x, es = $2 c in es, return_call_indirect (0l @@ at1) x } | BLOCK labeling_opt block { fun c -> let c' = $2 c [] in let bt, es = $3 c' in [], block bt es } | LOOP labeling_opt block diff --git a/interpreter/valid/valid.ml b/interpreter/valid/valid.ml index cfe7f310f2..63a9e4d492 100644 --- a/interpreter/valid/valid.ml +++ b/interpreter/valid/valid.ml @@ -295,6 +295,17 @@ let rec check_instr (c : context) (e : instr) (s : infer_result_type) : op_type " but table has " ^ string_of_ref_type t); (ts1 @ [NumType I32Type]) --> ts2 + | ReturnCall x -> + let FuncType (ins, out) = func c x in + require (out = c.results) e.at "type mismatch in function result"; + ins -->... [] + + | ReturnCallIndirect (x, y) -> + let TableType (lim, t) = table c x in + let FuncType (ins, out) = type_ c y in + require (out = c.results) e.at "type mismatch in function result"; + (ins @ [NumType I32Type]) -->... [] + | LocalGet x -> [] --> [local c x] diff --git a/proposals/tail-call/Overview.md b/proposals/tail-call/Overview.md new file mode 100644 index 0000000000..b422654f04 --- /dev/null +++ b/proposals/tail-call/Overview.md @@ -0,0 +1,145 @@ +# Tail Call Extension + +## Introduction + +### Motivation + +* Currently, the Wasm design explicitly forbids tail call optimisations + +* Want support to enable + - the correct and efficient implementations of languages that require tail call elimination + - the compilation of control constructs that can be implemented with it (e.g., forms of coroutines, continuations) + - compilation and optimization techniques that require it (e.g., dynamic recompilation, tracing, CPS) + - other sorts of computation being expressed as Wasm functions, e.g., FSMs + + +### Semantics + +Conceptually, tail-calling a function unwinds the current call frame before performing the actual call. +This can be applied to any form of call, that is: + +* Caller and callee can differ +* Caller and callee type can differ +* Callee may be dynamic (e.g., `call_indirect`) + + +## Design Space + +### Instructions + +* Tail calls are performed via separate, explicit call instructions (existing call instructions explicitly disallow TCE) + +* The proposal thus introduces a tail version of every call instruction + +* An alternative scheme introducing a single instruction prefix applicable to every call instruction was considered but rejected by the CG + - WebAssembly will likely get a few more call instructions in the future, e.g., `call_ref` + - otoh, instruction prefixes as modifiers are not used anywhere else in Wasm + + +### Execution + +* Tail calls behave like a combination of `return` followed by a respective call + +* Hence they unwind the operand stack like `return` does + +* Only keeps the necessary call arguments + +* Tail calls to host functions cannot guarantee tail behaviour (outside the scope of the spec) + +* Tail calls across WebAssembly module boundaries *do* guarantee tail behavior + + +### Typing + +* Typing rule for tail call instruction is derived by their nature of merging call and return + +* Because tail calls transfer control and unwind the stack they are stack-polymorphic + +* Previously open question: should tail calls induce different function types? Possibilities: + 1. Distinguish tail-callees by type + 2. Distinguish tail-callers by type + 3. Both + 4. Neither + +* Considerations: + - Option 1 (and 3) allows different calling conventions for non-tail-callable functions, which may reduce constraints on ABIs. + - On the other hand, it creates a bifurcated function space, which can lead to difficulties e.g. when using function tables or other forms of dynamic indirection. + - Benefit of option 2 (and thus 3) unclear. + - Experimental validation revealed that there isn't a notable performance benefit to option 1 either. + +* CG resolution was to go with option 4 as the conceptually simplest. + + +## Examples + +A simple boring example of a tail-recursive factorial funciton. +``` +(func $fac (param $x i64) (result i64) + (return_call $fac-aux (get_local $x) (i64.const 1)) +) + +(func $fac-aux (param $x i64) (param $r i64) (result i64) + (if (i64.eqz (get_local $x)) + (then (return (get_local $r))) + (else + (return_call $fac-aux + (i64.sub (get_local $x) (i64.const 1)) + (i64.mul (get_local $x) (get_local $r)) + ) + ) + ) +) + +``` + + +## Spec Changes + +### Structure + +Add two instructions: + +* `return_call `, the tail-call version of `call` +* `return_call_indirect `, the tail-call version of `call_indirect` + +Other language extensions like [typed function references](https://github.com/WebAssembly/function-references/blob/master/proposals/function-references/Overview.md) that introduce new call instructions will also introduce tail versions of these new instructions. + + +### Validation + +Validation of the new instructions is simply a combination of the typing rules for `return` and those for basic calls (and thus is stack-polymorphic). + +* If `x` refers to a function of type \[t1\*\] -> \[t2\*\], + then the instruction `return_call x` has type \[t3\* t1\*\] -> \[t4\*\], + for any t3\* and t4\*, + provided that the current function has return type \[t2\*\]. + +* If `x` refers to a function type \[t1\*\] -> \[t2\*\], + then the instruction `return_call_indirect x` has type \[t3\* t1\* i32\] -> \[t4\*\], + for any t3\* and t4\*, + provided that the current function has return type \[t2\*\]. + +Note that caller's and callee's parameter types do not need to match. + + +### Execution + +Execution semantics of the new instructions would + +1. pop the call operands +2. clear and pop the topmost stack frame in the same way `return` does +3. push back the operands +4. delegate to the semantics of the respective plain call instructions + + +### Binary Format + +Use the reserved opcodes after existing call instructions, i.e.: + +* `return_call` is 0x12 +* `return_call_indirect` is 0x13 + + +### Text Format + +The text format is extended with two new instructions in the obvious manner. diff --git a/test/core/return_call.wast b/test/core/return_call.wast new file mode 100644 index 0000000000..2f91f4deac --- /dev/null +++ b/test/core/return_call.wast @@ -0,0 +1,202 @@ +;; Test `return_call` operator + +(module + ;; Auxiliary definitions + (func $const-i32 (result i32) (i32.const 0x132)) + (func $const-i64 (result i64) (i64.const 0x164)) + (func $const-f32 (result f32) (f32.const 0xf32)) + (func $const-f64 (result f64) (f64.const 0xf64)) + + (func $id-i32 (param i32) (result i32) (local.get 0)) + (func $id-i64 (param i64) (result i64) (local.get 0)) + (func $id-f32 (param f32) (result f32) (local.get 0)) + (func $id-f64 (param f64) (result f64) (local.get 0)) + + (func $f32-i32 (param f32 i32) (result i32) (local.get 1)) + (func $i32-i64 (param i32 i64) (result i64) (local.get 1)) + (func $f64-f32 (param f64 f32) (result f32) (local.get 1)) + (func $i64-f64 (param i64 f64) (result f64) (local.get 1)) + + ;; Typing + + (func (export "type-i32") (result i32) (return_call $const-i32)) + (func (export "type-i64") (result i64) (return_call $const-i64)) + (func (export "type-f32") (result f32) (return_call $const-f32)) + (func (export "type-f64") (result f64) (return_call $const-f64)) + + (func (export "type-first-i32") (result i32) (return_call $id-i32 (i32.const 32))) + (func (export "type-first-i64") (result i64) (return_call $id-i64 (i64.const 64))) + (func (export "type-first-f32") (result f32) (return_call $id-f32 (f32.const 1.32))) + (func (export "type-first-f64") (result f64) (return_call $id-f64 (f64.const 1.64))) + + (func (export "type-second-i32") (result i32) + (return_call $f32-i32 (f32.const 32.1) (i32.const 32)) + ) + (func (export "type-second-i64") (result i64) + (return_call $i32-i64 (i32.const 32) (i64.const 64)) + ) + (func (export "type-second-f32") (result f32) + (return_call $f64-f32 (f64.const 64) (f32.const 32)) + ) + (func (export "type-second-f64") (result f64) + (return_call $i64-f64 (i64.const 64) (f64.const 64.1)) + ) + + ;; Recursion + + (func $fac-acc (export "fac-acc") (param i64 i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 1)) + (else + (return_call $fac-acc + (i64.sub (local.get 0) (i64.const 1)) + (i64.mul (local.get 0) (local.get 1)) + ) + ) + ) + ) + + (func $count (export "count") (param i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 0)) + (else (return_call $count (i64.sub (local.get 0) (i64.const 1)))) + ) + ) + + (func $even (export "even") (param i64) (result i32) + (if (result i32) (i64.eqz (local.get 0)) + (then (i32.const 44)) + (else (return_call $odd (i64.sub (local.get 0) (i64.const 1)))) + ) + ) + (func $odd (export "odd") (param i64) (result i32) + (if (result i32) (i64.eqz (local.get 0)) + (then (i32.const 99)) + (else (return_call $even (i64.sub (local.get 0) (i64.const 1)))) + ) + ) +) + +(assert_return (invoke "type-i32") (i32.const 0x132)) +(assert_return (invoke "type-i64") (i64.const 0x164)) +(assert_return (invoke "type-f32") (f32.const 0xf32)) +(assert_return (invoke "type-f64") (f64.const 0xf64)) + +(assert_return (invoke "type-first-i32") (i32.const 32)) +(assert_return (invoke "type-first-i64") (i64.const 64)) +(assert_return (invoke "type-first-f32") (f32.const 1.32)) +(assert_return (invoke "type-first-f64") (f64.const 1.64)) + +(assert_return (invoke "type-second-i32") (i32.const 32)) +(assert_return (invoke "type-second-i64") (i64.const 64)) +(assert_return (invoke "type-second-f32") (f32.const 32)) +(assert_return (invoke "type-second-f64") (f64.const 64.1)) + +(assert_return (invoke "fac-acc" (i64.const 0) (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac-acc" (i64.const 1) (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac-acc" (i64.const 5) (i64.const 1)) (i64.const 120)) +(assert_return + (invoke "fac-acc" (i64.const 25) (i64.const 1)) + (i64.const 7034535277573963776) +) + +(assert_return (invoke "count" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "count" (i64.const 1000)) (i64.const 0)) +(assert_return (invoke "count" (i64.const 1_000_000)) (i64.const 0)) + +(assert_return (invoke "even" (i64.const 0)) (i32.const 44)) +(assert_return (invoke "even" (i64.const 1)) (i32.const 99)) +(assert_return (invoke "even" (i64.const 100)) (i32.const 44)) +(assert_return (invoke "even" (i64.const 77)) (i32.const 99)) +(assert_return (invoke "even" (i64.const 1_000_000)) (i32.const 44)) +(assert_return (invoke "even" (i64.const 1_000_001)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 0)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 1)) (i32.const 44)) +(assert_return (invoke "odd" (i64.const 200)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 77)) (i32.const 44)) +(assert_return (invoke "odd" (i64.const 1_000_000)) (i32.const 99)) +(assert_return (invoke "odd" (i64.const 999_999)) (i32.const 44)) + + +;; Invalid typing + +(assert_invalid + (module + (func $type-void-vs-num (result i32) (return_call 1) (i32.const 0)) + (func) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-num-vs-num (result i32) (return_call 1) (i32.const 0)) + (func (result i64) (i64.const 1)) + ) + "type mismatch" +) + +(assert_invalid + (module + (func $arity-0-vs-1 (return_call 1)) + (func (param i32)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $arity-0-vs-2 (return_call 1)) + (func (param f64 i32)) + ) + "type mismatch" +) + +(module + (func $arity-1-vs-0 (i32.const 1) (return_call 1)) + (func) +) + +(module + (func $arity-2-vs-0 (f64.const 2) (i32.const 1) (return_call 1)) + (func) +) + +(assert_invalid + (module + (func $type-first-void-vs-num (return_call 1 (nop) (i32.const 1))) + (func (param i32 i32)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-second-void-vs-num (return_call 1 (i32.const 1) (nop))) + (func (param i32 i32)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-first-num-vs-num (return_call 1 (f64.const 1) (i32.const 1))) + (func (param i32 f64)) + ) + "type mismatch" +) +(assert_invalid + (module + (func $type-second-num-vs-num (return_call 1 (i32.const 1) (f64.const 1))) + (func (param f64 i32)) + ) + "type mismatch" +) + + +;; Unbound function + +(assert_invalid + (module (func $unbound-func (return_call 1))) + "unknown function" +) +(assert_invalid + (module (func $large-func (return_call 1012321300))) + "unknown function" +) diff --git a/test/core/return_call_indirect.wast b/test/core/return_call_indirect.wast new file mode 100644 index 0000000000..acf0a72e06 --- /dev/null +++ b/test/core/return_call_indirect.wast @@ -0,0 +1,536 @@ +;; Test `return_call_indirect` operator + +(module + ;; Auxiliary definitions + (type $proc (func)) + (type $out-i32 (func (result i32))) + (type $out-i64 (func (result i64))) + (type $out-f32 (func (result f32))) + (type $out-f64 (func (result f64))) + (type $over-i32 (func (param i32) (result i32))) + (type $over-i64 (func (param i64) (result i64))) + (type $over-f32 (func (param f32) (result f32))) + (type $over-f64 (func (param f64) (result f64))) + (type $f32-i32 (func (param f32 i32) (result i32))) + (type $i32-i64 (func (param i32 i64) (result i64))) + (type $f64-f32 (func (param f64 f32) (result f32))) + (type $i64-f64 (func (param i64 f64) (result f64))) + (type $over-i32-duplicate (func (param i32) (result i32))) + (type $over-i64-duplicate (func (param i64) (result i64))) + (type $over-f32-duplicate (func (param f32) (result f32))) + (type $over-f64-duplicate (func (param f64) (result f64))) + + (func $const-i32 (type $out-i32) (i32.const 0x132)) + (func $const-i64 (type $out-i64) (i64.const 0x164)) + (func $const-f32 (type $out-f32) (f32.const 0xf32)) + (func $const-f64 (type $out-f64) (f64.const 0xf64)) + + (func $id-i32 (type $over-i32) (local.get 0)) + (func $id-i64 (type $over-i64) (local.get 0)) + (func $id-f32 (type $over-f32) (local.get 0)) + (func $id-f64 (type $over-f64) (local.get 0)) + + (func $i32-i64 (type $i32-i64) (local.get 1)) + (func $i64-f64 (type $i64-f64) (local.get 1)) + (func $f32-i32 (type $f32-i32) (local.get 1)) + (func $f64-f32 (type $f64-f32) (local.get 1)) + + (func $over-i32-duplicate (type $over-i32-duplicate) (local.get 0)) + (func $over-i64-duplicate (type $over-i64-duplicate) (local.get 0)) + (func $over-f32-duplicate (type $over-f32-duplicate) (local.get 0)) + (func $over-f64-duplicate (type $over-f64-duplicate) (local.get 0)) + + (table funcref + (elem + $const-i32 $const-i64 $const-f32 $const-f64 + $id-i32 $id-i64 $id-f32 $id-f64 + $f32-i32 $i32-i64 $f64-f32 $i64-f64 + $fac $fac-acc $even $odd + $over-i32-duplicate $over-i64-duplicate + $over-f32-duplicate $over-f64-duplicate + ) + ) + + ;; Syntax + + (func + (return_call_indirect (i32.const 0)) + (return_call_indirect (param i64) (i64.const 0) (i32.const 0)) + (return_call_indirect (param i64) (param) (param f64 i32 i64) + (i64.const 0) (f64.const 0) (i32.const 0) (i64.const 0) (i32.const 0) + ) + (return_call_indirect (result) (i32.const 0)) + ) + + (func (result i32) + (return_call_indirect (result i32) (i32.const 0)) + (return_call_indirect (result i32) (result) (i32.const 0)) + (return_call_indirect (param i64) (result i32) (i64.const 0) (i32.const 0)) + (return_call_indirect + (param) (param i64) (param) (param f64 i32 i64) (param) (param) + (result) (result i32) (result) (result) + (i64.const 0) (f64.const 0) (i32.const 0) (i64.const 0) (i32.const 0) + ) + ) + + (func (result i64) + (return_call_indirect (type $over-i64) (param i64) (result i64) + (i64.const 0) (i32.const 0) + ) + ) + + ;; Typing + + (func (export "type-i32") (result i32) + (return_call_indirect (type $out-i32) (i32.const 0)) + ) + (func (export "type-i64") (result i64) + (return_call_indirect (type $out-i64) (i32.const 1)) + ) + (func (export "type-f32") (result f32) + (return_call_indirect (type $out-f32) (i32.const 2)) + ) + (func (export "type-f64") (result f64) + (return_call_indirect (type $out-f64) (i32.const 3)) + ) + + (func (export "type-index") (result i64) + (return_call_indirect (type $over-i64) (i64.const 100) (i32.const 5)) + ) + + (func (export "type-first-i32") (result i32) + (return_call_indirect (type $over-i32) (i32.const 32) (i32.const 4)) + ) + (func (export "type-first-i64") (result i64) + (return_call_indirect (type $over-i64) (i64.const 64) (i32.const 5)) + ) + (func (export "type-first-f32") (result f32) + (return_call_indirect (type $over-f32) (f32.const 1.32) (i32.const 6)) + ) + (func (export "type-first-f64") (result f64) + (return_call_indirect (type $over-f64) (f64.const 1.64) (i32.const 7)) + ) + + (func (export "type-second-i32") (result i32) + (return_call_indirect (type $f32-i32) + (f32.const 32.1) (i32.const 32) (i32.const 8) + ) + ) + (func (export "type-second-i64") (result i64) + (return_call_indirect (type $i32-i64) + (i32.const 32) (i64.const 64) (i32.const 9) + ) + ) + (func (export "type-second-f32") (result f32) + (return_call_indirect (type $f64-f32) + (f64.const 64) (f32.const 32) (i32.const 10) + ) + ) + (func (export "type-second-f64") (result f64) + (return_call_indirect (type $i64-f64) + (i64.const 64) (f64.const 64.1) (i32.const 11) + ) + ) + + ;; Dispatch + + (func (export "dispatch") (param i32 i64) (result i64) + (return_call_indirect (type $over-i64) (local.get 1) (local.get 0)) + ) + + (func (export "dispatch-structural") (param i32) (result i64) + (return_call_indirect (type $over-i64-duplicate) + (i64.const 9) (local.get 0) + ) + ) + + ;; Multiple tables + + (table $tab2 funcref (elem $tab-f1)) + (table $tab3 funcref (elem $tab-f2)) + + (func $tab-f1 (result i32) (i32.const 0x133)) + (func $tab-f2 (result i32) (i32.const 0x134)) + + (func (export "call-tab") (param $i i32) (result i32) + (if (i32.eq (local.get $i) (i32.const 0)) + (then (return_call_indirect (type $out-i32) (i32.const 0))) + ) + (if (i32.eq (local.get $i) (i32.const 1)) + (then (return_call_indirect 1 (type $out-i32) (i32.const 0))) + ) + (if (i32.eq (local.get $i) (i32.const 2)) + (then (return_call_indirect $tab3 (type $out-i32) (i32.const 0))) + ) + (i32.const 0) + ) + + ;; Recursion + + (func $fac (export "fac") (type $over-i64) + (return_call_indirect (param i64 i64) (result i64) + (local.get 0) (i64.const 1) (i32.const 13) + ) + ) + + (func $fac-acc (param i64 i64) (result i64) + (if (result i64) (i64.eqz (local.get 0)) + (then (local.get 1)) + (else + (return_call_indirect (param i64 i64) (result i64) + (i64.sub (local.get 0) (i64.const 1)) + (i64.mul (local.get 0) (local.get 1)) + (i32.const 13) + ) + ) + ) + ) + + (func $even (export "even") (param i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then (i32.const 44)) + (else + (return_call_indirect (type $over-i32) + (i32.sub (local.get 0) (i32.const 1)) + (i32.const 15) + ) + ) + ) + ) + (func $odd (export "odd") (param i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then (i32.const 99)) + (else + (return_call_indirect (type $over-i32) + (i32.sub (local.get 0) (i32.const 1)) + (i32.const 14) + ) + ) + ) + ) +) + +(assert_return (invoke "type-i32") (i32.const 0x132)) +(assert_return (invoke "type-i64") (i64.const 0x164)) +(assert_return (invoke "type-f32") (f32.const 0xf32)) +(assert_return (invoke "type-f64") (f64.const 0xf64)) + +(assert_return (invoke "type-index") (i64.const 100)) + +(assert_return (invoke "type-first-i32") (i32.const 32)) +(assert_return (invoke "type-first-i64") (i64.const 64)) +(assert_return (invoke "type-first-f32") (f32.const 1.32)) +(assert_return (invoke "type-first-f64") (f64.const 1.64)) + +(assert_return (invoke "type-second-i32") (i32.const 32)) +(assert_return (invoke "type-second-i64") (i64.const 64)) +(assert_return (invoke "type-second-f32") (f32.const 32)) +(assert_return (invoke "type-second-f64") (f64.const 64.1)) + +(assert_return (invoke "dispatch" (i32.const 5) (i64.const 2)) (i64.const 2)) +(assert_return (invoke "dispatch" (i32.const 5) (i64.const 5)) (i64.const 5)) +(assert_return (invoke "dispatch" (i32.const 12) (i64.const 5)) (i64.const 120)) +(assert_return (invoke "dispatch" (i32.const 17) (i64.const 2)) (i64.const 2)) +(assert_trap (invoke "dispatch" (i32.const 0) (i64.const 2)) "indirect call type mismatch") +(assert_trap (invoke "dispatch" (i32.const 15) (i64.const 2)) "indirect call type mismatch") +(assert_trap (invoke "dispatch" (i32.const 20) (i64.const 2)) "undefined element") +(assert_trap (invoke "dispatch" (i32.const -1) (i64.const 2)) "undefined element") +(assert_trap (invoke "dispatch" (i32.const 1213432423) (i64.const 2)) "undefined element") + +(assert_return (invoke "dispatch-structural" (i32.const 5)) (i64.const 9)) +(assert_return (invoke "dispatch-structural" (i32.const 5)) (i64.const 9)) +(assert_return (invoke "dispatch-structural" (i32.const 12)) (i64.const 362880)) +(assert_return (invoke "dispatch-structural" (i32.const 17)) (i64.const 9)) +(assert_trap (invoke "dispatch-structural" (i32.const 11)) "indirect call type mismatch") +(assert_trap (invoke "dispatch-structural" (i32.const 16)) "indirect call type mismatch") + +(assert_return (invoke "call-tab" (i32.const 0)) (i32.const 0x132)) +(assert_return (invoke "call-tab" (i32.const 1)) (i32.const 0x133)) +(assert_return (invoke "call-tab" (i32.const 2)) (i32.const 0x134)) + +(assert_return (invoke "fac" (i64.const 0)) (i64.const 1)) +(assert_return (invoke "fac" (i64.const 1)) (i64.const 1)) +(assert_return (invoke "fac" (i64.const 5)) (i64.const 120)) +(assert_return (invoke "fac" (i64.const 25)) (i64.const 7034535277573963776)) + +(assert_return (invoke "even" (i32.const 0)) (i32.const 44)) +(assert_return (invoke "even" (i32.const 1)) (i32.const 99)) +(assert_return (invoke "even" (i32.const 100)) (i32.const 44)) +(assert_return (invoke "even" (i32.const 77)) (i32.const 99)) +(assert_return (invoke "even" (i32.const 100_000)) (i32.const 44)) +(assert_return (invoke "even" (i32.const 111_111)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 0)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 1)) (i32.const 44)) +(assert_return (invoke "odd" (i32.const 200)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 77)) (i32.const 44)) +(assert_return (invoke "odd" (i32.const 200_002)) (i32.const 99)) +(assert_return (invoke "odd" (i32.const 300_003)) (i32.const 44)) + + +;; Invalid syntax + +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (result i32) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (param i32) (type $sig) (result i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (param i32) (result i32) (type $sig)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (result i32) (type $sig) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (result i32) (param i32) (type $sig)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (result i32) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "unexpected token" +) + +(assert_malformed + (module quote + "(table 0 funcref)" + "(func (return_call_indirect (param $x i32) (i32.const 0) (i32.const 0)))" + ) + "unexpected token" +) +(assert_malformed + (module quote + "(type $sig (func))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (result i32) (i32.const 0))" + ")" + ) + "inline function type" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (result i32) (i32.const 0))" + ")" + ) + "inline function type" +) +(assert_malformed + (module quote + "(type $sig (func (param i32) (result i32)))" + "(table 0 funcref)" + "(func" + " (return_call_indirect (type $sig) (param i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "inline function type" +) +(assert_malformed + (module quote + "(type $sig (func (param i32 i32) (result i32)))" + "(table 0 funcref)" + "(func (result i32)" + " (return_call_indirect (type $sig) (param i32) (result i32)" + " (i32.const 0) (i32.const 0)" + " )" + ")" + ) + "inline function type" +) + +;; Invalid typing + +(assert_invalid + (module + (type (func)) + (func $no-table (return_call_indirect (type 0) (i32.const 0))) + ) + "unknown table" +) + +(assert_invalid + (module + (type (func)) + (table 0 funcref) + (func $type-void-vs-num (i32.eqz (return_call_indirect (type 0) (i32.const 0)))) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (result i64))) + (table 0 funcref) + (func $type-num-vs-num (i32.eqz (return_call_indirect (type 0) (i32.const 0)))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type (func (param i32))) + (table 0 funcref) + (func $arity-0-vs-1 (return_call_indirect (type 0) (i32.const 0))) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param f64 i32))) + (table 0 funcref) + (func $arity-0-vs-2 (return_call_indirect (type 0) (i32.const 0))) + ) + "type mismatch" +) + +(module + (type (func)) + (table 0 funcref) + (func $arity-1-vs-0 (return_call_indirect (type 0) (i32.const 1) (i32.const 0))) +) + +(module + (type (func)) + (table 0 funcref) + (func $arity-2-vs-0 + (return_call_indirect (type 0) (f64.const 2) (i32.const 1) (i32.const 0)) + ) +) + +(assert_invalid + (module + (type (func (param i32))) + (table 0 funcref) + (func $type-func-void-vs-i32 (return_call_indirect (type 0) (i32.const 1) (nop))) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param i32))) + (table 0 funcref) + (func $type-func-num-vs-i32 (return_call_indirect (type 0) (i32.const 0) (i64.const 1))) + ) + "type mismatch" +) + +(assert_invalid + (module + (type (func (param i32 i32))) + (table 0 funcref) + (func $type-first-void-vs-num + (return_call_indirect (type 0) (nop) (i32.const 1) (i32.const 0)) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param i32 i32))) + (table 0 funcref) + (func $type-second-void-vs-num + (return_call_indirect (type 0) (i32.const 1) (nop) (i32.const 0)) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param i32 f64))) + (table 0 funcref) + (func $type-first-num-vs-num + (return_call_indirect (type 0) (f64.const 1) (i32.const 1) (i32.const 0)) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type (func (param f64 i32))) + (table 0 funcref) + (func $type-second-num-vs-num + (return_call_indirect (type 0) (i32.const 1) (f64.const 1) (i32.const 0)) + ) + ) + "type mismatch" +) + + +;; Unbound type + +(assert_invalid + (module + (table 0 funcref) + (func $unbound-type (return_call_indirect (type 1) (i32.const 0))) + ) + "unknown type" +) +(assert_invalid + (module + (table 0 funcref) + (func $large-type (return_call_indirect (type 1012321300) (i32.const 0))) + ) + "unknown type" +) + + +;; Unbound function in table + +(assert_invalid + (module (table funcref (elem 0 0))) + "unknown function 0" +)