Skip to content

Commit

Permalink
StringLowering pass (WebAssembly#6271)
Browse files Browse the repository at this point in the history
This extends StringGathering by replacing the gathered string globals to imported
globals. It adds a custom section with the strings that the imports are expected to
provide. It also replaces the string type with extern.

This is a complete lowering of strings, except for string operations that are a TODO.

After running this, no strings remain in the wasm, and the outside JS is expected
to provide the proper imports, which it can do by processing the JSON of the
strings in the custom section "string.consts", which looks like

["foo", "bar", ..]

That is, an array of strings, which are imported as

(import "string.const" "0" (global $string.const_foo (ref extern))) ;; foo
(import "string.const" "1" (global $string.const_bar (ref extern))) ;; bar
  • Loading branch information
kripken authored and radekdoulik committed Jul 12, 2024
1 parent bc0e990 commit 1bd9b89
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 4 deletions.
64 changes: 60 additions & 4 deletions src/passes/StringLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@
// globals, avoiding them appearing in code that can run more than once (which
// can have overhead in VMs).
//
// Building on that, an extended version of StringGathering will also replace
// those new globals with imported globals of type externref, for use with the
// string imports proposal. String operations will likewise need to be lowered.
// TODO
// StringLowering does the same, and also replaces those new globals with
// imported globals of type externref, for use with the string imports proposal.
// String operations will likewise need to be lowered. TODO
//

#include <algorithm>

#include "ir/module-utils.h"
#include "ir/names.h"
#include "ir/type-updating.h"
#include "pass.h"
#include "support/json.h"
#include "wasm-builder.h"
#include "wasm.h"

Expand Down Expand Up @@ -175,6 +176,61 @@ struct StringGathering : public Pass {
}
};

struct StringLowering : public StringGathering {
void run(Module* module) override {
if (!module->features.has(FeatureSet::Strings)) {
return;
}

// First, run the gathering operation so all string.consts are in one place.
StringGathering::run(module);

// Lower the string.const globals into imports.
makeImports(module);

// Remove all HeapType::string etc. in favor of externref.
updateTypes(module);

// Disable the feature here after we lowered everything away.
module->features.disable(FeatureSet::Strings);
}

void makeImports(Module* module) {
Index importIndex = 0;
json::Value stringArray;
stringArray.setArray();
std::vector<Name> importedStrings;
for (auto& global : module->globals) {
if (global->init) {
if (auto* c = global->init->dynCast<StringConst>()) {
global->module = "string.const";
global->base = std::to_string(importIndex);
importIndex++;
global->init = nullptr;

auto str = json::Value::make(std::string(c->string.str).c_str());
stringArray.push_back(str);
}
}
}

// Add a custom section with the JSON.
std::stringstream stream;
stringArray.stringify(stream);
auto str = stream.str();
auto vec = std::vector<char>(str.begin(), str.end());
module->customSections.emplace_back(
CustomSection{"string.consts", std::move(vec)});
}

void updateTypes(Module* module) {
TypeMapper::TypeUpdates updates;
updates[HeapType::string] = HeapType::ext;
TypeMapper(*module, updates).map();
}
};

Pass* createStringGatheringPass() { return new StringGathering(); }
Pass* createStringLoweringPass() { return new StringLowering(); }

} // namespace wasm
3 changes: 3 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ void PassRegistry::registerPasses() {
registerPass("string-gathering",
"gathers wasm strings to globals",
createStringGatheringPass);
registerPass("string-lowering",
"lowers wasm strings and operations to imports",
createStringLoweringPass);
registerPass(
"strip", "deprecated; same as strip-debug", createStripDebugPass);
registerPass("stack-check",
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Pass* createSimplifyLocalsNoStructurePass();
Pass* createSimplifyLocalsNoTeeNoStructurePass();
Pass* createStackCheckPass();
Pass* createStringGatheringPass();
Pass* createStringLoweringPass();
Pass* createStripDebugPass();
Pass* createStripDWARFPass();
Pass* createStripProducersPass();
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,9 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --string-gathering gathers wasm strings to globals
;; CHECK-NEXT:
;; CHECK-NEXT: --string-lowering lowers wasm strings and
;; CHECK-NEXT: operations to imports
;; CHECK-NEXT:
;; CHECK-NEXT: --strip deprecated; same as strip-debug
;; CHECK-NEXT:
;; CHECK-NEXT: --strip-debug strip debug info (including the
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --string-gathering gathers wasm strings to globals
;; CHECK-NEXT:
;; CHECK-NEXT: --string-lowering lowers wasm strings and
;; CHECK-NEXT: operations to imports
;; CHECK-NEXT:
;; CHECK-NEXT: --strip deprecated; same as strip-debug
;; CHECK-NEXT:
;; CHECK-NEXT: --strip-debug strip debug info (including the
Expand Down
46 changes: 46 additions & 0 deletions test/lit/passes/string-gathering.wast
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.

;; RUN: foreach %s %t wasm-opt --string-gathering -all -S -o - | filecheck %s
;; RUN: foreach %s %t wasm-opt --string-lowering -all -S -o - | filecheck %s --check-prefix=LOWER

;; All the strings should be collected into globals and used from there. They
;; should also be sorted deterministically (alphabetically).
;;
;; LOWER also lowers away strings entirely, leaving only imports and a custom
;; section (that part is tested in string-lowering.wast). It also removes all
;; uses of the string heap type, leaving extern instead for the imported
;; strings.

(module
;; Note that $global will be reused: no new global will be added for "foo".
Expand All @@ -19,6 +25,15 @@
(global $global (ref string) (string.const "foo"))

;; CHECK: (global $global2 stringref (global.get $string.const_bar))
;; LOWER: (type $0 (func))

;; LOWER: (import "string.const" "0" (global $string.const_bar (ref extern)))

;; LOWER: (import "string.const" "1" (global $string.const_other (ref extern)))

;; LOWER: (import "string.const" "2" (global $global (ref extern)))

;; LOWER: (global $global2 externref (global.get $string.const_bar))
(global $global2 (ref null string) (string.const "bar"))

;; CHECK: (func $a (type $0)
Expand All @@ -29,6 +44,14 @@
;; CHECK-NEXT: (global.get $global)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; LOWER: (func $a (type $0)
;; LOWER-NEXT: (drop
;; LOWER-NEXT: (global.get $string.const_bar)
;; LOWER-NEXT: )
;; LOWER-NEXT: (drop
;; LOWER-NEXT: (global.get $global)
;; LOWER-NEXT: )
;; LOWER-NEXT: )
(func $a
(drop
(string.const "bar")
Expand All @@ -52,6 +75,20 @@
;; CHECK-NEXT: (global.get $global2)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; LOWER: (func $b (type $0)
;; LOWER-NEXT: (drop
;; LOWER-NEXT: (global.get $string.const_bar)
;; LOWER-NEXT: )
;; LOWER-NEXT: (drop
;; LOWER-NEXT: (global.get $string.const_other)
;; LOWER-NEXT: )
;; LOWER-NEXT: (drop
;; LOWER-NEXT: (global.get $global)
;; LOWER-NEXT: )
;; LOWER-NEXT: (drop
;; LOWER-NEXT: (global.get $global2)
;; LOWER-NEXT: )
;; LOWER-NEXT: )
(func $b
(drop
(string.const "bar")
Expand All @@ -74,23 +111,32 @@
;; Multiple possible reusable globals. Also test ignoring of imports.
(module
;; CHECK: (import "a" "b" (global $import (ref string)))
;; LOWER: (import "a" "b" (global $import (ref extern)))
(import "a" "b" (global $import (ref string)))

;; CHECK: (global $global1 (ref string) (string.const "foo"))
(global $global1 (ref string) (string.const "foo"))

;; CHECK: (global $global2 (ref string) (global.get $global1))
;; LOWER: (import "string.const" "0" (global $global1 (ref extern)))

;; LOWER: (import "string.const" "1" (global $global4 (ref extern)))

;; LOWER: (global $global2 (ref extern) (global.get $global1))
(global $global2 (ref string) (string.const "foo"))

;; CHECK: (global $global3 (ref string) (global.get $global1))
;; LOWER: (global $global3 (ref extern) (global.get $global1))
(global $global3 (ref string) (string.const "foo"))

;; CHECK: (global $global4 (ref string) (string.const "bar"))
(global $global4 (ref string) (string.const "bar"))

;; CHECK: (global $global5 (ref string) (global.get $global4))
;; LOWER: (global $global5 (ref extern) (global.get $global4))
(global $global5 (ref string) (string.const "bar"))

;; CHECK: (global $global6 (ref string) (global.get $global4))
;; LOWER: (global $global6 (ref extern) (global.get $global4))
(global $global6 (ref string) (string.const "bar"))
)
23 changes: 23 additions & 0 deletions test/lit/passes/string-lowering.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
;; This file checks the custom section that --string-lowering adds. The other
;; operations are tested in string-gathering.wast (which is auto-updated, unlike
;; this which is manual).

;; RUN: foreach %s %t wasm-opt --string-lowering -all -S -o - | filecheck %s

(module
(func $consts
(drop
(string.const "foo")
)
(drop
(string.const "bar")
)
(drop
(string.const "foo")
)
)
)

;; The custom section should contain foo and bar, and foo only once.
;; CHECK: custom section "string.consts", size 13, contents: "[\"bar\",\"foo\"]"

0 comments on commit 1bd9b89

Please sign in to comment.