| Commit message (Collapse) | Author | Age | Files | Lines |
... | |
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
With nominal function types, this change makes it so that we preserve the
identity of the function type used with call_indirect instructions rather than
recreating a function heap type, which may or may not be the same as the
originally parsed heap type, from the function signature during module writing.
This will simplify the type system implementation by removing the need to store
a "canonical" nominal heap type for each unique signature. We previously
depended on those canonical types to avoid creating multiple duplicate function
types during module writing, but now we aren't creating any new function types
at all.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
This adds relaxed-simd instructions based on the current status of the
proposal
https://github.com/WebAssembly/relaxed-simd/blob/main/proposals/relaxed-simd/Overview.md.
Binary opcodes are based on what is listed in
https://github.com/WebAssembly/relaxed-simd/blob/main/proposals/relaxed-simd/Overview.md#binary-format.
Text names are not fixed yet, and some sort sort of names that maps to
the non-relaxed versions are chosen for this prototype.
Support for these instructions have been added to LLVM via builtins,
adding support here will allow Emscripten to successfully compile files
that use those builtins.
Interpreter support has also been added, and they delegate to the
non-relaxed versions of the instructions.
Most instructions are implemented in the interpreter the same way as the non-relaxed
simd128 instructions, except for fma/fms, which is always fused.
|
| |
|
| |
|
|
|
|
|
|
|
|
| |
See #4220 - this lets us handle the common case for now of simply having
an identical heap type to the table when the signature is identical.
With this PR, #4207's optimization of call_ref + table.get into
call_indirect now leads to a binary that works in V8 in nominal mode.
|
| |
|
|
|
|
| |
Adds the part of the spec test suite that this passes (without table.set we
can't do it all).
|
|
|
|
|
|
|
|
|
| |
See #4149
This modifies the test added in #4163 which used static casts on
dynamically-created structs and arrays. That was technically not
valid (as we won't want users to "mix" the two forms). This makes that
test 100% static, which both fixes the test and gives test coverage
to the new instructions added here.
|
|
|
|
|
|
|
|
|
|
|
|
| |
These variants take a HeapType that is the type we intend to cast to,
and do not take an RTT.
These are intended to be more statically optimizable. For now though
this PR just implements the minimum to get them parsing and to get
through the optimizer without crashing.
Spec: https://docs.google.com/document/d/1afthjsL_B9UaMqCA5ekgVmOm75BVFu6duHNsN9-gnXw/edit#
See #4149
|
|
|
|
|
|
|
| |
array.init is like array.new_with_rtt except that it takes
as arguments the values to initialize the array with (as opposed to
a size and an optional initial value).
Spec: https://docs.google.com/document/d/1afthjsL_B9UaMqCA5ekgVmOm75BVFu6duHNsN9-gnXw/edit#
|
|
|
|
|
|
|
|
|
|
|
| |
We recently decided to change 'event' to 'tag', and to 'event section'
to 'tag section', out of the rationale that the section contains a
generalized tag that references a type, which may be used for something
other than exceptions, and the name 'event' can be confusing in the web
context.
See
- https://github.com/WebAssembly/exception-handling/issues/159#issuecomment-857910130
- https://github.com/WebAssembly/exception-handling/pull/161
|
|
|
|
|
|
|
|
|
|
| |
This is the same as rtt.sub, but creates a "new" rtt each time. See
https://docs.google.com/document/d/1DklC3qVuOdLHSXB5UXghM_syCh-4cMinQ50ICiXnK3Q/edit#
The old Literal implementation of rtts becomes a little more complex here,
as it was designed for the original spec where only structure matters. It may
be worth a complete redesign there, but for now as the spec is in flux I think
the approach here is good enough.
|
|
|
|
|
|
| |
They are basically the flip versions. The only interesting part in the impl is that their
returned typed and sent types are different.
Spec: https://docs.google.com/document/d/1DklC3qVuOdLHSXB5UXghM_syCh-4cMinQ50ICiXnK3Q/edit
|
|
|
|
|
|
|
|
| |
Spec for it is here:
https://docs.google.com/document/d/1DklC3qVuOdLHSXB5UXghM_syCh-4cMinQ50ICiXnK3Q/edit#
Also reorder some things in wasm.h that were not in the canonical order (that has
no effect, but it is confusing to read).
|
|
|
|
|
|
|
|
|
| |
Renames the SIMD instructions
* LoadExtSVec8x8ToVecI16x8 -> Load8x8SVec128
* LoadExtUVec8x8ToVecI16x8 -> Load8x8UVec128
* LoadExtSVec16x4ToVecI32x4 -> Load16x4SVec128
* LoadExtUVec16x4ToVecI32x4 -> Load16x4UVec128
* LoadExtSVec32x2ToVecI64x2 -> Load32x2SVec128
* LoadExtUVec32x2ToVecI64x2 -> Load32x2UVec128
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Updates binary constants of SIMD instructions to match new opcodes:
* I16x8LoadExtSVec8x8 -> V128Load8x8S
* I16x8LoadExtUVec8x8 -> V128Load8x8U
* I32x4LoadExtSVec16x4 -> V128Load16x4S
* I32x4LoadExtUVec16x4 -> V128Load16x4U
* I64x2LoadExtSVec32x2 -> V128Load32x2S
* I64x2LoadExtUVec32x2 -> V128Load32x2U
* V8x16LoadSplat -> V128Load8Splat
* V16x8LoadSplat -> V128Load16Splat
* V32x4LoadSplat -> V128Load32Splat
* V64x2LoadSplat -> V128Load64Splat
* V8x16Shuffle -> I8x16Shuffle
* V8x16Swizzle -> I8x16Swizzle
* V128AndNot -> V128Andnot
* F32x4DemoteZeroF64x2 -> F32x4DemoteF64x2Zero
* I8x16NarrowSI16x8 -> I8x16NarrowI16x8S
* I8x16NarrowUI16x8 -> I8x16NarrowI16x8U
* I16x8ExtAddPairWiseSI8x16 -> I16x8ExtaddPairwiseI8x16S
* I16x8ExtAddPairWiseUI8x16 -> I16x8ExtaddPairwiseI8x16U
* I32x4ExtAddPairWiseSI16x8 -> I32x4ExtaddPairwiseI16x8S
* I32x4ExtAddPairWiseUI16x8 -> I32x4ExtaddPairwiseI16x8U
* I16x8Q15MulrSatS -> I16x8Q15mulrSatS
* I16x8NarrowSI32x4 -> I16x8NarrowI32x4S
* I16x8NarrowUI32x4 -> I16x8NarrowI32x4U
* I16x8ExtendLowSI8x16 -> I16x8ExtendLowI8x16S
* I16x8ExtendHighSI8x16 -> I16x8ExtendHighI8x16S
* I16x8ExtendLowUI8x16 -> I16x8ExtendLowI8x16U
* I16x8ExtendHighUI8x16 -> I16x8ExtendHighI8x16U
* I16x8ExtMulLowSI8x16 -> I16x8ExtmulLowI8x16S
* I16x8ExtMulHighSI8x16 -> I16x8ExtmulHighI8x16S
* I16x8ExtMulLowUI8x16 -> I16x8ExtmulLowI8x16U
* I16x8ExtMulHighUI8x16 -> I16x8ExtmulHighI8x16U
* I32x4ExtendLowSI16x8 -> I32x4ExtendLowI16x8S
* I32x4ExtendHighSI16x8 -> I32x4ExtendHighI16x8S
* I32x4ExtendLowUI16x8 -> I32x4ExtendLowI16x8U
* I32x4ExtendHighUI16x8 -> I32x4ExtendHighI16x8U
* I32x4DotSVecI16x8 -> I32x4DotI16x8S
* I32x4ExtMulLowSI16x8 -> I32x4ExtmulLowI16x8S
* I32x4ExtMulHighSI16x8 -> I32x4ExtmulHighI16x8S
* I32x4ExtMulLowUI16x8 -> I32x4ExtmulLowI16x8U
* I32x4ExtMulHighUI16x8 -> I32x4ExtmulHighI16x8U
* I64x2ExtendLowSI32x4 -> I64x2ExtendLowI32x4S
* I64x2ExtendHighSI32x4 -> I64x2ExtendHighI32x4S
* I64x2ExtendLowUI32x4 -> I64x2ExtendLowI32x4U
* I64x2ExtendHighUI32x4 -> I64x2ExtendHighI32x4U
* I64x2ExtMulLowSI32x4 -> I64x2ExtmulLowI32x4S
* I64x2ExtMulHighSI32x4 -> I64x2ExtmulHighI32x4S
* I64x2ExtMulLowUI32x4 -> I64x2ExtmulLowI32x4U
* I64x2ExtMulHighUI32x4 -> I64x2ExtmulHighI32x4U
* F32x4PMin -> F32x4Pmin
* F32x4PMax -> F32x4Pmax
* F64x2PMin -> F64x2Pmin
* F64x2PMax -> F64x2Pmax
* I32x4TruncSatSF32x4 -> I32x4TruncSatF32x4S
* I32x4TruncSatUF32x4 -> I32x4TruncSatF32x4U
* F32x4ConvertSI32x4 -> F32x4ConvertI32x4S
* F32x4ConvertUI32x4 -> F32x4ConvertI32x4U
* I32x4TruncSatZeroSF64x2 -> I32x4TruncSatF64x2SZero
* I32x4TruncSatZeroUF64x2 -> I32x4TruncSatF64x2UZero
* F64x2ConvertLowSI32x4 -> F64x2ConvertLowI32x4S
* F64x2ConvertLowUI32x4 -> F64x2ConvertLowI32x4U
|
|
|
|
|
|
|
|
|
| |
Renames the SIMD instructions
* LoadSplatVec8x16 -> Load8SplatVec128
* LoadSplatVec16x8 -> Load16SplatVec128
* LoadSplatVec32x4 -> Load32SplatVec128
* LoadSplatVec64x2 -> Load64SplatVec128
* Load32Zero -> Load32ZeroVec128
* Load64Zero -> Load64ZeroVec128
|
|
|
|
|
|
|
|
|
|
|
| |
Adds C/JS APIs for the SIMD instructions
* Load8LaneVec128 (was LoadLaneVec8x16)
* Load16LaneVec128 (was LoadLaneVec16x8)
* Load32LaneVec128 (was LoadLaneVec32x4)
* Load64LaneVec128 (was LoadLaneVec64x2)
* Store8LaneVec128 (was StoreLaneVec8x16)
* Store16LaneVec128 (was StoreLaneVec16x8)
* Store32LaneVec128 (was StoreLaneVec32x4)
* Store64LaneVec128 (was StoreLaneVec64x2)
|
|
|
|
| |
Also removes experimental SIMD instructions that were not included in the final
spec proposal.
|
|
|
|
|
| |
Works around #3682 With this, I can roundtrip
a large real-world testcase.
|
|
|
|
|
|
|
|
|
| |
We handled them as S63 instead of U32. That should be fine, as all U32 values fit
in S63. But it is not strictly correct. The signed encoding may use an additional byte
which is unnecessary, and there is an actual correctness issue where a U32 may
be interpreted as a large negative S63 (because it sign extends a final bit that
happens to be 1).
May help #3656 but that testcase still does not pass even with this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
When writing a binary, we take the local indexes in the IR and turn
them into the format in the binary, which clumps them by type. When
writing the names section we should be aware of that ordering, but
we never were, as noticed in #3499
This fixes that by saving the mapping of locals when we are emitting
the name section, then using it when emitting the local names.
This also fixes the order of the types themselves as part of the
refactoring. We used to depend on the ordering of types to decide
which to emit first, but that isn't good for at least two reasons. First,
it hits #3648 - that order is not fully
defined for recursive types. Also, it's not good for code size - we've
ordered the locals in a way we think is best already (ReorderLocals pass).
This PR makes us pick an order of types based on that, as much as
possible, that is, when we see a type for the first time we append it to
a list whose order we use.
Test changes: Some are just because we use a different order than
before, as in atomics64. But some are actual fixes, e.g. in heap-types
where we now have (local $tv (ref null $vector)) which is indeed
right - v there is for vector, and likewise m for matrix etc. - we
just had wrong names before. Another example, we now have
(local $local_externref externref) whereas before the name was
funcref, and which was wrong... seems like the incorrectness was
more common on reference types and GC types, which is why this was
not noticed before.
Fixes #3499
Makes part of #3648 moot.
|
|
|
|
|
|
| |
We were missing a pop of catchIndexStack at a Delegate. It ends the scope,
so it should do that, like TryEnd does.
Found by emscripten-core/emscripten#13485 on -O2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
I was previously mistaken about `rethrow`'s argument rule and thought
it only counted `catch`'s depth. But it turns out it follows the same
rule `delegate`'s label: the immediate argument follows the same rule as
when computing branch labels, but it only can target `try` labels
(semantically it targets that `try`'s corresponding `catch`); otherwise
it will be a validation failure. Unlike `delegate`, `rethrow`'s label
denotes not where to rethrow, but which exception to rethrow. For
example,
```wasm
try $l0
catch ($l0)
try $l1
catch ($l1)
rethrow $l0 ;; rethrow the exception caught by 'catch ($l0)'
end
end
```
Refer to this comment for the more detailed informal semantics:
https://github.com/WebAssembly/exception-handling/issues/146#issuecomment-777714491
---
This also reverts some of `delegateTarget` -> `exceptionTarget` changes
done in #3562 in the validator. Label validation rules apply differently
for `delegate` and `rethrow` for try-catch. For example, this is valid:
```wasm
try $l0
try
delegate $l0
catch ($l0)
end
```
But this is NOT valid:
```wasm
try $l0
catch ($l0)
try
delegate $l0
end
```
So `try`'s label should be used within try-catch range (not catch-end
range) for `delegate`s.
But for the `rethrow` the rule is different. For example, this is valid:
```wasm
try $l0
catch ($l0)
rethrow $l0
end
```
But this is NOT valid:
```wasm
try $l0
rethrow $l0
catch ($l0)
end
```
So the `try`'s label should be used within catch-end range instead.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
This adds support for reading/writing of the new `delegate` instruction
in the folded wast format, the stack IR format, the poppy IR format, and
the binary format in Binaryen. We don't have a formal spec written down
yet, but please refer to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics. In the
current version of spec `delegate` is basically a rethrow, but with
branch-like immediate argument so that it can bypass other
catches/delegates in between.
`delegate` is not represented as a new `Expression`, but it is rather
an option within a `Try` class, like `catch`/`catch_all`.
One special thing about `delegate` is, even though it is written
_within_ a `try` in the folded wat format, like
```wasm
(try
(do
...
)
(delegate $l)
)
```
In the unfolded wat format or in the binary format, `delegate` serves as
a scope end instruction so there is no separate `end`:
```wasm
try
...
delegate $l
```
`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we have not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.
So after discussions in #3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.
This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.
Many of changes to the existing test cases are because now all `try`s
are automatically assigned a label. They will be removed in
`RemoveUnusedNames` pass in the same way as block labels if not targeted
by any delegates.
This only supports reading and writing and has not been tested against
any optimization passes yet.
---
Original unfolded wat file to generate test/try-delegate.wasm:
```wasm
(module
(event $e)
(func
try
try
delegate 0
catch $e
end)
(func
try
try
catch $e
i32.const 0
drop
try
delegate 1
end
catch $e
end
)
)
```
|
|
|
| |
Adds support for modules with multiple tables. Adds a field for the table name to `CallIndirect` and updates the C/JS APIs accordingly.
|
|
|
|
|
| |
This adds missing stack IR printing support for the new form of
try-catch-catch_all. Also uses `printMedium` when printing instructions
consistently.
|
|
|
|
|
|
|
|
| |
As proposed in https://github.com/WebAssembly/simd/pull/395. Note that the other
instructions in the proposal have not been implemented in LLVM or in V8, so
there is no need to implement them in Binaryen right now either. This PR
introduces a new expression class for the new instructions because they uniquely
take an immediate argument identifying which portion of the input vector to
widen.
|
|
|
|
|
|
|
|
|
|
|
| |
This is only partial support, as br_on_null also has an extra optional
value in the spec. Implementing that is cumbersome in binaryen, and
there is ongoing spec discussions about it (see
https://github.com/WebAssembly/function-references/issues/45 ), so
for now we only support the simple case without the default value.
Also fix prefixed opcodes to be LEBs in RefAs, which was noticed here
as the change here made it noticeable whether the values were int8 or
LEBs.
|
|
|
|
|
|
| |
This break stack is maintained to compute branch depths, but it seems we
don't need to push and pop for each of if and else block or every single
catch/catch_all block; it would be sufficient to have just one stack
entry for the whole try-catch or if-else.
|
|
|
|
|
|
| |
This is different than the other RefAs variants in that it is part of the
typed functions proposal, and not GC. But it is part of GC prototype 3.
Note: This is not useful to us yet as we don't support non-nullable types.
|
|
|
|
|
|
|
|
| |
This expands the existing BrOnCast into BrOn that can also handle the
func/data/i31 variants. This is not as elegant as RefIs / RefAs in that BrOnCast
has an extra rtt field, but I think it is still the best option. We already have optional
fields on Break (the value and condition), so making rtt optional is not odd. And
it allows us to share all the behavior of br_on_* which aside from the cast or the
check itself, is identical - returning the value if the branch is not taken, etc.
|
|
|
|
|
|
|
|
| |
These are similar to is, but instead of returning an i32 answer, they trap on
an invalid value, and return it otherwise.
These could in theory be in a single RefDoThing, with opcodes for both As
and Is, but as the return values are different, that would be a little odd, and
the name would be less clear.
|
| |
|
|
|
|
|
|
|
|
| |
This internal refactoring prepares us for ref.is_func/data/i31, by renaming
the node and adding an "op" field. For now that field must always be "Null"
which means it is a ref.is_null.
This adjusts the C API to match the new IR shape. The high-level JS API
is unchanged.
|
|
|
|
|
|
|
|
| |
We now have multiple catches in each try, and a possible catch-all.
This changes our "extra delimiter" storage to store either an "else"
(unchanged from before) or an arbitrary list of things - we use that
for catches.
|
|
|
|
|
| |
Some fields were removed, see
https://docs.google.com/document/d/1yAWU3dbs8kUa_wcnnirDxUu9nEBsNfq0Xo90OWx6yuo/edit#
|
|
|
| |
This removes `exnref` type and `br_on_exn` instruction.
|
|
|
|
|
| |
This is not 100% of everything, but is enough to get tests passing, which
includes full binary and text format support, getting all switches to compile
without error, and some additions to InstrumentLocals.
|
|
|
|
| |
As proposed in https://github.com/WebAssembly/simd/pull/383, with opcodes
coordinated with the WIP V8 prototype.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
This updates `try`-`catch`-`catch_all` and `rethrow` instructions to
match the new spec. `delegate` is not included. Now `Try` contains not a
single `catchBody` expression but a vector of catch
bodies and events.
This updates most existing routines, optimizations, and tests modulo the
interpreter and the CFG traversal. Because the interpreter has not been
updated yet, the EH spec test is temporarily disabled in check.py. Also,
because the CFG traversal for EH is not yet updated, several EH tests in
`rse_all-features.wast`, which uses CFG traversal, are temporarily
commented out.
Also added a few more tests in existing EH test functions in
test/passes. In the previous spec, `catch` was catching all exceptions
so it was assumed that anything `try` body throws is caught by its
`catch`, but now we can assume the same only if there is a `catch_all`.
Newly added tests test cases when there is a `catch_all` and cases there
are only `catch`es separately.
|
|
|
|
|
|
|
|
|
| |
Noticed by comparing to V8 and Wasp. After this things are almost identical,
but there is also at least https://bugs.chromium.org/p/v8/issues/detail?id=11300
Test updates are due to having an instruction with two operands of which one is
unreachable. The new order puts the non-unreachable first, so it is not removed by
round-tripping through the binary format like before (which removes all unreachable
code).
|
|
|
|
| |
As proposed in https://github.com/WebAssembly/simd/pull/352, using the opcodes
used in the LLVM and V8 implementations.
|
|
|
|
|
|
| |
As proposed in https://github.com/WebAssembly/simd/pull/380, using the opcodes
used in LLVM and V8. Since these opcodes overlap with the opcodes of
i64x2.all_true and i64x2.any_true, which have long since been removed from the
SIMD proposal, this PR also removes those instructions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The tricky part here, as pointed out by aheejin in my previous attempt, is that
we need to know the type of the value we send if the branch is taken. We can
normally calculate that from the rtt parameter's type - we are casting to that
RTT, so we know what type that is - but if the rtt is unreachable, that's a problem.
To fix that, store the cast type on BrOnCast instructions.
This includes a test with a br_on_cast that succeeds and sends the cast value,
one that fails and passes through the uncast value, and also of one with an
unreachable RTT.
This includes a fix for Precompute, as noticed by that new test. If a break is
taken, with a ref as a value, we can't precompute it - for the same reasons
we can't precompute a ref in general, that it is a pointer to possibly shared
data.
|
|
|
|
|
|
| |
- i64x2.eq (https://github.com/WebAssembly/simd/pull/381)
- i64x2 widens (https://github.com/WebAssembly/simd/pull/290)
- i64x2.bitmask (https://github.com/WebAssembly/simd/pull/368)
- signselect ops (https://github.com/WebAssembly/simd/pull/124)
|
|
|
|
| |
This adds enough to read and write them and test that, but leaves
interpreter support for later.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
array.new/get/set/len - pretty straightforward after structs and all the
infrastructure for them.
Also fixes validation of the unnecessary heapType param in the
text and binary formats in structs as well as arrays.
Fixes printing of packed types in type names, which emitted i32
for them. That broke when we emitted the same name for an array
of i8 and i32 as in the new testing here.
Also fix a bug in Field::operator< which was wrong for packed
types; again, this was easy to notice with the new testing.
|