Clarifying lift-then-lower and lower-then-lift semantics
This issue was originally discussed in https://github.com/WebAssembly/component-model/issues/9 and I believe the intention was that "degenerate cases" of pairing lower/lift would turn into a function that immediately traps. Interpreting the current canonical ABI, however I think may result in slightly different semantics.
Lower then lift
For this situation my understanding is that when the final function is called it'll call def canon_lift in the explainer and internally call the def canon_lower. My read is that in this situation the final function is expected to work correctly in the sense that when called will lower arguments into the canon lift options, immediately re-lift them from the options specified in canon lower, call the lower'd function, and then do the opposite for the results.
Effectively, to confirm, this use case is not intended to trap at all purely by construction and it could be part of a component. It's a bit silly to configure and does require that the linear memories and such are probably all the same, but otherwise this is a valid configuration that engines should run?
Lift then lower
For this case it's the opposite of the above where canon_lower is called first then canon_lift. This is slightly different where canon_lower immediately sets a flag which will cause canon_lift to trap. This means that by the time the canon_lowerfunction calls thecalleewe're guaranteed a trap will happen. Before this happens thoughcanon_lowerexecutes thelift operation for the arguemnts. In general lifting is a noop in terms of side effects within the module itself, but I believe this does mean that runtimes still have to validate all the arguments that are being lifted (e.g. that enum` discriminants are in-bounds and such).
Is this argument validation intentional? Or should instead the pseudo-code for lift/lower be updated to have a lifted-then-lowered function immediately trap without looking at the arguments at all?
For the "Lower then lift" case: that's right. (In addition to the caveats you listed, it will also fail to validate in various future cases where the canonical ABI of canon lower is different than canon lift.)
For "Lift then lower", that's a great point. So in general, if in the future we add custom adapter functions, we'll already need lifting to interleave with lowering and thus this spec-Python would need to make lift() a generator that gets passed into callee() (which itself would return a generator). Technically, until custom adapter functions, the only difference between synchronous-lift() and generator-lift() is which trap you get and, from a spec POV, all traps are same, so in theory, even as written now, it would be "fine" to trap before lifting.
But maybe I should just go ahead and make lift() a generator now to avoid this sort of spec-mental-gymnastics?
Looking at canonical.py a bit more, it is a non-trivial change to make lift() (transitively) a generator so, if you're happy with the answer of "it doesn't matter which kind of trap pops out", then perhaps we can just leave it as is? Technically, with lockdown semantics, it doesn't even matter if there are component-internal side effects before the trap as long as they are inside the blast zone.
My concern for which trap comes up would primarily be for the embedder rather than in-wasm content where different embeddings could perhaps surface different error messages. For example with a hypothetical official *.wast test suite for the component which test this corner case it might assert one particular error message when other implementations generate other error messages (e.g. in Wasmtime I might try to implement this as simply unreachable but a more spec-appropriate implementation might return the argument validation errors).
This is a very niche concern though and I don't think justifies any sort of large-scale rewrite of the current Python spec code so I think it's fine to leave as-is for now and generally consider a lift-then-lower function as the lowered function unconditionally trapping when called.
Thinking about this issue some more recently, I realized there's a composability hazard to have same-instance lift/lower trap. If we look again at the example I wrote in #9 where a reexport causes $Parent to non-obviously end up lowering its own lift, and then ask "what if $Child and $Parent were developed by separate people (and possibly imported instead of nested)", then neither $Child nor $Parent did anything obviously wrong, and yet there's a trap. I'm not aware of a good way to rule this situation out by construction without just disallowing re-exports (which seem necessary in general), so it seems like we should relax the semantics to not trap.
Going back to one of the original motivations for trapping: it allowed the simple copy semantics to be optimized into an efficient fused direct copy. That would still be possible in the 99% case which is also statically-known to an AOT compiler (which can see that the lift+lower being fused are in different instances). However, there would still need to be support to handle the rare same-instance case, which is an unfortunate implementation burden (eventually: probably this could remain a NYI panic in implementations for quite some time without anyone hitting it).