Skip to content

Remove has_feature#523

Merged
titzer merged 1 commit intomasterfrom
remove_has_feature
Feb 25, 2016
Merged

Remove has_feature#523
titzer merged 1 commit intomasterfrom
remove_has_feature

Conversation

@titzer
Copy link

@titzer titzer commented Jan 21, 2016

Current consensus is to remove has_feature as an operator. This PR removes it from AstSemantics.md.

@lukewagner
Copy link
Member

lgtm. To expand: the consensus was that we don't yet have the experience writing polyfills to know whether has_feature is the right primitive building block so we should hold off on defining it (or something else) until we do. In the interim, it's possible to do a crude feature test (as people do in JS) by just evaling some wasm code and catching validation errors.

@kg
Copy link
Contributor

kg commented Jan 21, 2016

Prior discussions (#416 for example, there are others) have raised reasons to not remove has_feature and as far as I know there isn't a recorded consensus to the effect that those problems are resolved. We need to document our reasoning here if we're going to strip it.

@mtrofin
Copy link
Contributor

mtrofin commented Jan 21, 2016

The plan was to get the experience @lukewagner is mentioning, before moving forward with a decision about the feature, for MVP. We collected a list of scenarios back in October. We agreed that, once we have that polyfil experience, but before MVP, to take a critical look at that scenario list and make a better informed decision about a design to address them.

So we thought it was important to have an answer, for MVP, to the problem has_feature was trying to solve, but we were not sure what the final design would be.

Is this still the plan?

@lukewagner
Copy link
Member

From my perspective, yes.

@titzer
Copy link
Author

titzer commented Jan 23, 2016

Yes, that is also my understanding, and that's how we had converged on
simply removing it.

On Thu, Jan 21, 2016 at 10:55 PM, Luke Wagner notifications@github.com
wrote:

From my perspective, yes.


Reply to this email directly or view it on GitHub
#523 (comment).

@kg
Copy link
Contributor

kg commented Jan 24, 2016

If we agree that we need it before finalizing the MVP, and we already have a list of known use cases, why are we removing what we have when it's an extremely trivial opcode?

There's a ton of existing software out there using a similar feature-detection model, some of it actively shipping in browsers right now. This is not a high-risk or controversial mechanism. I'm all for us replacing has_feature with something better later but I don't see any good reason to burn energy removing what we have. If we're eager to strip unnecessary complexity from wasm I can think of some better places to start :-)

How will we figure out what the right form of feature testing is if we don't implement any form of it to try out? We've already examined the 'just do a crude eval' alternative a couple times in the past, and it's trivial to come up with real-world scenarios where 'eval a test module' is not just awkward but actively insufficient.

@titzer
Copy link
Author

titzer commented Jan 24, 2016

On Sun, Jan 24, 2016 at 7:28 AM, Katelyn Gadd notifications@github.com
wrote:

If we agree that we need it before finalizing the MVP, and we already have
a list of known use cases, why are we removing what we have when it's an
extremely trivial opcode?

There's a ton of existing software out there using a similar
feature-detection model, some of it actively shipping in browsers right
now. This is not a high-risk or controversial mechanism. I'm all for us
replacing has_feature with something better later but I don't see any
good reason to burn energy removing what we have. If we're eager to strip
unnecessary complexity from wasm I can think of some better places to start
:-)

How will we figure out what the right form of feature testing is if we
don't implement any form of it to try out? We've already examined the 'just
do a crude eval' alternative a couple times in the past, and it's trivial
to come up with real-world scenarios where 'eval a test module' is not just
awkward but actively insufficient.

Unfortunately has_feature is neither fully designed, fully spec'd, nor
fully implemented, and there currently aren't any use cases. This is how
the consensus to remove it formed.


Reply to this email directly or view it on GitHub
#523 (comment).

@kg
Copy link
Contributor

kg commented Jan 24, 2016

Unfortunately has_feature is neither fully designed, fully spec'd, nor
fully implemented, and there currently aren't any use cases. This is how
the consensus to remove it formed.

It's been in AstSemantics.md forever. The design seems more than sufficient to accompany the spec, which has contained this feature for months and months:
https://github.com/WebAssembly/spec/blob/master/ml-proto/host/params.ml#L1
It has been functioning and it has test coverage.
There is a list of scenarios that @mtrofin linked to just above (https://github.com/WebAssembly/design/blob/master/Rationale.md#motivating-scenarios-for-feature-testing).

Any lack of implementation re: has_feature is the result of people opting not to implement it. If this is based on some unforeseen difficulties that make it hard to implement, or if there are specific things wrong with the design of the feature, we should address that explicitly. If those things provide a specific rationale to remove the feature, we should add that rationale to Rationale.md when removing the feature.

Furthermore, while I can agree with the premise stated above:

we don't yet have the experience writing polyfills to know whether has_feature is the right primitive building block so we should hold off on defining it (or something else) until we do

acting based on this premise requires at least a small amount of rigor here. If our new consensus is to pull out what we have (despite it being designed and specced for months) in order to learn about the problem, let's get serious:

  • What are the things we want to learn?
  • How are we going to learn them?
  • How will those lessons inform what we're actually going to do?

At present the plan amounts to 'we'll figure it out later', which is totally fine in most cases but is less than totally fine when it's being used as the justification to remove a tiny opcode that is already designed and already specced.

@ghost
Copy link

ghost commented Jan 24, 2016

@kg Personally I don't think it was well designed.

It might not meet the security needs of the web to be giving the wasm code access to feature tests at runtime as it would give the wasm code a good fingerprint of the runtime. We might want to explore only giving access to the features information to the layer-1 code, so that it can rewrite the layer-0 code, etc. After some experience, the layer-1 might be standardized so that even the JS can not access these features. I only briefly touched on such issues with one member - it needs a lot more thought. Also I have a use case needing the memory size, and some negotiation, that can not be handled by such a simple feature test alone, but perhaps I could explore it at layer-1.

I recall an issue about skipping unknown code, and this is on my mind, but I want to deal with it by adding a data layer that even future code is written in, so that adding more operators can be done by just adding them to the opcode table which will declare the number of arguments, and immediate arguments and optional arguments, and perhaps even their type for checking. With this the runtime has enough information to be able to at least skip unknown operators. This is also needed for view-source, which I presume we don't want to depend on knowledge or particular features. This strategy has limits, but might deal with many feature additions, requiring much less frequent version changes.

@kg
Copy link
Contributor

kg commented Jan 24, 2016

@kg Personally I don't think it was well designed.

This is a valid point of view, but not if it isn't justified.

It might not meet the security needs of the web to be giving the wasm code access to feature tests at runtime as it would give the wasm code a good fingerprint of the runtime.

Runtime traits will largely be a result of which version of which browser you have installed. You can already detect browser name & version through various methods.

We might want to explore only giving access to the features information to the layer-1 code, so that it can rewrite the layer-0 code, etc. After some experience, the layer-1 might be standardized so that even the JS can not access these features.

Any feature test based rewriting would allow the target code to identify which features are available. You'd just be making it more awkward.

I only briefly touched on such issues with one member - it needs a lot more thought. Also I have a use case needing the memory size, and some negotiation, that can not be handled by such a simple feature test alone, but perhaps I could explore it at layer-1.

Features aren't just method calls. Most things can be addressed by has_feature; things that can't would be addressed by a specific mechanism that you first feature detect before using it.

I recall an issue about skipping unknown code, and this is on my mind, but I want to deal with it by adding a data layer that even future code is written in, so that adding more operators can be done by just adding them to the opcode table which will declare the number of arguments, and immediate arguments and optional arguments, and perhaps even their type for checking.

There's been some debate over skipping unknown code and related topics. has_feature doesn't actually fit into that (though maybe you could come up with a way to leverage it).
The opcode table we discussed in the past already worked that way. Without it, we'd have to version the format to introduce new opcodes (which almost everyone wants to avoid). We just don't have the opcode table in v8-native/sexpr-wasm yet.

With this the runtime has enough information to be able to at least skip unknown operators. This is also needed for view-source, which I presume we don't want to depend on knowledge or particular features. This strategy has limits, but might deal with many feature additions, requiring much less frequent version changes.

Right. And to restate, we have not made any plans to version the format. Whenever it came up so far multiple CG members expressed a desire to never version it. In a versionless model feature detection and an opcode table are much more important.

@ghost
Copy link

ghost commented Jan 24, 2016

It might not meet the security needs of the web to be giving the
wasm code access to feature tests at runtime as it would give the
wasm code a good fingerprint of the runtime.

Runtime traits will largely be a result of which version of which
browser you have installed. You can already detect browser name &
version through various methods.

Yes, that was basically the feedback I received. But I wanted to find time to understand the issue.

We might want to explore only giving access to the features
information to the layer-1 code, so that it can rewrite the layer-0
code, etc. After some experience, the layer-1 might be standardized
so that even the JS can not access these features.

Any feature test based rewriting would allow the target code to identify
which features are available. You'd just be making it more awkward.

This was also the feedback I received, but it is not immediately clear to me. Just asking for time, and supporting revisiting this later in this regard.

Lets assume we have an encoding that allows new operators and new types to be declared, so no new versions are needed. Then it become a matter of a runtime supporting these, not of the runtime supporting the binary format?

So the runtime can either fail to validate code that uses any unknown operator or type, or it can signal an error at runtime if such code is executed? For has_feature to be usable it must be the later? Whereas if it is handled at layer-1 then it could be the former.

has_feature might be ok for coarse grain choices but for a use case of interest to me the decision would affect the pattern of every memory access which is something that seems much better suited to handle at layer-1.

I am really just supporting re-visiting this later, so we can focus on getting something working. It does not bother me if it's removed now or not and it does not seem to be blocking anything.

@kg
Copy link
Contributor

kg commented Jan 25, 2016

Lets assume we have an encoding that allows new operators and new types to be declared, so no new versions are needed. Then it become a matter of a runtime supporting these, not of the runtime supporting the binary format?

Supporting new operators and types is something you do both at the AST level and the encoding level. The encoding needs to know how to represent new types and operators (by shape, essentially) while the AST level needs to know the semantics so the code using new types/operators can actually be executed.

However, operators and types aren't the only things you can add, and they're not the only things you could need to feature-detect.

So the runtime can either fail to validate code that uses any unknown operator or type, or it can signal an error at runtime if such code is executed? For has_feature to be usable it must be the later? Whereas if it is handled at layer-1 then it could be the former.

has_feature isn't just about being able to detect whether a given opcode is available. Though you'd probably want to use it for that. Most 'opcode available' use cases would be addressed by the opcode table from past design discussions, where you forward-declare the opcodes you use (and can provide optional polyfills for some of them).

has_feature might be ok for coarse grain choices but for a use case of interest to me the decision would affect the pattern of every memory access which is something that seems much better suited to handle at layer-1.

I don't understand what you mean here. Can you clarify? Runtime behavior of the application has nothing to do with the binary encoding, regardless of layer.

I am really just supporting re-visiting this later, so we can focus on getting something working. It does not bother me if it's removed now or not and it does not seem to be blocking anything.

Revisiting later is fine, but we need to have an actual plan for what we're going to revisit and how.

@ghost
Copy link

ghost commented Jan 25, 2016

Lets assume we have an encoding that allows new operators and new
types to be declared, so no new versions are needed. Then it become
a matter of a runtime supporting these, not of the runtime
supporting the binary format?

Supporting new operators and types is something you do both at the AST
level and the encoding level. The encoding needs to know how to
represent new types and operators (by shape, essentially) while the AST
level needs to know the semantics so the code using new types/operators
can actually be executed.

If new operators and types need extensions to the encoding (matters that can not be accommodated in the operator encoding table) then surely an encoding revision would be needed - something you have already suggested is not practical.

For example, if someone wants to add a 128 bit SIMD immediate type then we need to either plan for this in the MVP encoding or they will need to use the existing 64/32/8 bit immediate encodings and repeat them, otherwise an encoding revision will be needed because the binary will not be readable by older runtimes. This could be caught at layer-1 with a feature test and emitting an encoding compatible with the runtime - not by a runtime has_feature.

However, operators and types aren't the only things you can add, and
they're not the only things you could need to feature-detect.

Yes, immediate values. There are some operators that currently need special handling, but there are only a few and rest can be declared in a table. What else can you think of? We can explore this now working on abstracting the encoding.

So the runtime can either fail to validate code that uses any
unknown operator or type, or it can signal an error at runtime if
such code is executed? For has_feature to be usable it must be the
later? Whereas if it is handled at layer-1 then it could be the former.

|has_feature| isn't just about being able to detect whether a given
opcode is available. Though you'd probably want to use it for that. Most
'opcode available' use cases would be addressed by the opcode table from
past design discussions, where you forward-declare the opcodes you use
(and can provide optional polyfills for some of them).

The opcode table will not address the 'opcode available' matter, it just declares their layout and encoding. Only the runtime knows if they are supported.

If a module does not validate when unknown operators or types are used then has_feature is of no help because it will never run!

has_feature might be ok for coarse grain choices but for a use case
of interest to me the decision would affect the pattern of every
memory access which is something that seems much better suited to
handle at layer-1.

I don't understand what you mean here. Can you clarify? Runtime behavior
of the application has nothing to do with the binary encoding,
regardless of layer.

For example, the layer-1 code negotiates an initial memory size and if memory growth is supported, and then chooses to use index masking on each memory access versus omit the masking, and this would change every memory access - very fine grain code changes. has_feature just does not handle this well. The need for this might depend on other performance issues and solutions that we have not even got around to exploring.

What can has_feature do that can not already be done by a runtime call to an imported function for coarse grain decisions?

Perhaps yielding a static result that would allow the compiler to bake in the choice. But again that does not help if the decision can change from one instance to the next, and this has implications for caching and perhaps security. If supporting baking in choices then I would like to be able to bake in constants too - wasm does not even have the const support available in asm.js where I can use this to bake in a mask of zero to disable masking or to give a static mask to enable it. But again perhaps this is better handled at layer-1 to reduce the burden on the runtime compiler to bake in constants (including has_feature) and fold them and optimize away dead code etc. My experience trying to get useful support for this across JS implementation was a failure.

The whole matter of the higher architecture needs to be re-visited. I support revisiting it later and focussing on getting the lower layers working. has_feature is a dead end from my perspective - happy to see it go.

@kg
Copy link
Contributor

kg commented Jan 25, 2016

If new operators and types need extensions to the encoding (matters that can not be accommodated in the operator encoding table) then surely an encoding revision would be needed - something you have already suggested is not practical.
For example, if someone wants to add a 128 bit SIMD immediate type then we need to either plan for this in the MVP encoding or they will need to use the existing 64/32/8 bit immediate encodings and repeat them, otherwise an encoding revision will be needed because the binary will not be readable by older runtimes. This could be caught at layer-1 with a feature test and emitting an encoding compatible with the runtime - not by a runtime has_feature.
Yes, immediate values. There are some operators that currently need special handling, but there are only a few and rest can be declared in a table. What else can you think of? We can explore this now working on abstracting the encoding.

You don't need to extend the encoding, the encoding just needs to be able to accommodate new opcodes and value types (by design, from the beginning). For example, a simd128 immediate is basically just a const.simd128 opcode followed by 16 bytes. The opcode table doesn't need to specify how to interpret those bytes, just the size of the immediate. That allows a correct decoding of the module and allows tools to pass it through and round-trip it. Of course, to execute code that uses the opcode, you need to implement the semantics.

Abstract decoding like this is also why it would be possible to put small polyfill functions into the opcode table. Many new opcodes can be implemented (with a performance penalty) by raw wasm that receives the opcode's immediates/operands and processes them. It would be up to a compiler or toolchain to generate the polyfills and put them in the opcode table.

There are specific types of features that probably can't be added this way, but I can't think of any off-hand.

So the runtime can either fail to validate code that uses any
unknown operator or type, or it can signal an error at runtime if
such code is executed? For has_feature to be usable it must be the
later? Whereas if it is handled at layer-1 then it could be the former.
The opcode table will not address the 'opcode available' matter, it just declares their layout and encoding. Only the runtime knows if they are supported.
If a module does not validate when unknown operators or types are used then has_feature is of no help because it will never run!

has_feature doesn't need to do everything to be useful. It's part of a bigger picture that allows developers to ship wasm applications that will work on older runtimes, and allows us to introduce new features into wasm runtimes. There are a few high level use cases to think about:

  • I want to determine what features are supported before loading a module, so that I can select a special version of it for that feature set.
    • This requires has_feature, but you could do it outside of wasm as long as you have access to the feature test primitive. In the browser, you could do this using JS. Non-browser hosts make this trickier, which is why I think it's important for it to be accessible from pure wasm applications.
  • I want to determine what features are supported to decide how to behave in a certain scenario
    • We never reached an agreement on this in design discussions, but a wasm implementation could theoretically delay 'are opcodes supported' checks until the first use of a given function. In that case, has_feature would let you pick an appropriate function at runtime, as with cpuid on x86.
    • Even if unsupported opcodes cause a module decode failure, if you provided a polyfill for the unsupported opcode, this lets you strike an ideal balance: Stray uses of the opcode are polyfilled, but you can avoid calling tight performance-sensitive loops that hit the polyfill over and over.
    • Features are not all opcodes, so you can use feature information to decide the best way to behave. Things like (completely hypothetical) 'is this SIMD operation one instruction on this architecture' or 'are 16-byte writes atomic on this architecture'. Note that neither of those are things you can detect with eval.
  • I'm writing a JIT
    • You need to know what opcodes are available before writing code that uses them. Same for non-opcode features. This information needs to be exposed somehow. Applications generating and JIT-compiling wasm is a use case that we're explicitly considering even if we won't be exposing a JIT API in the MVP.

For example, the layer-1 code negotiates an initial memory size and if memory growth is supported, and then chooses to use index masking on each memory access versus omit the masking, and this would change every memory access - very fine grain code changes. has_feature just does not handle this well. The need for this might depend on other performance issues and solutions that we have not even got around to exploring.

Layer-1 isn't user code and it's got nothing to do with feature detection or polyfilling. It's just structural compression. You're talking about adapting a loaded module at startup before it runs based on the nature of the target hardware/runtime, which is a good idea but is totally distinct from structural compression.

I honestly don't understand what your case has to do with has_feature or why your use case justifies removing it.

What can has_feature do that can not already be done by a runtime call to an imported function for coarse grain decisions?

You could implement the same primitive as a runtime call to an imported function. You could call the import has_feature and standardize it. What's the advantage? The point is that we need to standardize something so that people can use it. If there's some particular problem with has_feature being an opcode instead of a standardized import, I'd love to have that explained. It was my understanding that we were uninterested in standardizing any imports.

Perhaps yielding a static result that would allow the compiler to bake in the choice. But again that does not help if the decision can change from one instance to the next, and this has implications for caching and perhaps security. If supporting baking in choices then I would like to be able to bake in constants too - wasm does not even have the const support available in asm.js where I can use this to bake in a mask of zero to disable masking or to give a static mask to enable it. But again perhaps this is better handled at layer-1 to reduce the burden on the runtime compiler to bake in constants (including has_feature) and fold them and optimize away dead code etc. My experience trying to get useful support for this across JS implementation was a failure.

Again, layer-1 is just compression. But what you describe - yielding a static result - is how this opcode was designed up until this point. It doesn't make much sense if some of the features you feature-detect can go away at runtime - something like that would demand a different mechanism or a dedicated API.

The whole matter of the higher architecture needs to be re-visited. I support revisiting it later and focussing on getting the lower layers working. has_feature is a dead end from my perspective - happy to see it go.

Anyone arguing that has_feature is a dead end needs to provide more concrete reasoning or lay out a proposal for how a good solution would look. has_feature follows in the footsteps of successful feature detection/extensibility systems in production software, including the browser. For starters, you can look at how OpenGL extensions work. The OpenGL extension model is not perfect but it's extremely successful, to say the least.

@ghost
Copy link

ghost commented Jan 25, 2016

You don't need to extend the encoding, the encoding just needs to be
able to accommodate new opcodes and value types (by design, from the

I think we agree on trying to make the encoding extensible.

Abstract decoding like this is also why it would be possible to put
small polyfill functions into the opcode table. Many new opcodes can be
implemented (with a performance penalty) by raw wasm that receives the
opcode's immediates/operands and processes them. It would be up to a
compiler or toolchain to generate the polyfills and put them in the
opcode table.

Interesting idea for operations that are equivalent to functions. But again it exposes the features to the use code. A privileged upper layer could just rewrite the operations and perhaps without being visible to the user code.
...

  • I want to determine what features are supported before loading a
    module, so that I can select a special version of it for that
    feature set.

It might be possible to design this to be declarative not procedural, and has_feature is procedural. Again the privileged upper layer code could read the declarations and check the features and make the decisions.

I want to determine what features are supported to decide how to
behave in a certain scenario

  o We never reached an agreement on this in design discussions, but
    a wasm implementation could theoretically delay 'are opcodes
    supported' checks until the first use of a given function. In
    that case, |has_feature| would let you pick an appropriate
    function at runtime, as with |cpuid| on x86.

has_feature was not a runtime variable function, rather it was designed to give a static value available at compile time.

  o Even if unsupported opcodes cause a module decode failure, if
    you provided a polyfill for the unsupported opcode, this lets
    you strike an ideal balance: Stray uses of the opcode are
    polyfilled, but you can avoid calling tight
    performance-sensitive loops that hit the polyfill over and over.

Why not just detect before compile time and do the rewriting then.

  o Features are not all opcodes, so you can use feature information
    to decide the best way to behave. Things like (completely
    hypothetical) 'is this SIMD operation one instruction on this
    architecture' or 'are 16-byte writes atomic on this
    architecture'. Note that neither of those are things you can
    detect with |eval|.

Again, a privileged upper layer could make these choices.

  • I'm writing a JIT

    o You need to know what opcodes are available before writing code
    that uses them. Same for non-opcode features. This information
    needs to be exposed somehow. Applications generating and
    JIT-compiling wasm is a use case that we're explicitly
    considering even if we won't be exposing a JIT API in the MVP.

If wasm code is generating wasm then it could target the same generic pipe that all code goes through, the same pipe that allows the runtime to make these decisions. It could pipe it through the same privileged upper layer which would do any necessary rewriting.

For example, the layer-1 code negotiates an initial memory size and
if memory growth is supported, and then chooses to use index masking
on each memory access versus omit the masking, and this would change
every memory access - very fine grain code changes. has_feature just
does not handle this well. The need for this might depend on other
performance issues and solutions that we have not even got around to
exploring.

Layer-1 isn't user code and it's got nothing to do with feature
detection or polyfilling. It's just structural compression. You're
talking about adapting a loaded module at startup before it runs based
on the nature of the target hardware/runtime, which is a good idea but
is /totally distinct/ from structural compression.

The suggestion seems to be to experiment with layer-1 in user code, and this experimentation could very well explore adapting the code to features etc. It might be just 'structural compression' to you, but if you add some declarative macro support that has inputs from runtime options then it would subsume the has_feature support. If it could be implemented in a privileged module that is open source then it might just be possible to avoid exposing many of the features to the user code - I just don't know and have not had time to think it through.

I honestly don't understand what your case has to do with |has_feature|
or why your use case justifies removing it.

Because the solution would subsume has_feature.

What can has_feature do that can not already be done by a runtime
call to an imported function for coarse grain decisions?

You could implement the same primitive as a runtime call to an imported
function. You could call the import |has_feature| and standardize it.

It would not support fine grain code modification.

Again, layer-1 is just compression.

That's news to me. I for one would like to explore other upper layer options.

The whole matter of the higher architecture needs to be re-visited.
I support revisiting it later and focussing on getting the lower
layers working. has_feature is a dead end from my perspective -
happy to see it go.

Anyone arguing that |has_feature| is a dead end needs to provide more
concrete reasoning or lay out a proposal for how a good solution would
look.

  1. It does not support fine grain code choices.
  2. It only supports one-bit parameters. It does not even support compile time integer constants such as a memory size.
  3. Even if it did support compile time constants there might not be good support for the necessary compiler optimizations across runtimes. Practically we need to demand the least possible of the runtimes and seek to be able to do as much as possible in user code and we might want a separation of responsibility only giving access to the feature detection and choice to privileged code.
  4. It exposes the runtime features to the user wasm code - a potential security problem.

@kg
Copy link
Contributor

kg commented Jan 25, 2016

Interesting idea for operations that are equivalent to functions. But again it exposes the features to the use code. A privileged upper layer could just rewrite the operations and perhaps without being visible to the user code.

Privileged upper layers can solve a great many problems, but if we're going to introduce something like that we have to spec it, and define a way to give it proper security isolation and make it extensible and rev it. That's a lot of complexity.

It might be possible to design this to be declarative not procedural, and has_feature is procedural. Again the privileged upper layer code could read the declarations and check the features and make the decisions.

I'd be in favor of a declarative solution if someone proposed one that was easy to implement. We have neither a proposal nor implementation for a declarative solution right now. "Privileged upper layer code" sounds like a nontrivial thing to spec, even if its only responsibility is this.

has_feature was not a runtime variable function, rather it was designed to give a static value available at compile time.

No, the information isn't available at compile time. When you run llvm and generate your wasm AST and encode it to binary, you don't know anything about the user's machine. At runtime, the user's machine decodes the AST and maps it to native machine code (or interprets it, or compiles it to javascript). At that point the values for has_feature are available, but at no point before.

Why not just detect before compile time and do the rewriting then.

Because you can't. :-) You could of course do a compile-time target for a given set of feature flags, and do that for every configuration you care to support. Then you can pick the right configuration to load for each module... by detecting the available features.

If wasm code is generating wasm then it could target the same generic pipe that all code goes through, the same pipe that allows the runtime to make these decisions. It could pipe it through the same privileged upper layer which would do any necessary rewriting.

Funneling JIT code through extra layers is a great way to make JIT so slow that it's unusable. Keep in mind that JITs often run on a method-at-a-time basis.

The suggestion seems to be to experiment with layer-1 in user code, and this experimentation could very well explore adapting the code to features etc. It might be just 'structural compression' to you, but if you add some declarative macro support that has inputs from runtime options then it would subsume the has_feature support. If it could be implemented in a privileged module that is open source then it might just be possible to avoid exposing many of the features to the user code - I just don't know and have not had time to think it through.

The current definition of layer 1 doesn't match what you're discussing. What you're suggesting sounds like it could be worthwhile, so do you mind filing a separate issue to propose your redefinition of layer 1? There have been some past discussions about whether we should spec it at all, so that issue is probably useful to have.

It would not support fine grain code modification.

This needs to be defined. Those four words don't mean anything to me without context.

It only supports one-bit parameters. It does not even support compile time integer constants such as a memory size.

Memory size isn't a feature. It's a parameter. You'd query the parameter using some sort of parameter querying function or opcode. GL extensions work this way: You detect the existence of the extension, and the extension can come with new operations, queries, etc.

Even if it did support compile time constants there might not be good support for the necessary compiler optimizations across runtimes. Practically we need to demand the least possible of the runtimes and seek to be able to do as much as possible in user code and we might want a separation of responsibility only giving access to the feature detection and choice to privileged code.

No optimization is required on the part of the runtime for has_feature to work. You just implement it.

It exposes the runtime features to the user wasm code - a potential security problem.

Please do not make claims like this unless they are supported.

@ghost
Copy link

ghost commented Jan 25, 2016

Interesting idea for operations that are equivalent to functions.
But again it exposes the features to the use code. A privileged
upper layer could just rewrite the operations and perhaps without
being visible to the user code.

Privileged upper layers can solve a great many problems, but if we're
going to introduce something like that we have to spec it, and define a
way to give it proper security isolation and make it extensible and rev
it. That's a lot of complexity.

Yes, it does seem out of reach to get something like that spec'ed. But we could at least explore the separation and possibility in user code.

has_feature was not a runtime variable function, rather it was
designed to give a static value available at compile time.

No, the information isn't available at compile time. When you run llvm

Let me clarify 'compile time' - let it be the runtime compiling the wasm into machine code, not the upstream tools. So if the runtime knows that the result of has_feature is a constant it can bake it into the code, it could fold constants, and delete unreachable code. In contrast, if it were a runtime variable then the compiler could not do this. I do think this was a significant property of has_feature. But if an upper layer can do these compiler optimizations then that reduces the wasm runtime burden. I see a lot of resistance from the browser vendors to supporting optimizations that they see as unnecessary, which I do appreciate. If we can move these optimization up a layer it helps in a number of ways.

Why not just detect before compile time and do the rewriting then.

Because you can't. :-) You could of course do a compile-time target for

Rewriting on the client side, but before the wasm runtime compiles to machine code.

It would not support fine grain code modification.

This needs to be defined. Those four words don't mean anything to me
without context.

Ok, this seems to be a core communication failure.

  • Coarse grain: code path choices between separate blocks of code that each do a significant amount of work relative to the cost of making the choice. Here it does not make much difference to code size or performance whether has_feature runs at compile time (runtime compiling to machine code) or at runtime. For example choosing between a SIMD optimized function and an non-SIMD function each that do a significant amount of work.
  • Fine grain: code choices between small blocks of code or between code patterns for which performance is affected by the cost of making a runtime choice and/or for which even the AST overhead of including the has_feature operator and separate code patterns is significant. For example changing the pattern of all memory accesses to mask the index and changing the mask based on a negotiated memory size.
It only supports one-bit parameters. It does not even support
compile time integer constants such as a memory size.

Memory size isn't a feature. It's a parameter. You'd query the parameter
using some sort of parameter querying function or opcode. GL extensions
work this way: You detect the existence of the extension, and the
extension can come with new operations, queries, etc.

A runtime parameter query does not support compile time (the runtime compiling to machine code) optimization and thus not the fine grain code change use cases.

No optimization is required on the part of the runtime for |has_feature|
to work. You just implement it.

Only for the coarse grain use case.

It exposes the runtime features to the user wasm code - a potential
security problem.

Please do not make claims like this unless they are supported.

It's a concern, not some shocking allegation, and I believe it is true to the extent that giving the wasm user code access to a features list supports fingerprinting the runtime and it might even help target exploits. Perhaps 'security problem' is a poor choice of words, sorry, but what do you call giving every piece of wasm code the capability to access a fingerprint of the runtime? What's not clear is if we could do any better, and I am only suggesting I would like to revisit this area to think it through.

@qwertie
Copy link

qwertie commented Jan 25, 2016

Oh boy, you two ... I feel like I know perfectly well what both of you are saying, but somehow you're having trouble listening to each other. Or maybe I'm missing something.

In earlier discussions, I felt like there was consensus that "layer 1" shouldn't be fully spec'd in MVP, and that therefore layer 1 would be implemented by user code. It wasn't clear to me whether said user code would itself be written in wasm, but I hope and expect so, as there is an immense amount of power in allowing wasm code to introspect and generate other wasm code (us .NET developers love our runtime code generation, and I imagine various programming languages want or need such a feature, especially all those research languages struggling to gain traction...). Ideally, wasm functions could generate other functions that use the same address space, although having two separate address spaces (one for the layer 1 "decompressor" and another used later by the expanded module) would suffice for implementing a "normal" layer 1.

But of course, if layer 1 is implemented by user code, there is nothing forcing it to be a mere decompressor; thus it can implement all the wizardry described by JSStats, including implementing has_feature itself. It could recognize a has_feature opcode that is not in the wasm spec, and implement it with some other mechanism that is in the spec (though I am left wondering what mechanism that would be). IIRC, JSStats has also pushed for some guarantee that the layer 1 code could be invoked for a module in some cases when the browser has already cached the corresponding layer 0 - he must have interesting ideas for things to do with layer 1 other than compression.

I am sympathetic to kg's point that

the plan amounts to 'we'll figure it out later', which is totally fine in most cases but is less than totally fine when it's being used as the justification to remove a tiny opcode that is already designed and already specced.

We could just modify the design docs to say that the feature is likely to be removed before MVP because "blah blah blah layer 1 subsumes it".

Funneling JIT code through extra layers is a great way to make JIT so slow that it's unusable.

I didn't figure out if that's what JSStats meant to suggest (my mental model of wasm remains quite impoverished from merely reading design docs and following discussions..) but assuming your interpretation is correct, my response would be that yes, a feature used inappropriately will have undesirable results. But a feature like this could also enable/augment some really cool stuff - such as metaprogramming, AOP, debuggers implemented in wasm, profiling tools, and garbage collectors. If we can spec it to avoid unnecessary performance hits, I'm not only in favor, I'm salivating.

I'm not following the "potential security problem" of has_feature, since (1) doesn't a user-code layer 1 need access to something equivalent to has_feature to do its job? (the user-code layer 1 could be evil.) (2) as kg mentioned, the fingerprint of given by has_feature seems to just be the browser version + CPU type, information that is probably available anyway.

@kg
Copy link
Contributor

kg commented Jan 25, 2016

I'll probably follow up on the rest of your replies later, but:

But of course, if layer 1 is implemented by user code, there is nothing forcing it to be a mere decompressor; thus it can implement all the wizardry described by JSStats, including implementing has_feature itself. It could recognize a has_feature opcode that is not in the wasm spec, and implement it with some other mechanism that is in the spec (though I am left wondering what mechanism that would be). IIRC, JSStats has also pushed for some guarantee that the layer 1 code could be invoked for a module in some cases when the browser has already cached the corresponding layer 0 - he must have interesting ideas for things to do with layer 1 other than compression.

Yeah, I want to make this explicitly clear: When we talk about layer 0 / layer 1 / layer 2 that is specifically in terms of how we compress wasm. One of the key details here is that it needs to be possible for a runtime (like a browser) to implement all three layers in native code without extra buffers or multiple passes. This gives us a path towards extremely good load performance, even on mobile devices. Note that you aren't obligated to do that, so it's certainly possible to start out by implementing those things in user space - doing your layer 2 decompress and passing layer 1 bytes to the decoder, for example - but it's not intended.

That's why I keep saying that layer 1 isn't anything more than compression. You can do all sorts of cool stuff by filtering wasm modules before they're compiled, but mixing that with compression means that now all of it has to run in user space, forever, unless we standardize all the filtering and other cool tricks at the same time.

It sounds like what you want is to introduce some sort of filter layer defined in userspace that runs at some point before the code is compiled. I can imagine something like that being possible, but I have no idea how I'd spec it and I don't know how happy v8 or spidermonkey or jsc or chakra people would be about implementing it :-) So again, please file an issue about that!

@ghost
Copy link

ghost commented Jan 25, 2016

Fwiw #378

@s3ththompson
Copy link
Member

[sent before reply was complete]
Doesn't punting on defining has_feature mean that all post-MVP code will have to detect the existence of has_feature itself (by trying to compile code with the has_feature opcode), before using it?

@titzer
Copy link
Author

titzer commented Jan 25, 2016

Another option is to simply kick "has_feature" up to the embedding level.
In the case of JS, we could make the "WASM" object expose coarse-grained
properties that could be queried, allowing the JS to load different WASM
modules depending on querying the WASM object. E.g.

if (WASM.SIMD != undefined) ...

Or even individual opcodes:

if (WASM.int64_rotl) ...

Not a great solution, but a workaround.

I think we need to have some fairly-well baked examples to go into this
territory. E.g. even if we allow has_feature into code, there is still the
possibility of introducing new types which are encoded in signatures,
locals, etc.

On Mon, Jan 25, 2016 at 6:46 PM, Seth Thompson notifications@github.com
wrote:

[sent before reply was complete]
Doesn't punting on defining has_feature mean that all post-MVP code will
have to detect the existence of has_feature itself (by trying to compile
code with the has_feature opcode), before using it?


Reply to this email directly or view it on GitHub
#523 (comment).

@qwertie
Copy link

qwertie commented Jan 26, 2016

@kg Is there a design doc or recorded consensus saying "layer 1 is only used for compression"?

I think the layer 1 that will be spec'd out later is currently expected to do compression only. But whatever pipeline is defined by MVP to allow a userspace layer 1+2 would not be disabled, since the feature is useful and we don't want to break backward compatibility. So whatever technique enables a "userspace has_feature" would still be available, even, I would think, when using the built-in layer 1.

My question remains: if there's no has_feature, what's on the table as a replacement? A "management module" (#378) would still need a way to query Wasm's features. Edit: titzer's suggestion seems to be exposing the info to JS rather than Wasm. Hmm. I'd prefer an in-wasm solution standardized across browser and non-browser scenarios.

@kg
Copy link
Contributor

kg commented Jan 26, 2016

Another option is to simply kick "has_feature" up to the embedding level. In the case of JS, we could make the "WASM" object expose coarse-grained properties that could be queried, allowing the JS to load different WASM modules depending on querying the WASM object. E.g.

@titzer's point here is a good one - we can just punt this up to the embedder. I think this is an adequate compromise if we're dead-set on removing the opcode. We need to leave some form of this primitive in, though, and we will end up having to standardize how that embedder call works. It leaves out the ability to make runtime behavior decisions, but it's possible that you can funnel all the feature flags in to the module from outside so you can make those decisions.

@kg Is there a design doc or recorded consensus saying "layer 1 is only used for compression"?
I think the layer 1 that will be spec'd out later is currently expected to do compression only. But whatever pipeline is defined by MVP to allow a userspace layer 1+2 would not be disabled, since the feature is useful and we don't want to break backward compatibility. So whatever technique enables a "userspace has_feature" would still be available, even, I would think, when using the built-in layer 1.

The idea of putting metaprogramming features into the layered compression pipeline has not come up before now, with the exception of macros - and in particular we seemed agreed on the idea that nullary macros (deduplication) are uncontroversial and good, but macros-with-arguments is hard and would need experimentation to figure out.

So: A metaprogramming layer is an interesting idea, but it really doesn't belong in the layered compression pipeline, because it would undermine the viability of the compression pipeline as a whole. It sounds like a great candidate for something to spec in parallel with the core standards process so that it could potentially be introduced post-MVP. It's definitely something you could prototype in user-space, and a runtime could potentially offer the metaprogramming code easy access to decompressed layer0 bytecode so it's easy to do the filtering/rewriting.

@kg
Copy link
Contributor

kg commented Jan 26, 2016

Hit 'Comment' too early.

One last thing: Even if we strip has_feature out entirely, as @titzer points out it doesn't actually eliminate the hard problems:

I think we need to have some fairly-well baked examples to go into this territory. E.g. even if we allow has_feature into code, there is still the possibility of introducing new types which are encoded in signatures, locals, etc.

We know that new local/argument types will show up eventually, and that new opcodes will potentially have new types of immediates. We also know that new opcodes will have arguments. This all demands that we do a certain amount of work so that existing encoders can decode modules containing these new types and opcodes, even if the end result is that they get rejected as incompatible and punted to a polyfill. The 'contains new opcodes = doesn't decode at all' approach is only suitable for a subset of runtime scenarios, and isn't suitable at all for debuggers or development tools.

Though to clarify again: being able to decode new opcodes/immediates is not about being able to execute code depending on either of those things, it's about being able to decode a module and make sense of it. This basically means modules need to have a small schema attached to them (though it doesn't actually need to be INSIDE the file - there are some alternatives like Brotli's static dictionary).

@jfbastien
Copy link
Member

(continuing a conversation @s3ththompson and I had today)

Can the embedder optimize has_feature well if it's all implemented on the embedder side, versus being built into wasm? We can do it, and punting is often fine, but I naively think we lose DCE opportunities if we punt has_feature to the embedder because then we need to prove the JS state doesn't change at all.

Or am I missing something?

Again, it's fine to punt, but we have to be OK with the cost.

@titzer
Copy link
Author

titzer commented Jan 26, 2016

Another possibility is to make the approach of "probing via eval()'ing
modules" extremely cheap. For example, we could add a header section to the
binary format that encodes "features required for this module to work".
Features could be described as strings such as "SIMD" (SIMD), "INT_ROT"
(integer rotation instructions), "MDX" (managed data extensions), "VMX"
(virtual memory extensions, e.g. mmap() and similar). The section of
requested features could be very concise and immediately follow a
WASM-identifying header. Then the procedure to feature-detect would be as
simple as eval()'ing modules that contain only the feature request
section and seeing if they succeed. That'd be really easy to implement in
decoders too: they simply parse this special section and report a failure
if it module requests a feature it does not support. This would be
relatively fast since these probing modules would be only a few dozen bytes
at most. It's future-extensible since all features are encoded in strings
whose uniqueness we could manage through a community process.

E.g. for the "SIMD" feature I can see that this would unlock:

  • new local types which would be valid in function signatures and as
    function locals
  • new operators, which entails new bytecodes (either through an opcode
    table or directly encoded)

E.g. for the "THREADS" feature this would unlock

  • a new local type for a thread handle (?)
  • new operators which start, stop, and manipulate threads
  • new operators for atomic operations and futexes

E.g. for "MDX" managed data extensions this would unlock:

  • new local types
  • new type constructors (function references and structs)
  • new operators for accessing managed objects

On Tue, Jan 26, 2016 at 7:40 AM, JF Bastien notifications@github.com
wrote:

(continuing a conversation @s3ththompson https://github.com/s3ththompson
and I had today)

Can the embedder optimize has_feature well if it's all implemented on the
embedder side, versus being built into wasm? We can do it, and punting is
often fine, but I naively think we lose DCE opportunities if we punt
has_feature to the embedder because then we need to prove the JS state
doesn't change at all.

Or am I missing something?

Again, it's fine to punt, but we have to be OK with the cost.


Reply to this email directly or view it on GitHub
#523 (comment).

@lukewagner
Copy link
Member

@titzer I think the opcode table would essentially be that: any unknown operator name string would raise a validation error. Assuming we make opcode tables their own sections and the other sections are optional, you have a super-small module by creating a module containing only an opcode-table section with only the opcodes of interest in the table. What's nice about this is it avoids the need to create an ad hoc collection of feature strings with ad hoc granularity (which is what also started to seem awkward, especially without concrete use cases, with has_feature).

@titzer
Copy link
Author

titzer commented Feb 17, 2016

@lukewagner It's more than just opcodes. We will have new types and new values. An opcode table is not enough, since we need to have binary representations of new types and possibly new values if we allow constants of the new types to be in the binary.

Do we really need to have feature detection at the granularity of individual opcodes? It seems to only increase the problem of combinatorics.

In some sense this comes down to a question of what we are spec'ing as "standard" and "in-development". Are we specing a binary format? How binary is binary?

So far in the binary format, strings do not have semantic meaning, since we have an already agreed upon base set of types, values and operators. To extend this, we can go one of two ways; either add more standard binary encodings, or add a generic mechanism by which strings map to binary encodings (i.e. opcode table). But that then implies the semantic meaning is really in the strings of individual opcodes, individual types, etc. IMHO that's no longer a binary format but an adhoc compression mechanism. That's not just an aesthetics concern; it means work in implementations. Implementations have to understand strings for everything. Today they do not.

The other thing is that that inherently separates "base WASM" from all the extensions will we will ever have, unless we have a mechanism to add extensions to "base WASM" and give them blessed, standardized encodings.

@lukewagner
Copy link
Member

It's more than just opcodes. We will have new types and new values.

Not coincidentally, I think in addition we also need a "type-code table" :) We can probably delay adding this until after the MVP by saying, just like with opcode tables, if you don't specify a table, there is a default one (which in the MVP, for types, contains the 4 value types).

I don't know what you mean by "new values" though?

Do we really need to have feature detection at the granularity of individual opcodes? It seems to only
increase the problem of combinatorics.

I don't think we need to but if it's already there it seems better than attempting to define a new thing which forces us to make some ad hoc granularity choices. Maybe ping @s3ththompson; he was saying he talked to some other Webby Googlers who had experience in this space before and they've also said that attempts to define coarse granularity feature tests had been troublesome. Detecting at the individual op level is like wasm's version of the common JS technique of testing the existence of representative constructors/functions.

So far in the binary format, strings do not have semantic meaning

In the proposed extension where sections have string names then those few blessed section names strings would have semantics; we're just replacing a number with a string (and not worrying about size b/c of low frequency). Similarly, that's what I think we should do with operators, replacing op_code_s with op_name_s. Of course that would bloat us 5x so yes, opcode tables are a compression technique, but, just like variable-length integers et al, it's worth it in layer 0.

Now you could ask: do we reallllly need the op names? I think so. During the MVP we have a grace period where noone ships anything until everyone agrees on everything. However, I think it's wishful thinking to imagine that's how it will always be; it's certainly not the case in JS or Web APIs: browsers implement things in (different) priority order and then you have holdouts on various contentious features and that will definitely, imho, happen at some time in the future with wasm and then lead to conflicts/ambiguities over this precious dense opcode index space.

And in addition to those "social" constraints, you have the added benefit that if, in the early days, we allocate all the prime 1-byte real estate to the MVP operators, including all the super-cold ones like grow_memory, in the future when we add some super-hot/useful opcode, it won't forever be stuck with a 2-byte encoding. (I'm sure Intel wishes it could reallocate the single-byte opcode 0x04 from add al, imm8 ;) With the opcode table, modules can always give the hot ops single-byte encodings.

The other thing is that that inherently separates "base WASM" from all the extensions will we will ever
have, unless we have a mechanism to add extensions to "base WASM" and give them blessed,
standardized encodings.

This is a good point: with the MVP we'd define a "default" opcode table (what you'd have if you don't declare any table) which would be "base WASM" as you described. However, every quanta of time we'd get everyone to agree on extending this default opcode table (conservatively: so appending, not rearranging) with new opcodes. You can think of opcode tables as a way to avoid having this process be really awkward/political: if a feature doesn't have consensus, it can simply "miss the train".

titzer pushed a commit that referenced this pull request Feb 25, 2016
@titzer titzer merged commit cbb5902 into master Feb 25, 2016
@jfbastien jfbastien deleted the remove_has_feature branch February 25, 2016 17:13
jfbastien added a commit that referenced this pull request Mar 18, 2016
It was removed in #523, but was still referenced. Leave it as a high-level sketch for now.

Fixes #613.
@jfbastien jfbastien mentioned this pull request Mar 18, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants