No effects for string builtins
diff --git a/src/ir/CMakeLists.txt b/src/ir/CMakeLists.txt
index 9190697..11c80fd 100644
--- a/src/ir/CMakeLists.txt
+++ b/src/ir/CMakeLists.txt
@@ -23,6 +23,7 @@
   runtime-global.cpp
   runtime-table.cpp
   stack-utils.cpp
+  string-builtin-names.cpp
   table-utils.cpp
   type-updating.cpp
   module-splitting.cpp
diff --git a/src/ir/string-builtin-names.cpp b/src/ir/string-builtin-names.cpp
new file mode 100644
index 0000000..f0cbd99
--- /dev/null
+++ b/src/ir/string-builtin-names.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2026 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.
+ */
+
+#include "ir/string-builtin-names.h"
+
+namespace wasm {
+
+namespace StringBuiltins {
+
+const ImportNames fromCharCodeArray{"wasm:js-string", "fromCharCodeArray"};
+const ImportNames fromCodePoint{"wasm:js-string", "fromCodePoint"};
+const ImportNames concat{"wasm:js-string", "concat"};
+const ImportNames intoCharCodeArray{"wasm:js-string", "intoCharCodeArray"};
+const ImportNames equals{"wasm:js-string", "equals"};
+const ImportNames test{"wasm:js-string", "test"};
+const ImportNames compare{"wasm:js-string", "compare"};
+const ImportNames length{"wasm:js-string", "length"};
+const ImportNames charCodeAt{"wasm:js-string", "charCodeAt"};
+const ImportNames substring{"wasm:js-string", "substring"};
+
+const std::array<ImportNames, 10> allBuiltins = {fromCharCodeArray,
+                                                 fromCodePoint,
+                                                 concat,
+                                                 intoCharCodeArray,
+                                                 equals,
+                                                 test,
+                                                 compare,
+                                                 length,
+                                                 charCodeAt,
+                                                 substring};
+
+} // namespace StringBuiltins
+
+} // namespace wasm
diff --git a/src/ir/string-builtin-names.h b/src/ir/string-builtin-names.h
new file mode 100644
index 0000000..6db5e07
--- /dev/null
+++ b/src/ir/string-builtin-names.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 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.
+ */
+
+#ifndef wasm_ir_string_builtin_names_h
+#define wasm_ir_string_builtin_names_h
+
+#include "ir/import-names.h"
+#include <array>
+
+namespace wasm {
+
+namespace StringBuiltins {
+
+extern const ImportNames fromCharCodeArray;
+extern const ImportNames fromCodePoint;
+extern const ImportNames concat;
+extern const ImportNames intoCharCodeArray;
+extern const ImportNames equals;
+extern const ImportNames test;
+extern const ImportNames compare;
+extern const ImportNames length;
+extern const ImportNames charCodeAt;
+extern const ImportNames substring;
+
+extern const std::array<ImportNames, 10> allBuiltins;
+
+} // namespace StringBuiltins
+
+} // namespace wasm
+
+#endif // wasm_ir_string_builtin_names_h
diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp
index ca82b2b..cdccd7c 100644
--- a/src/passes/GlobalEffects.cpp
+++ b/src/passes/GlobalEffects.cpp
@@ -21,6 +21,7 @@
 
 #include "ir/effects.h"
 #include "ir/module-utils.h"
+#include "ir/string-builtin-names.h"
 #include "pass.h"
 #include "support/graph_traversal.h"
 #include "support/strongly_connected_components.h"
@@ -50,6 +51,14 @@
   ModuleUtils::ParallelFunctionAnalysis<FuncInfo> analysis(
     module, [&](Function* func, FuncInfo& funcInfo) {
       if (func->imported()) {
+        ImportNames importName{func->module, func->base};
+        auto& allBuiltins = StringBuiltins::allBuiltins;
+        if (std::find(allBuiltins.begin(), allBuiltins.end(), importName) !=
+            allBuiltins.end()) {
+          funcInfo.effects.emplace(passOptions, module);
+          return;
+        }
+
         // Imports can do anything, so we need to assume the worst anyhow,
         // which is the same as not specifying any effects for them in the
         // map (which we do by not setting funcInfo.effects).
diff --git a/src/passes/StringLowering.cpp b/src/passes/StringLowering.cpp
index bd753ef..df86caf 100644
--- a/src/passes/StringLowering.cpp
+++ b/src/passes/StringLowering.cpp
@@ -37,6 +37,7 @@
 
 #include "ir/module-utils.h"
 #include "ir/names.h"
+#include "ir/string-builtin-names.h"
 #include "ir/type-updating.h"
 #include "ir/utils.h"
 #include "pass.h"
@@ -355,14 +356,15 @@
 
   // Creates an imported string function, returning its name (which is equal to
   // the true name of the import, if there is no conflict).
-  Name addImport(Module* module, Name trueName, Type params, Type results) {
-    auto name = Names::getValidFunctionName(*module, trueName);
+  Name
+  addImport(Module* module, ImportNames importName, Type params, Type results) {
+    auto name = Names::getValidFunctionName(*module, importName.name);
     auto sig = Signature(params, results);
     Builder builder(*module);
     auto* func = module->addFunction(
       builder.makeFunction(name, Type(sig, NonNullable, Inexact), {}));
-    func->module = WasmStringsModule;
-    func->base = trueName;
+    func->module = importName.module;
+    func->base = importName.name;
     return name;
   }
 
@@ -371,31 +373,40 @@
     // parallel work. Optimizations can remove unneeded ones later.
 
     // string.fromCharCodeArray: array, start, end -> ext
-    fromCharCodeArrayImport = addImport(
-      module, "fromCharCodeArray", {nullArray16, Type::i32, Type::i32}, nnExt);
+    fromCharCodeArrayImport = addImport(module,
+                                        StringBuiltins::fromCharCodeArray,
+                                        {nullArray16, Type::i32, Type::i32},
+                                        nnExt);
     // string.fromCodePoint: codepoint -> ext
-    fromCodePointImport = addImport(module, "fromCodePoint", Type::i32, nnExt);
+    fromCodePointImport =
+      addImport(module, StringBuiltins::fromCodePoint, Type::i32, nnExt);
     // string.concat: string, string -> string
-    concatImport = addImport(module, "concat", {nullExt, nullExt}, nnExt);
+    concatImport =
+      addImport(module, StringBuiltins::concat, {nullExt, nullExt}, nnExt);
     // string.intoCharCodeArray: string, array, start -> num written
     intoCharCodeArrayImport = addImport(module,
-                                        "intoCharCodeArray",
+                                        StringBuiltins::intoCharCodeArray,
                                         {nullExt, nullArray16, Type::i32},
                                         Type::i32);
     // string.equals: string, string -> i32
-    equalsImport = addImport(module, "equals", {nullExt, nullExt}, Type::i32);
+    equalsImport =
+      addImport(module, StringBuiltins::equals, {nullExt, nullExt}, Type::i32);
     // string.test: externref -> i32
-    testImport = addImport(module, "test", {nullExt}, Type::i32);
+    testImport = addImport(module, StringBuiltins::test, {nullExt}, Type::i32);
     // string.compare: string, string -> i32
-    compareImport = addImport(module, "compare", {nullExt, nullExt}, Type::i32);
+    compareImport =
+      addImport(module, StringBuiltins::compare, {nullExt, nullExt}, Type::i32);
     // string.length: string -> i32
-    lengthImport = addImport(module, "length", nullExt, Type::i32);
+    lengthImport =
+      addImport(module, StringBuiltins::length, nullExt, Type::i32);
     // string.codePointAt: string, offset -> i32
-    charCodeAtImport =
-      addImport(module, "charCodeAt", {nullExt, Type::i32}, Type::i32);
+    charCodeAtImport = addImport(
+      module, StringBuiltins::charCodeAt, {nullExt, Type::i32}, Type::i32);
     // string.substring: string, start, end -> string
-    substringImport =
-      addImport(module, "substring", {nullExt, Type::i32, Type::i32}, nnExt);
+    substringImport = addImport(module,
+                                StringBuiltins::substring,
+                                {nullExt, Type::i32, Type::i32},
+                                nnExt);
 
     // Replace the string instructions in parallel.
     struct Replacer : public WalkerPass<PostWalker<Replacer>> {