Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is a WIP branch adding some multiple-value expression support to the interpreter. It uses bottom up typing and demonstrates that even this single pass compiler can get enough type information to generate useful output, and that adding type information to blocks is not necessary. The code is rough, far from optimized, but this seems to be enough to demonstration the type system and the ability to handle these operations which is the main goal.
The extensions:
values
operator constructs a multiple value expression from its arguments and thevalues
type subsumes the currentvoid
and single values types:(values) == (nop)
(values (i32.const 1)) == (i32.const 1))
. A single value is consumed from each argument and the rest discarded:(values (values (i32.const 1) (i32.const 2)) (values (i32.const 3) (i32.const 4)) == (values (i32.const 1) (i32.const 3))
conc_values
operator constructs a multiple value expression from the concatenation of all the values of its arguments. For example(conc_values (i32.const1) (nop) (values (i32.const 2) (i32.const 3))) == (values (i32.const1) (i32.const 2) (i32.const 3))
Valid code must have a fixed number of values for each argument to ensure that the number of consumed values for each expression is static, for example the follow is invalid:(conc_values (if (i32.const 1) (nop) (i32.const 1)))
. This operator would challenge top-down typing which might require encoding the number of values consumed from each argument.mv_call
operator is similar to thecall
operator but accepts a single expression argument and passes all the values to the callee. For example(mv_call $f (values (i32.const1) (i32.const 2))) == (call $f (i32.const1) (i32.const 2)))
. This could be generalized to accept multiple arguments and concatenate all the values likeconc_values
but this can done with(mv_call $f (conc_values ...))
so would just be an extra convenience.block/break
if
select
, the type system accepts conflicting value types and missing values so long as they are not consumed. This makes it convenient and efficient to discard unused values which is a nice property in a language in which all operators are expressions. In the limit this gives the current behaviour for the single values types and the void/zero-value type, where single values can be discarded when not used. For example(i32.add (call $fn_returning_i32_f32) (call $fn_returning_i32))
is valid and(i32.add (call $fn_returning_i32_f32) (call $fn_returning_i32_f64))
is valid, and in both cases only the first value is consumed.br
br_if
return
and potentially thebr_table
operators pass on all the values of their single expression. The text syntax accepts a missing expression which is encoded as anop
:(br $l) == (br $l (nop)) == (br $l (values))
. For example(values (i32.const 1) (i32.const 2)) == (block (values (i32.const 1) (i32.const 2))) == (block $l (br $l (values (i32.const 1) (i32.const 2))))
(func $f1 (result i32 i64) (values (i32.const 1) (i64.const 2)))
for which(call $f1) => (values (i32.const 1) (i64.const 2))
. The interpreter supports returning multiple values for exported functions.block1
operator returns the values of the first expression and discards the values of all other block top level expressions, in contrast to theblock
operator that returns the values of the last expression. Combined withblock
this allows returning the values of an arbitrary expression in an effective block. For example(block (exp1) (block1 (exp2) (exp3)))
returns the values of(exp2)
.The type system:
empty
andvalues
. Theempty
type is the set of no types and represents the type of unreachable code. Thevalues
type is a sequence of value types,i32
,i64
,f32
,f64
, plus theunion
type which represents any ofi32
,i64
,f32
, orf64
, plus anoptional
type which also includes the value being missing.empty
type is itself. The union ofvalues
types is computed per element. If either value is theoptional
type or missing then the result type isoptional
, otherwise if the types differ the result type isunion
, otherwise the types are the same and this is the result type for the element. Note that this computation is an expansion of the set of result value types, not the most specific, but wasm only validly consumes values elements with a matching type, or expressions with a fixed number of values, so the union operators can be simplified to a flat sequence. For example, the precise union applied to(if (cond) (values (i32.const 1) (f32.const 2)) (values (f32.const 3) (i32.const 4)))
is(or (values i32 f32) (values f32 i32))
, but it is sufficient for wasm to expand this set to(values union union) == (or (values i32 i32) (values i32 i64) (values i32 f32) (values i32 f64) (values i64 i32) ...)
.empty
type or havevalues
type elements of typeunion
oroptional
. (Todo an expected element type for non-consumed elements). If the actual type is theempty
type then this test is always true for any expected type as the empty set is a subset of all sets. If the actual and expected types arevalues
types then all the consumed expected element types must match their respective element in the actual type, and other elements in the actual type are ignored.