diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index 1003c63f671..113eaeaa1c0 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -621,6 +621,7 @@ ("string.measure_wtf16", "makeStringMeasure(s, StringMeasureWTF16)"), ("string.encode_wtf8", "makeStringEncode(s, StringEncodeWTF8)"), ("string.encode_wtf16", "makeStringEncode(s, StringEncodeWTF16)"), + ("string.concat", "makeStringConcat(s)"), ] diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index c3b6bcec1d0..be1ab85e7bc 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -3129,9 +3129,17 @@ switch (op[0]) { switch (op[3]) { case 'i': { switch (op[7]) { - case 'c': - if (strcmp(op, "string.const") == 0) { return makeStringConst(s); } - goto parse_error; + case 'c': { + switch (op[10]) { + case 'c': + if (strcmp(op, "string.concat") == 0) { return makeStringConcat(s); } + goto parse_error; + case 's': + if (strcmp(op, "string.const") == 0) { return makeStringConst(s); } + goto parse_error; + default: goto parse_error; + } + } case 'e': { switch (op[17]) { case '1': diff --git a/src/ir/ReFinalize.cpp b/src/ir/ReFinalize.cpp index 8acffaa342b..102d701b901 100644 --- a/src/ir/ReFinalize.cpp +++ b/src/ir/ReFinalize.cpp @@ -176,6 +176,7 @@ void ReFinalize::visitStringNew(StringNew* curr) { curr->finalize(); } void ReFinalize::visitStringConst(StringConst* curr) { curr->finalize(); } void ReFinalize::visitStringMeasure(StringMeasure* curr) { curr->finalize(); } void ReFinalize::visitStringEncode(StringEncode* curr) { curr->finalize(); } +void ReFinalize::visitStringConcat(StringConcat* curr) { curr->finalize(); } void ReFinalize::visitFunction(Function* curr) { // we may have changed the body from unreachable to none, which might be bad diff --git a/src/ir/cost.h b/src/ir/cost.h index afc091ffbd6..2dcf7c456a2 100644 --- a/src/ir/cost.h +++ b/src/ir/cost.h @@ -681,6 +681,9 @@ struct CostAnalyzer : public OverriddenVisitor { CostType visitStringEncode(StringEncode* curr) { return 6 + visit(curr->ref) + visit(curr->ptr); } + CostType visitStringConcat(StringConcat* curr) { + return 10 + visit(curr->left) + visit(curr->right); + } private: CostType nullCheckCost(Expression* ref) { diff --git a/src/ir/effects.h b/src/ir/effects.h index 8e33f65bf90..dbd1bb48f90 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -742,6 +742,10 @@ class EffectAnalyzer { // traps when ref is null or we write out of bounds. parent.implicitTrap = true; } + void visitStringConcat(StringConcat* curr) { + // traps when an input is null. + parent.implicitTrap = true; + } }; public: diff --git a/src/ir/possible-contents.cpp b/src/ir/possible-contents.cpp index c865585f1a6..21eadc50be6 100644 --- a/src/ir/possible-contents.cpp +++ b/src/ir/possible-contents.cpp @@ -689,6 +689,10 @@ struct InfoCollector // TODO: optimize when possible addRoot(curr); } + void visitStringConcat(StringConcat* curr) { + // TODO: optimize when possible + addRoot(curr); + } // TODO: Model which throws can go to which catches. For now, anything thrown // is sent to the location of that tag, and any catch of that tag can diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 49cade313f2..dc02144538a 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -2272,6 +2272,9 @@ struct PrintExpressionContents WASM_UNREACHABLE("invalid string.encode*"); } } + void visitStringConcat(StringConcat* curr) { + printMedium(o, "string.concat"); + } }; // Prints an expression in s-expr format, including both the diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 14658890e3d..6734d3bd6a4 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1144,6 +1144,7 @@ enum ASTNodes { StringMeasureWTF16 = 0x85, StringEncodeWTF8 = 0x86, StringEncodeWTF16 = 0x87, + StringConcat = 0x88, }; enum MemoryAccess { @@ -1728,6 +1729,7 @@ class WasmBinaryBuilder { bool maybeVisitStringConst(Expression*& out, uint32_t code); bool maybeVisitStringMeasure(Expression*& out, uint32_t code); bool maybeVisitStringEncode(Expression*& out, uint32_t code); + bool maybeVisitStringConcat(Expression*& out, uint32_t code); void visitSelect(Select* curr, uint8_t code); void visitReturn(Return* curr); void visitMemorySize(MemorySize* curr); diff --git a/src/wasm-builder.h b/src/wasm-builder.h index 75ebcd0a306..b9bb2f7aa6a 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -1020,6 +1020,13 @@ class Builder { ret->finalize(); return ret; } + StringConcat* makeStringConcat(Expression* left, Expression* right) { + auto* ret = wasm.allocator.alloc(); + ret->left = left; + ret->right = right; + ret->finalize(); + return ret; + } // Additional helpers diff --git a/src/wasm-delegations-fields.def b/src/wasm-delegations-fields.def index 7d9ffc37d5e..7f450b16de6 100644 --- a/src/wasm-delegations-fields.def +++ b/src/wasm-delegations-fields.def @@ -742,6 +742,13 @@ switch (DELEGATE_ID) { DELEGATE_END(StringEncode); break; } + case Expression::Id::StringConcatId: { + DELEGATE_START(StringConcat); + DELEGATE_FIELD_CHILD(StringConcat, right); + DELEGATE_FIELD_CHILD(StringConcat, left); + DELEGATE_END(StringConcat); + break; + } } #undef DELEGATE_ID diff --git a/src/wasm-delegations.def b/src/wasm-delegations.def index 5ee2c0a672a..7802c63fbad 100644 --- a/src/wasm-delegations.def +++ b/src/wasm-delegations.def @@ -89,5 +89,6 @@ DELEGATE(StringNew); DELEGATE(StringConst); DELEGATE(StringMeasure); DELEGATE(StringEncode); +DELEGATE(StringConcat); #undef DELEGATE diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index c1bd06e1cc9..05de81256ec 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -1967,6 +1967,9 @@ class ExpressionRunner : public OverriddenVisitor { Flow visitStringEncode(StringEncode* curr) { WASM_UNREACHABLE("unimplemented string.encode"); } + Flow visitStringConcat(StringConcat* curr) { + WASM_UNREACHABLE("unimplemented string.concat"); + } virtual void trap(const char* why) { WASM_UNREACHABLE("unimp"); } diff --git a/src/wasm-s-parser.h b/src/wasm-s-parser.h index 3ecde76330d..9eb4a2085c8 100644 --- a/src/wasm-s-parser.h +++ b/src/wasm-s-parser.h @@ -307,6 +307,7 @@ class SExpressionWasmBuilder { Expression* makeStringConst(Element& s); Expression* makeStringMeasure(Element& s, StringMeasureOp op); Expression* makeStringEncode(Element& s, StringEncodeOp op); + Expression* makeStringConcat(Element& s); // Helper functions Type parseOptionalResultType(Element& s, Index& i); diff --git a/src/wasm.h b/src/wasm.h index 1cb0b381bce..957d2d36801 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -701,6 +701,7 @@ class Expression { StringConstId, StringMeasureId, StringEncodeId, + StringConcatId, NumExpressionIds }; Id _id; @@ -1714,6 +1715,16 @@ class StringEncode : public SpecificExpression { void finalize(); }; +class StringConcat : public SpecificExpression { +public: + StringConcat(MixedArena& allocator) {} + + Expression* left; + Expression* right; + + void finalize(); +}; + // Globals struct Named { diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 429ff7bae34..5aa5948fe85 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -3930,6 +3930,9 @@ BinaryConsts::ASTNodes WasmBinaryBuilder::readExpression(Expression*& curr) { if (maybeVisitStringEncode(curr, opcode)) { break; } + if (maybeVisitStringConcat(curr, opcode)) { + break; + } if (opcode == BinaryConsts::RefIsFunc || opcode == BinaryConsts::RefIsData || opcode == BinaryConsts::RefIsI31) { @@ -7220,6 +7223,17 @@ bool WasmBinaryBuilder::maybeVisitStringEncode(Expression*& out, return true; } +bool WasmBinaryBuilder::maybeVisitStringConcat(Expression*& out, + uint32_t code) { + if (code != BinaryConsts::StringConcat) { + return false; + } + auto* right = popNonVoidExpression(); + auto* left = popNonVoidExpression(); + out = Builder(wasm).makeStringConcat(left, right); + return true; +} + void WasmBinaryBuilder::visitRefAs(RefAs* curr, uint8_t code) { BYN_TRACE("zz node: RefAs\n"); switch (code) { diff --git a/src/wasm/wasm-s-parser.cpp b/src/wasm/wasm-s-parser.cpp index 68bd691042c..fe52864d86f 100644 --- a/src/wasm/wasm-s-parser.cpp +++ b/src/wasm/wasm-s-parser.cpp @@ -2990,6 +2990,11 @@ Expression* SExpressionWasmBuilder::makeStringEncode(Element& s, op, parseExpression(s[i]), parseExpression(s[i + 1])); } +Expression* SExpressionWasmBuilder::makeStringConcat(Element& s) { + return Builder(wasm).makeStringConcat(parseExpression(s[1]), + parseExpression(s[2])); +} + // converts an s-expression string representing binary data into an output // sequence of raw bytes this appends to data, which may already contain // content. diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 38c8f166161..6596cccc7d2 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -2300,6 +2300,10 @@ void BinaryInstWriter::visitStringEncode(StringEncode* curr) { } } +void BinaryInstWriter::visitStringConcat(StringConcat* curr) { + o << int8_t(BinaryConsts::GCPrefix) << U32LEB(BinaryConsts::StringConcat); +} + void BinaryInstWriter::emitScopeEnd(Expression* curr) { assert(!breakStack.empty()); breakStack.pop_back(); diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index d4337a87d66..d3b1c215112 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -1200,6 +1200,14 @@ void StringEncode::finalize() { } } +void StringConcat::finalize() { + if (left->type == Type::unreachable || right->type == Type::unreachable) { + type = Type::unreachable; + } else { + type = Type(HeapType::string, NonNullable); + } +} + size_t Function::getNumParams() { return getParams().size(); } size_t Function::getNumVars() { return vars.size(); } diff --git a/src/wasm2js.h b/src/wasm2js.h index 6042e2f5bc5..c6b5cbb6c41 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -2319,6 +2319,10 @@ Ref Wasm2JSBuilder::processFunctionBody(Module* m, unimplemented(curr); WASM_UNREACHABLE("unimp"); } + Ref visitStringConcat(StringConcat* curr) { + unimplemented(curr); + WASM_UNREACHABLE("unimp"); + } Ref visitRefAs(RefAs* curr) { unimplemented(curr); WASM_UNREACHABLE("unimp"); diff --git a/test/lit/strings.wast b/test/lit/strings.wast index 5afdfdbcbe2..64974d0051d 100644 --- a/test/lit/strings.wast +++ b/test/lit/strings.wast @@ -11,6 +11,8 @@ ;; CHECK: (type $none_=>_none (func)) + ;; CHECK: (type $ref?|string|_ref?|string|_=>_none (func (param stringref stringref))) + ;; CHECK: (global $string-const stringref (string.const "string in a global")) (global $string-const stringref (string.const "string in a global")) @@ -185,4 +187,21 @@ ) ) ) + + ;; CHECK: (func $string.concat (param $a stringref) (param $b stringref) + ;; CHECK-NEXT: (local.set $a + ;; CHECK-NEXT: (string.concat + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $string.concat (param $a stringref) (param $b stringref) + (local.set $a ;; validate the output is a stringref + (string.concat + (local.get $a) + (local.get $b) + ) + ) + ) )