| /* | |
| * Copyright 2025 WebAssembly Community Group participants | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| // | |
| // Lift JS string imports into wasm strings in Binaryen IR, which can then be | |
| // fully optimized. Typically StringLowering would be run later to lower them | |
| // back down. | |
| // | |
| // A pass argument allows customizing the module name for string constants: | |
| // | |
| // --pass-arg=string-constants-module@MODULE_NAME | |
| // | |
| #include "ir/utils.h" | |
| #include "pass.h" | |
| #include "passes/string-utils.h" | |
| #include "support/json.h" | |
| #include "support/string.h" | |
| #include "wasm-builder.h" | |
| #include "wasm.h" | |
| namespace wasm { | |
| struct StringLifting : public Pass { | |
| // Maps the global name of an imported string to the actual string. | |
| std::unordered_map<Name, Name> importedStrings; | |
| // Imported string functions. Imports that do not exist remain null. | |
| Name fromCharCodeArrayImport; | |
| Name intoCharCodeArrayImport; | |
| Name fromCodePointImport; | |
| Name concatImport; | |
| Name equalsImport; | |
| Name testImport; | |
| Name compareImport; | |
| Name lengthImport; | |
| Name charCodeAtImport; | |
| Name substringImport; | |
| void run(Module* module) override { | |
| // Whether we found any work to do. | |
| bool found = false; | |
| // Imported string constants look like | |
| // | |
| // (import "\'" "bar" (global $string.bar.internal.name (ref extern))) | |
| // | |
| // That is, they are imported from module "'" and the basename is the | |
| // actual string. Find them all so we can apply them. | |
| Name stringConstsModule = | |
| getArgumentOrDefault("string-constants-module", WasmStringConstsModule); | |
| for (auto& global : module->globals) { | |
| if (!global->imported()) { | |
| continue; | |
| } | |
| if (global->module == stringConstsModule) { | |
| // Encode from WTF-8 to WTF-16. | |
| auto wtf8 = global->base; | |
| std::stringstream wtf16; | |
| bool valid = String::convertWTF8ToWTF16(wtf16, wtf8.view()); | |
| if (!valid) { | |
| Fatal() << "Bad string to lift: " << wtf8; | |
| } | |
| // TODO: Use wtf16.view() once we have C++20. | |
| importedStrings[global->name] = wtf16.str(); | |
| found = true; | |
| } | |
| } | |
| // Imported strings may also be found in the string section. | |
| auto stringSectionIter = std::find_if( | |
| module->customSections.begin(), | |
| module->customSections.end(), | |
| [&](CustomSection& section) { return section.name == "string.consts"; }); | |
| if (stringSectionIter != module->customSections.end()) { | |
| // We found the string consts section. Parse it. | |
| auto& section = *stringSectionIter; | |
| auto copy = section.data; | |
| json::Value array; | |
| array.parse(copy.data(), json::Value::WTF16); | |
| if (!array.isArray()) { | |
| Fatal() << "StringLifting: string.const section should be a JSON array"; | |
| } | |
| // We have the array of constants from the section. Find globals that | |
| // refer to it. | |
| for (auto& global : module->globals) { | |
| if (!global->imported() || global->module != "string.const") { | |
| continue; | |
| } | |
| // The index in the array is the basename. | |
| Index index = std::stoi(std::string(global->base.view())); | |
| if (index >= array.size()) { | |
| Fatal() << "StringLifting: bad index in string.const section"; | |
| } | |
| auto item = array[index]; | |
| if (!item->isString()) { | |
| Fatal() | |
| << "StringLifting: string.const section entry is not a string"; | |
| } | |
| if (importedStrings.contains(global->name)) { | |
| Fatal() << "StringLifting: string.const section tramples other const"; | |
| } | |
| importedStrings[global->name] = item->getIString(); | |
| } | |
| // Remove the custom section: After lifting it has no purpose (and could | |
| // cause problems with repeated lifting/lowering). | |
| module->customSections.erase(stringSectionIter); | |
| } | |
| auto array16 = Type(Array(Field(Field::i16, Mutable)), Nullable); | |
| auto refExtern = Type(HeapType::ext, NonNullable); | |
| auto externref = Type(HeapType::ext, Nullable); | |
| auto i32 = Type::i32; | |
| // Find imported string functions. | |
| for (auto& func : module->functions) { | |
| if (!func->imported() || func->module != WasmStringsModule) { | |
| continue; | |
| } | |
| // TODO: Check exactness here too. | |
| auto type = func->type; | |
| if (func->base == "fromCharCodeArray") { | |
| if (type.getHeapType() != Signature({array16, i32, i32}, refExtern)) { | |
| Fatal() << "StringLifting: bad type for fromCharCodeArray: " << type; | |
| } | |
| fromCharCodeArrayImport = func->name; | |
| found = true; | |
| } else if (func->base == "fromCodePoint") { | |
| if (type.getHeapType() != Signature(i32, refExtern)) { | |
| Fatal() << "StringLifting: bad type for fromCodePoint: " << type; | |
| } | |
| fromCodePointImport = func->name; | |
| found = true; | |
| } else if (func->base == "concat") { | |
| if (type.getHeapType() != | |
| Signature({externref, externref}, refExtern)) { | |
| Fatal() << "StringLifting: bad type for concat: " << type; | |
| } | |
| concatImport = func->name; | |
| found = true; | |
| } else if (func->base == "intoCharCodeArray") { | |
| if (type.getHeapType() != Signature({externref, array16, i32}, i32)) { | |
| Fatal() << "StringLifting: bad type for intoCharCodeArray: " << type; | |
| } | |
| intoCharCodeArrayImport = func->name; | |
| found = true; | |
| } else if (func->base == "equals") { | |
| if (type.getHeapType() != Signature({externref, externref}, i32)) { | |
| Fatal() << "StringLifting: bad type for equals: " << type; | |
| } | |
| equalsImport = func->name; | |
| found = true; | |
| } else if (func->base == "test") { | |
| if (type.getHeapType() != Signature({externref}, i32)) { | |
| Fatal() << "StringLifting: bad type for test: " << type; | |
| } | |
| testImport = func->name; | |
| found = true; | |
| } else if (func->base == "compare") { | |
| if (type.getHeapType() != Signature({externref, externref}, i32)) { | |
| Fatal() << "StringLifting: bad type for compare: " << type; | |
| } | |
| compareImport = func->name; | |
| found = true; | |
| } else if (func->base == "length") { | |
| if (type.getHeapType() != Signature({externref}, i32)) { | |
| Fatal() << "StringLifting: bad type for length: " << type; | |
| } | |
| lengthImport = func->name; | |
| found = true; | |
| } else if (func->base == "charCodeAt") { | |
| if (type.getHeapType() != Signature({externref, i32}, i32)) { | |
| Fatal() << "StringLifting: bad type for charCodeAt: " << type; | |
| } | |
| charCodeAtImport = func->name; | |
| found = true; | |
| } else if (func->base == "substring") { | |
| if (type.getHeapType() != Signature({externref, i32, i32}, refExtern)) { | |
| Fatal() << "StringLifting: bad type for substring: " << type; | |
| } | |
| substringImport = func->name; | |
| found = true; | |
| } else { | |
| std::cerr << "warning: unknown strings import: " << func->base << '\n'; | |
| } | |
| } | |
| if (!found) { | |
| // Nothing to do. | |
| return; | |
| } | |
| struct StringApplier : public WalkerPass<PostWalker<StringApplier>> { | |
| bool isFunctionParallel() override { return true; } | |
| const StringLifting& parent; | |
| StringApplier(const StringLifting& parent) : parent(parent) {} | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<StringApplier>(parent); | |
| } | |
| bool modified = false; | |
| void visitGlobalGet(GlobalGet* curr) { | |
| // Replace global.gets of imported strings with string.const. | |
| auto iter = parent.importedStrings.find(curr->name); | |
| if (iter != parent.importedStrings.end()) { | |
| auto wtf16 = iter->second; | |
| replaceCurrent(Builder(*getModule()).makeStringConst(wtf16.view())); | |
| modified = true; | |
| } | |
| } | |
| void visitCall(Call* curr) { | |
| // Replace calls of imported string methods with stringref operations. | |
| if (curr->target == parent.fromCharCodeArrayImport) { | |
| replaceCurrent(Builder(*getModule()) | |
| .makeStringNew(StringNewWTF16Array, | |
| curr->operands[0], | |
| curr->operands[1], | |
| curr->operands[2])); | |
| } else if (curr->target == parent.fromCodePointImport) { | |
| replaceCurrent( | |
| Builder(*getModule()) | |
| .makeStringNew(StringNewFromCodePoint, curr->operands[0])); | |
| } else if (curr->target == parent.concatImport) { | |
| replaceCurrent( | |
| Builder(*getModule()) | |
| .makeStringConcat(curr->operands[0], curr->operands[1])); | |
| } else if (curr->target == parent.intoCharCodeArrayImport) { | |
| replaceCurrent(Builder(*getModule()) | |
| .makeStringEncode(StringEncodeWTF16Array, | |
| curr->operands[0], | |
| curr->operands[1], | |
| curr->operands[2])); | |
| } else if (curr->target == parent.equalsImport) { | |
| replaceCurrent(Builder(*getModule()) | |
| .makeStringEq(StringEqEqual, | |
| curr->operands[0], | |
| curr->operands[1])); | |
| } else if (curr->target == parent.testImport) { | |
| replaceCurrent( | |
| Builder(*getModule()).makeStringTest(curr->operands[0])); | |
| } else if (curr->target == parent.compareImport) { | |
| replaceCurrent(Builder(*getModule()) | |
| .makeStringEq(StringEqCompare, | |
| curr->operands[0], | |
| curr->operands[1])); | |
| } else if (curr->target == parent.lengthImport) { | |
| replaceCurrent( | |
| Builder(*getModule()) | |
| .makeStringMeasure(StringMeasureWTF16, curr->operands[0])); | |
| } else if (curr->target == parent.charCodeAtImport) { | |
| replaceCurrent( | |
| Builder(*getModule()) | |
| .makeStringWTF16Get(curr->operands[0], curr->operands[1])); | |
| } else if (curr->target == parent.substringImport) { | |
| replaceCurrent(Builder(*getModule()) | |
| .makeStringSliceWTF(curr->operands[0], | |
| curr->operands[1], | |
| curr->operands[2])); | |
| } | |
| } | |
| void visitFunction(Function* curr) { | |
| // If we made modifications then we need to refinalize, as we replace | |
| // externrefs with stringrefs, a subtype. | |
| if (modified) { | |
| ReFinalize().walkFunctionInModule(curr, getModule()); | |
| } | |
| } | |
| }; | |
| StringApplier applier(*this); | |
| applier.run(getPassRunner(), module); | |
| applier.walkModuleCode(module); | |
| // TODO: Add casts. We generate new string.* instructions, and all their | |
| // string inputs should be stringref, not externref, but we have not | |
| // converted all externrefs to stringrefs (since some externrefs might | |
| // be something else). It is not urgent to fix this as the validator | |
| // accepts externrefs there atm, and since toolchains will lower | |
| // strings out at the end anyhow (which would remove such casts). Note | |
| // that if we add a type import for stringref then this problem would | |
| // become a lot simpler (we'd convert that type to stringref). | |
| // Enable the feature so the module validates. | |
| module->features.enable(FeatureSet::Strings); | |
| } | |
| }; | |
| Pass* createStringLiftingPass() { return new StringLifting(); } | |
| } // namespace wasm |