Open binji opened 4 years ago
Great idea! I think this opens up a new option that we haven't considered before: what if, instead of having named parameters that select which version of a nested module to use, we instead had:
So, as a strawman, a tool could generate:
(module
(optional module $SIMD ...)
(optional instance $simd (instantiate $SIMD))
(module $SCALAR ...)
(instance $scalar (instantiate $SCALAR))
(module $CORE
(import "simd" (optional instance $simd ...))
(import "scalar" (instance $scalar ...))
(func $work
if (optional.test $simd)
call $simd.work
else
call $scalar.work
end
)
(instance $core (instantiate $CORE (instance $simd) (instance $scalar)))
(export $core) ;; zero-level export replaces outer modules exports with $core
)
I think this simultaneously addresses two competing concerns mentioned in #20:
WebAssembly.validate()
to core wasm, allowing non-JS hosts to do the same "feature detection by validation tests" technique as we initially proposed for JS hosts.Even better: WASI has a strong use case for needing some form of optional imports. If the above mechanism existed, I think WASI could just use that, making WASI less magic and more like a plain library API, which is a general goal.
One downside with the above strawman is that we're always compiling the fallback code. For SIMD use cases, the amount of memory/compile-time wasted is probably insignificant. For more cross-cutting features, the waste could be significant. One option I haven't thought through is to allow the presence of one optional module ($SIMD
) to disable another module ($SCALAR
). Maybe we even allow more-general boolean expressions :) The key is that the "variables" in these expressions now are exclusively the results of module validation.
Thoughts?
One of the main worries about conditional sections is that if the section isn't understood it causes indices to get renumbered, but I think nested modules would have a similar issue? If an optional module/instance were not understood by the engine, it may not necessarily understand how many functions/types/etc were in that module that it didn't include. Given that, how would call $scalar.work
get encoded because presumably the index there needs to account for the number of functions in the optional $simd
instance @lukewagner listed above?
Great question! In the ...
of the import
s, $CORE
needs to declare the imported instances' types, and the index spaces' contents' are entirely determined by these types, which are valid even if the optional modules' bodies aren't. Strong modularity FTW!
Aha right! The interface from a non-simd module into a SIMD module would have to avoid using SIMD types, which means that an inner module using SIMD would always have a valid module type according to the core spec, it's just that the implementation internals wouldn't validate if an engine didn't implement SIMD.
Given all that to me optional nested modules/instances sounds like a fantastic way to solve the issues that have come up with conditional sections.
a new form of nested module that says: "here's a nested module that might not validate; if it fails, don't fail the outer module's validation, just mark this one module as "borked"
This is actually similar to the way I originally sketched conditional sections working (see https://github.com/WebAssembly/design/issues/1280)! The big difference is that I originally thought we'd use the embedded module as a "feature test" module. But allowing nested modules makes this a cleaner solution, since as you say it can be the real module being verified.
I'm not sure I like optional.test
being used here instead of linking this in statically. Is this required for WASI? Or can we push this out to something declarative instead?
I have a more general question too, about using module-linking at all: Luke, in your example, you show a $CORE
module importing $SIMD
and $SCALAR
. But realistically, we'll probably want both of those modules to share a linear memory. To do so, would we need to split this out into a 4th module? e.g. $BASE
defines and exports a memory, which $SIMD
, $SCALAR
, and $CORE
import, and then (in all likelihood) $CORE
exports as well. It seems like this will work fine, but it does feel a bit circuitous to me.
Is it possible to define the memory in the outer module, then import that memory in nested modules? I would think not, since the outer module hasn't been instantiated yet. But it seems like a common use case.
Having conditional contents constrained to match a shared module type would be great. I agree with @binji that it would be better to have a static mechanism for resolving the conditions and linking the modules, though. It could be as simple as a sequence of modules, where the first one that validates is the one that is chosen. If no module in the sequence validates, the result would be a validation error.
@binji Hah, right, I had forgotten about that; good idea :)
For memory sharing: good point; I think it can be done by having a tiny utility module that only contains and exports memory. Or if the optional module is hefty, you might want a full libc
(so the hefty module can malloc/free). Then you can wire it all up in the style of shared-everything linking example.
It's possible I was overly-excited to match this with the optional imports use case. However, under the assumption that optional.test
could be optimized as a constant value (allowing branch elimination), I think optional imports give the toolchain some potentially-useful generalizations of a conditional selection of nested modules:
using inline code when a feature is missing (e.g., avoiding the $SCALAR
module call):
if (optional.test $fancy-simd)
call $fancy-simd.$routine
else
do it inline
end
having multiple nested modules that don't all implement exactly the same interface:
if (optional.test $fancy-simd-algo-1)
(call $fancy-simd-algo-1.$run (x) (y) (z))
else if (optional.test $fancy-simd-algo-2)
(call $fancy-simd-algo-2.$run (x) (y))
else
call $ya-basic
end
having multiple module with partially-overlapping sets of functionality so that you don't have a strict either-or relationship
@tlively and I briefly discussed this over coffee the other day. What if we layered this proposal on top of module-types (with linking as described in https://github.com/WebAssembly/module-types/pull/3), and used nested modules here instead of conditional sections?
At a first glance, there are some nice properties here. We can fix the unstable index problem (issues #21 and #10), because the outer module will always have a fixed module type. Any helper functions/types/etc. would be local to the nested module, and the outer module could then conditionally export whichever version it prefers.
It addresses module merging issues (issue #14), since again, the outer module can choose which inner components to export/define, and can be bound by whichever constraints we like (single memory, single start section, etc.)
There are no concerns about the name section (issue #16), since the names will be attached to the whichever module defines the function/global/etc.
I'm still not quite sure how the conditional nested imports will work, though. My initial thought is that this would be a switch, where a particular import comes from 1 of N modules choosing the first predicate that is satisfied. If the predicate is never satisfied, then the module is invalid. Similarly, module definitions would be conditioned on a single predicate.
Thoughts?