Title: [208402] trunk/Source/_javascript_Core
Revision
208402
Author
[email protected]
Date
2016-11-04 15:53:21 -0700 (Fri, 04 Nov 2016)

Log Message

Add support for Wasm br_table
https://bugs.webkit.org/show_bug.cgi?id=164429

Reviewed by Michael Saboff.

This patch adds support for Wasm br_table. The Wasm br_table
opcode essentially directly maps to B3's switch opcode.

There are also three other minor changes:
1) all non-argument locals should be initialized to zero at function entry.
2) add new setErrorMessage member to WasmFunctionParser.h
3) return does not decode an extra immediate anymore.

* testWasm.cpp:
(runWasmTests):
* wasm/WasmB3IRGenerator.cpp:
* wasm/WasmFunctionParser.h:
(JSC::Wasm::FunctionParser::setErrorMessage):
(JSC::Wasm::FunctionParser<Context>::parseExpression):
(JSC::Wasm::FunctionParser<Context>::parseUnreachableExpression):
(JSC::Wasm::FunctionParser<Context>::popExpressionStack):
* wasm/WasmValidate.cpp:
(JSC::Wasm::Validate::checkBranchTarget):
(JSC::Wasm::Validate::addBranch):
(JSC::Wasm::Validate::addSwitch):

Modified Paths

Diff

Modified: trunk/Source/_javascript_Core/ChangeLog (208401 => 208402)


--- trunk/Source/_javascript_Core/ChangeLog	2016-11-04 22:12:12 UTC (rev 208401)
+++ trunk/Source/_javascript_Core/ChangeLog	2016-11-04 22:53:21 UTC (rev 208402)
@@ -1,3 +1,31 @@
+2016-11-04  Keith Miller  <[email protected]>
+
+        Add support for Wasm br_table
+        https://bugs.webkit.org/show_bug.cgi?id=164429
+
+        Reviewed by Michael Saboff.
+
+        This patch adds support for Wasm br_table. The Wasm br_table
+        opcode essentially directly maps to B3's switch opcode.
+
+        There are also three other minor changes:
+        1) all non-argument locals should be initialized to zero at function entry.
+        2) add new setErrorMessage member to WasmFunctionParser.h
+        3) return does not decode an extra immediate anymore.
+
+        * testWasm.cpp:
+        (runWasmTests):
+        * wasm/WasmB3IRGenerator.cpp:
+        * wasm/WasmFunctionParser.h:
+        (JSC::Wasm::FunctionParser::setErrorMessage):
+        (JSC::Wasm::FunctionParser<Context>::parseExpression):
+        (JSC::Wasm::FunctionParser<Context>::parseUnreachableExpression):
+        (JSC::Wasm::FunctionParser<Context>::popExpressionStack):
+        * wasm/WasmValidate.cpp:
+        (JSC::Wasm::Validate::checkBranchTarget):
+        (JSC::Wasm::Validate::addBranch):
+        (JSC::Wasm::Validate::addSwitch):
+
 2016-11-04  JF Bastien  <[email protected]>
 
         WebAssembly JS API: implement more sections

Modified: trunk/Source/_javascript_Core/testWasm.cpp (208401 => 208402)


--- trunk/Source/_javascript_Core/testWasm.cpp	2016-11-04 22:12:12 UTC (rev 208401)
+++ trunk/Source/_javascript_Core/testWasm.cpp	2016-11-04 22:53:21 UTC (rev 208402)
@@ -296,6 +296,126 @@
     {
         // Generated from:
         //    (module
+        //     (func (export "br_table-with-loop") (param $x i32) (result i32)
+        //      (local $i i32)
+        //      (loop
+        //       (block
+        //        (get_local $x)
+        //        (set_local $i (i32.add (get_local $i) (i32.const 1)))
+        //        (set_local $x (i32.sub (get_local $x) (i32.const 1)))
+        //        (br_table 0 1)
+        //        )
+        //       )
+        //      (get_local $i)
+        //      )
+        //     )
+        Vector<uint8_t> vector = {
+            0x00, 0x61, 0x73, 0x6d, 0x0c, 0x00, 0x00, 0x00, 0x01, 0x86, 0x80, 0x80, 0x80, 0x00, 0x01, 0x40,
+            0x01, 0x01, 0x01, 0x01, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x07, 0x96, 0x80, 0x80,
+            0x80, 0x00, 0x01, 0x12, 0x62, 0x72, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x2d, 0x77, 0x69, 0x74,
+            0x68, 0x2d, 0x6c, 0x6f, 0x6f, 0x70, 0x00, 0x00, 0x0a, 0xa6, 0x80, 0x80, 0x80, 0x00, 0x01, 0xa0,
+            0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x01, 0x02, 0x00, 0x01, 0x00, 0x14, 0x00, 0x14, 0x01, 0x10,
+            0x01, 0x40, 0x15, 0x01, 0x14, 0x00, 0x10, 0x01, 0x41, 0x15, 0x00, 0x08, 0x01, 0x00, 0x01, 0x0f,
+            0x0f, 0x14, 0x01, 0x0f
+        };
+
+        Plan plan(*vm, vector);
+        checkPlan(plan, 1);
+
+        // Test this doesn't crash.
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(0) }), 1);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(1) }), 2);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(100) }), 101);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(122) }), 123);
+    }
+
+    {
+        // Generated from:
+        //    (module
+        //     (func (export "multiple-value") (param i32) (result i32)
+        //      (local i32)
+        //      (set_local 1 (block i32
+        //        (set_local 1 (block i32
+        //          (set_local 1 (block i32
+        //            (set_local 1 (block i32
+        //              (set_local 1 (block i32
+        //                (br_table 3 2 1 0 4 (i32.const 200) (get_local 0))
+        //                (return (i32.add (get_local 1) (i32.const 99)))
+        //                ))
+        //              (return (i32.add (get_local 1) (i32.const 10)))
+        //              ))
+        //            (return (i32.add (get_local 1) (i32.const 11)))
+        //            ))
+        //          (return (i32.add (get_local 1) (i32.const 12)))
+        //          ))
+        //        (return (i32.add (get_local 1) (i32.const 13)))
+        //        ))
+        //      (i32.add (get_local 1) (i32.const 14))
+        //      )
+        //     )
+        Vector<uint8_t> vector = {
+            0x00, 0x61, 0x73, 0x6d, 0x0c, 0x00, 0x00, 0x00, 0x01, 0x86, 0x80, 0x80, 0x80, 0x00, 0x01, 0x40,
+            0x01, 0x01, 0x01, 0x01, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x07, 0x92, 0x80, 0x80,
+            0x80, 0x00, 0x01, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x2d, 0x76, 0x61, 0x6c,
+            0x75, 0x65, 0x00, 0x00, 0x0a, 0xd3, 0x80, 0x80, 0x80, 0x00, 0x01, 0xcd, 0x80, 0x80, 0x80, 0x00,
+            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x10, 0xc8, 0x01,
+            0x14, 0x00, 0x08, 0x04, 0x03, 0x02, 0x01, 0x00, 0x04, 0x14, 0x01, 0x10, 0xe3, 0x00, 0x40, 0x09,
+            0x0f, 0x15, 0x01, 0x14, 0x01, 0x10, 0x0a, 0x40, 0x09, 0x0f, 0x15, 0x01, 0x14, 0x01, 0x10, 0x0b,
+            0x40, 0x09, 0x0f, 0x15, 0x01, 0x14, 0x01, 0x10, 0x0c, 0x40, 0x09, 0x0f, 0x15, 0x01, 0x14, 0x01,
+            0x10, 0x0d, 0x40, 0x09, 0x0f, 0x15, 0x01, 0x14, 0x01, 0x10, 0x0e, 0x40, 0x0f
+        };
+
+        Plan plan(*vm, vector);
+        checkPlan(plan, 1);
+
+        // Test this doesn't crash.
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(0) }), 213);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(1) }), 212);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(2) }), 211);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(3) }), 210);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(3) }), 210);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(4) }), 214);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(5) }), 214);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(-1) }), 214);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(-1000) }), 214);
+    }
+
+    {
+        // Generated from:
+        //    (module
+        //     (func (export "singleton") (param i32) (result i32)
+        //      (block
+        //       (block
+        //        (br_table 1 0 (get_local 0))
+        //        (return (i32.const 21))
+        //        )
+        //       (return (i32.const 20))
+        //       )
+        //      (i32.const 22)
+        //      )
+        //     )
+        Vector<uint8_t> vector = {
+            0x00, 0x61, 0x73, 0x6d, 0x0c, 0x00, 0x00, 0x00, 0x01, 0x86, 0x80, 0x80, 0x80, 0x00, 0x01, 0x40,
+            0x01, 0x01, 0x01, 0x01, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x07, 0x8d, 0x80, 0x80,
+            0x80, 0x00, 0x01, 0x09, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x00, 0x00, 0x0a,
+            0x9c, 0x80, 0x80, 0x80, 0x00, 0x01, 0x96, 0x80, 0x80, 0x80, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
+            0x14, 0x00, 0x08, 0x01, 0x01, 0x00, 0x10, 0x15, 0x09, 0x0f, 0x10, 0x14, 0x09, 0x0f, 0x10, 0x16,
+            0x0f
+        };
+
+        Plan plan(*vm, vector);
+        checkPlan(plan, 1);
+
+        // Test this doesn't crash.
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(0) }), 22);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(1) }), 20);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(11) }), 20);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(-100) }), 20);
+    }
+
+    {
+        // Generated from:
+        //    (module
         //     (func (export "if-then-both-fallthrough") (param $x i32) (param $y i32) (result i32)
         //      (block $block i32
         //       (if i32 (i32.eq (get_local $x) (i32.const 0))
@@ -320,8 +440,8 @@
         checkPlan(plan, 1);
 
         // Test this doesn't crash.
-        CHECK(isIdentical(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { boxf(0), boxf(32) }), 1));
-        CHECK(isIdentical(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { boxf(1), boxf(32) }), 2));
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(0), box(32) }), 1);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(1), box(32) }), 2);
     }
 
     {
@@ -347,8 +467,8 @@
         checkPlan(plan, 1);
 
         // Test this doesn't crash.
-        CHECK(isIdentical(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { boxf(0), boxf(32) }), 1));
-        CHECK(isIdentical(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { boxf(1), boxf(32) }), 2));
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(0), box(32) }), 1);
+        CHECK_EQ(invoke<int>(*plan.compiledFunction(0)->jsEntryPoint, { box(1), box(32) }), 2);
     }
 
     {

Modified: trunk/Source/_javascript_Core/wasm/WasmB3IRGenerator.cpp (208401 => 208402)


--- trunk/Source/_javascript_Core/wasm/WasmB3IRGenerator.cpp	2016-11-04 22:12:12 UTC (rev 208401)
+++ trunk/Source/_javascript_Core/wasm/WasmB3IRGenerator.cpp	2016-11-04 22:53:21 UTC (rev 208402)
@@ -32,6 +32,7 @@
 #include "B3ConstPtrValue.h"
 #include "B3FixSSA.h"
 #include "B3StackmapGenerationParams.h"
+#include "B3SwitchValue.h"
 #include "B3Validate.h"
 #include "B3ValueInlines.h"
 #include "B3Variable.h"
@@ -175,6 +176,7 @@
 
     bool WARN_UNUSED_RETURN addReturn(const ExpressionList& returnValues);
     bool WARN_UNUSED_RETURN addBranch(ControlData&, ExpressionType condition, const ExpressionList& returnValues);
+    bool WARN_UNUSED_RETURN addSwitch(ExpressionType condition, const Vector<ControlData*>& targets, ControlData& defaultTargets, const ExpressionList& expressionStack);
     bool WARN_UNUSED_RETURN endBlock(ControlEntry&, ExpressionList& expressionStack);
     bool WARN_UNUSED_RETURN addEndToUnreachable(ControlEntry&);
 
@@ -191,6 +193,7 @@
 
     void unify(Variable* target, const ExpressionType source);
     void unifyValuesWithBlock(const ExpressionList& resultStack, ResultList& stack);
+    Value* zeroForType(Type);
 
     Memory* m_memory;
     Procedure& m_proc;
@@ -200,6 +203,7 @@
     Vector<UnlinkedCall>& m_unlinkedCalls;
     GPRReg m_memoryBaseGPR;
     GPRReg m_memorySizeGPR;
+    Value* m_zeroValues[Type::LastValueType];
 };
 
 B3IRGenerator::B3IRGenerator(Memory* memory, Procedure& procedure, Vector<UnlinkedCall>& unlinkedCalls)
@@ -209,6 +213,9 @@
 {
     m_currentBlock = m_proc.addBlock();
 
+    for (unsigned i = 0; i < Type::LastValueType; ++i)
+        m_zeroValues[i] = m_currentBlock->appendIntConstant(m_proc, Origin(), toB3Type(static_cast<Type>(i + 1)), 0);
+
     if (m_memory) {
         m_memoryBaseGPR = m_memory->pinnedRegisters().baseMemoryPointer;
         m_proc.pinRegister(m_memoryBaseGPR);
@@ -225,13 +232,22 @@
     }
 }
 
+Value* B3IRGenerator::zeroForType(Type type)
+{
+    ASSERT(type != Void);
+    return m_zeroValues[type - 1];
+}
+
 bool B3IRGenerator::addLocal(Type type, uint32_t count)
 {
     if (!m_locals.tryReserveCapacity(m_locals.size() + count))
         return false;
 
-    for (uint32_t i = 0; i < count; ++i)
-        m_locals.uncheckedAppend(m_proc.addVariable(toB3Type(type)));
+    for (uint32_t i = 0; i < count; ++i) {
+        Variable* local = m_proc.addVariable(toB3Type(type));
+        m_locals.uncheckedAppend(local);
+        m_currentBlock->appendNew<VariableValue>(m_proc, Set, Origin(), local, zeroForType(type));
+    }
     return true;
 }
 
@@ -545,6 +561,20 @@
     return true;
 }
 
+bool B3IRGenerator::addSwitch(ExpressionType condition, const Vector<ControlData*>& targets, ControlData& defaultTarget, const ExpressionList& expressionStack)
+{
+    for (size_t i = 0; i < targets.size(); ++i)
+        unifyValuesWithBlock(expressionStack, targets[i]->result);
+    unifyValuesWithBlock(expressionStack, defaultTarget.result);
+
+    SwitchValue* switchValue = m_currentBlock->appendNew<SwitchValue>(m_proc, Origin(), condition);
+    switchValue->setFallThrough(FrequentedBlock(defaultTarget.targetBlockForBranch()));
+    for (size_t i = 0; i < targets.size(); ++i)
+        switchValue->appendCase(SwitchCase(i, FrequentedBlock(targets[i]->targetBlockForBranch())));
+
+    return true;
+}
+
 bool B3IRGenerator::endBlock(ControlEntry& entry, ExpressionList& expressionStack)
 {
     ControlData& data = ""

Modified: trunk/Source/_javascript_Core/wasm/WasmFunctionParser.h (208401 => 208402)


--- trunk/Source/_javascript_Core/wasm/WasmFunctionParser.h	2016-11-04 22:12:12 UTC (rev 208401)
+++ trunk/Source/_javascript_Core/wasm/WasmFunctionParser.h	2016-11-04 22:53:21 UTC (rev 208402)
@@ -65,6 +65,8 @@
 
     bool WARN_UNUSED_RETURN popExpressionStack(ExpressionType& result);
 
+    void setErrorMessage(String&& message) { m_context.setErrorMessage(WTFMove(message)); }
+
     Context& m_context;
     ExpressionList m_expressionStack;
     Vector<ControlEntry> m_controlStack;
@@ -345,7 +347,7 @@
 
     case OpType::Else: {
         if (!m_controlStack.size()) {
-            m_context.setErrorMessage("Attempted to use else block at the top-level of a function");
+            setErrorMessage("Attempted to use else block at the top-level of a function");
             return false;
         }
 
@@ -373,6 +375,44 @@
         return m_context.addBranch(data, condition, m_expressionStack);
     }
 
+    case OpType::BrTable: {
+        uint32_t numberOfTargets;
+        if (!parseVarUInt32(numberOfTargets))
+            return false;
+
+        Vector<ControlType*> targets;
+        if (!targets.tryReserveCapacity(numberOfTargets))
+            return false;
+
+        for (uint32_t i = 0; i < numberOfTargets; ++i) {
+            uint32_t target;
+            if (!parseVarUInt32(target))
+                return false;
+
+            if (target >= m_controlStack.size())
+                return false;
+
+            targets.uncheckedAppend(&m_controlStack[m_controlStack.size() - 1 - target].controlData);
+        }
+
+        uint32_t defaultTarget;
+        if (!parseVarUInt32(defaultTarget))
+            return false;
+
+        if (defaultTarget >= m_controlStack.size())
+            return false;
+
+        ExpressionType condition;
+        if (!popExpressionStack(condition))
+            return false;
+        
+        if (!m_context.addSwitch(condition, targets, m_controlStack[m_controlStack.size() - 1 - defaultTarget].controlData, m_expressionStack))
+            return false;
+
+        m_unreachableBlocks = 1;
+        return true;
+    }
+
     case OpType::Return: {
         return addReturn();
     }
@@ -394,7 +434,6 @@
     }
 
     case OpType::Select:
-    case OpType::BrTable:
     case OpType::Nop:
     case OpType::Drop:
     case OpType::TeeLocal:
@@ -453,7 +492,6 @@
     }
 
     // one immediate cases
-    case OpType::Return:
     case OpType::F32Const:
     case OpType::I32Const:
     case OpType::F64Const:
@@ -478,7 +516,7 @@
         return true;
     }
 
-    m_context.setErrorMessage("Attempted to use a stack value when none existed");
+    setErrorMessage("Attempted to use a stack value when none existed");
     return false;
 }
 

Modified: trunk/Source/_javascript_Core/wasm/WasmValidate.cpp (208401 => 208402)


--- trunk/Source/_javascript_Core/wasm/WasmValidate.cpp	2016-11-04 22:12:12 UTC (rev 208401)
+++ trunk/Source/_javascript_Core/wasm/WasmValidate.cpp	2016-11-04 22:53:21 UTC (rev 208402)
@@ -102,7 +102,8 @@
     bool WARN_UNUSED_RETURN addElseToUnreachable(ControlData&);
 
     bool WARN_UNUSED_RETURN addReturn(const ExpressionList& returnValues);
-    bool WARN_UNUSED_RETURN addBranch(ControlData&, ExpressionType condition, const ExpressionList& returnValues);
+    bool WARN_UNUSED_RETURN addBranch(ControlData&, ExpressionType condition, const ExpressionList& expressionStack);
+    bool WARN_UNUSED_RETURN addSwitch(ExpressionType condition, const Vector<ControlData*>& targets, ControlData& defaultTarget, const ExpressionList& expressionStack);
     bool WARN_UNUSED_RETURN endBlock(ControlEntry&, ExpressionList& expressionStack);
     bool WARN_UNUSED_RETURN addEndToUnreachable(ControlEntry&);
 
@@ -122,6 +123,8 @@
     bool unify(Type, Type);
     bool unify(const ExpressionList&, const ControlData&);
 
+    bool checkBranchTarget(ControlData& target, const ExpressionList& expressionStack);
+
     ExpressionType m_returnType;
     Vector<Type> m_locals;
     String m_errorMessage;
@@ -223,26 +226,52 @@
     return false;
 }
 
+bool Validate::checkBranchTarget(ControlType& target, const ExpressionList& expressionStack)
+    {
+        if (target.type() == BlockType::Loop)
+            return true;
+
+        if (target.signature() == Void)
+            return true;
+
+        if (!expressionStack.size()) {
+            m_errorMessage = makeString("Attempting to branch to block with expected type: ", toString(target.signature()), " but the stack was empty");
+            return false;
+        }
+
+        if (target.signature() == expressionStack.last())
+            return true;
+
+        m_errorMessage = makeString("Attempting to branch to block with expected type: ", toString(target.signature()), " but stack has type: ", toString(target.signature()));
+        return false;
+    }
+
 bool Validate::addBranch(ControlType& target, ExpressionType condition, const ExpressionList& stack)
 {
-    // Void means this is not a conditional branch.
+    // Void means this is an unconditional branch.
     if (condition != Void && condition != I32) {
         m_errorMessage = makeString("Attempting to add a conditional branch with condition type: ", toString(condition), " but expected i32.");
         return false;
     }
 
-    if (target.type() == BlockType::Loop)
-        return true;
+    return checkBranchTarget(target, stack);
+}
 
-    if (target.signature() == Void)
-        return true;
+bool Validate::addSwitch(ExpressionType condition, const Vector<ControlData*>& targets, ControlData& defaultTarget, const ExpressionList& expressionStack)
+{
+    if (condition != I32) {
+        m_errorMessage = makeString("Attempting to add a br_table with condition type: ", toString(condition), " but expected i32.");
+        return false;
+    }
 
-    ASSERT(stack.size() == 1);
-    if (target.signature() == stack[0])
-        return true;
+    for (auto target : targets) {
+        if (defaultTarget.signature() != target->signature()) {
+            m_errorMessage = makeString("Attempting to add a br_table with different expected types. Default target has type: ", toString(defaultTarget.signature()), " but case has type: ", toString(target->signature()));
+            return false;
+        }
+    }
 
-    m_errorMessage = makeString("Attempting to branch to block with expected type: ", toString(target.signature()), " but branching with type: ", toString(target.signature()));
-    return false;
+    return checkBranchTarget(defaultTarget, expressionStack);
 }
 
 bool Validate::endBlock(ControlEntry& entry, ExpressionList& stack)
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to