Skip to content

Commit 8faada6

Browse files
authored
[ctor-eval] Partial evaluation (#4438)
This lets us eval part of a function but not all, which is necessary to handle real-world things like __wasm_call_ctors in LLVM output, as that is the single ctor that is exported and it has calls to the actual ctors. To do so, we look for a toplevel block and execute its items one by one, in a FunctionScope. If we stop in the middle, then we are performing a partial eval. In that case, we only remove the parts of the function that we removed, and we also serialize the locals whose values we read from the FunctionScope. For example, consider this: function foo() { return 10; } function __wasm_call_ctors() { var x; x = foo(); x++; // We stop evalling here. import1(); import2(x); } We can eval x = foo() and x++, but we must stop evalling when we reach the first of those imports. The partially-evalled function then looks like this: function __wasm_call_ctors() { var x; x = 11; import1(); import2(x); } That is, we evalled two lines of executing code and simply removed them, and then we wrote out the value of the local at that point, and then the rest of the code in the function is as it used to be.
1 parent 7796031 commit 8faada6

22 files changed

+430
-49
lines changed

src/tools/wasm-ctor-eval.cpp

Lines changed: 179 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
#include "ir/import-utils.h"
3030
#include "ir/literal-utils.h"
3131
#include "ir/memory-utils.h"
32-
#include "ir/module-utils.h"
32+
#include "ir/names.h"
3333
#include "pass.h"
3434
#include "support/colors.h"
3535
#include "support/file.h"
@@ -41,11 +41,17 @@
4141

4242
using namespace wasm;
4343

44+
namespace {
45+
4446
struct FailToEvalException {
4547
std::string why;
4648
FailToEvalException(std::string why) : why(why) {}
4749
};
4850

51+
// The prefix for a recommendation, so it is aligned properly with the rest of
52+
// the output.
53+
#define RECOMMENDATION "\n recommendation: "
54+
4955
// We do not have access to imported globals
5056
class EvallingGlobalManager {
5157
// values of globals
@@ -66,8 +72,9 @@ class EvallingGlobalManager {
6672
if (dangerousGlobals.count(name) > 0) {
6773
std::string extra;
6874
if (name == "___dso_handle") {
69-
extra = "\nrecommendation: build with -s NO_EXIT_RUNTIME=1 so that "
70-
"calls to atexit that use ___dso_handle are not emitted";
75+
extra = RECOMMENDATION
76+
"build with -s NO_EXIT_RUNTIME=1 so that "
77+
"calls to atexit that use ___dso_handle are not emitted";
7178
}
7279
throw FailToEvalException(
7380
std::string(
@@ -302,10 +309,10 @@ struct CtorEvalExternalInterface : EvallingModuleInstance::ExternalInterface {
302309

303310
std::string extra;
304311
if (import->module == ENV && import->base == "___cxa_atexit") {
305-
extra = "\nrecommendation: build with -s NO_EXIT_RUNTIME=1 so that calls "
306-
"to atexit are not emitted";
312+
extra = RECOMMENDATION "build with -s NO_EXIT_RUNTIME=1 so that calls "
313+
"to atexit are not emitted";
307314
} else if (import->module == WASI && !ignoreExternalInput) {
308-
extra = "\nrecommendation: consider --ignore-external-input";
315+
extra = RECOMMENDATION "consider --ignore-external-input";
309316
}
310317
throw FailToEvalException(std::string("call import: ") +
311318
import->module.str + "." + import->base.str +
@@ -475,6 +482,163 @@ struct CtorEvalExternalInterface : EvallingModuleInstance::ExternalInterface {
475482
}
476483
};
477484

485+
// Eval a single ctor function. Returns whether we succeeded to completely
486+
// evaluate the ctor, which means that the caller can proceed to try to eval
487+
// further ctors if there are any.
488+
bool evalCtor(EvallingModuleInstance& instance,
489+
CtorEvalExternalInterface& interface,
490+
Name funcName,
491+
Name exportName) {
492+
auto& wasm = instance.wasm;
493+
auto* func = wasm.getFunction(funcName);
494+
495+
// We don't know the values of parameters, so give up if there are any.
496+
// TODO: Maybe use ignoreExternalInput?
497+
if (func->getNumParams() > 0) {
498+
std::cout << " ...stopping due to params\n";
499+
return false;
500+
}
501+
502+
// TODO: Handle a return value by emitting a proper constant.
503+
if (func->getResults() != Type::none) {
504+
std::cout << " ...stopping due to results\n";
505+
return false;
506+
}
507+
508+
// We want to handle the form of the global constructor function in LLVM. That
509+
// looks like this:
510+
//
511+
// (func $__wasm_call_ctors
512+
// (call $ctor.1)
513+
// (call $ctor.2)
514+
// (call $ctor.3)
515+
// )
516+
//
517+
// Some of those ctors may be inlined, however, which would mean that the
518+
// function could have locals, control flow, etc. However, we assume for now
519+
// that it does not have parameters at least (whose values we can't tell),
520+
// or results. And for now we look for a toplevel block and process its
521+
// children one at a time. This allows us to eval some of the $ctor.*
522+
// functions (or their inlined contents) even if not all.
523+
//
524+
// TODO: Support complete partial evalling, that is, evaluate parts of an
525+
// arbitrary function, and not just a sequence in a single toplevel
526+
// block.
527+
528+
if (auto* block = func->body->dynCast<Block>()) {
529+
// Go through the items in the block and try to execute them. We do all this
530+
// in a single function scope for all the executions.
531+
EvallingModuleInstance::FunctionScope scope(func, LiteralList());
532+
533+
EvallingModuleInstance::RuntimeExpressionRunner expressionRunner(
534+
instance, scope, instance.maxDepth);
535+
536+
// After we successfully eval a line we will apply the changes here. This is
537+
// the same idea as applyToModule() - we must only do it after an entire
538+
// atomic "chunk" has been processed, we do not want partial updates from
539+
// an item in the block that we only partially evalled.
540+
EvallingModuleInstance::FunctionScope appliedScope(func, LiteralList());
541+
542+
Index successes = 0;
543+
for (auto* curr : block->list) {
544+
Flow flow;
545+
try {
546+
flow = expressionRunner.visit(curr);
547+
} catch (FailToEvalException& fail) {
548+
if (successes == 0) {
549+
std::cout << " ...stopping (in block) since could not eval: "
550+
<< fail.why << "\n";
551+
} else {
552+
std::cout << " ...partial evalling successful, but stopping since "
553+
"could not eval: "
554+
<< fail.why << "\n";
555+
}
556+
break;
557+
}
558+
559+
// So far so good! Apply the results.
560+
interface.applyToModule();
561+
appliedScope = scope;
562+
successes++;
563+
564+
if (flow.breaking()) {
565+
// We are returning out of the function (either via a return, or via a
566+
// break to |block|, which has the same outcome. That means we don't
567+
// need to execute any more lines, and can consider them to be executed.
568+
std::cout << " ...stopping in block due to break\n";
569+
570+
// Mark us as having succeeded on the entire block, since we have: we
571+
// are skipping the rest, which means there is no problem there. We must
572+
// set this here so that lower down we realize that we've evalled
573+
// everything.
574+
successes = block->list.size();
575+
break;
576+
}
577+
}
578+
579+
if (successes > 0 && successes < block->list.size()) {
580+
// We managed to eval some but not all. That means we can't just remove
581+
// the entire function, but need to keep parts of it - the parts we have
582+
// not evalled - around. To do so, we create a copy of the function with
583+
// the partially-evalled contents and make the export use that (as the
584+
// function may be used in other places than the export, which we do not
585+
// want to affect).
586+
auto copyName = Names::getValidFunctionName(wasm, funcName);
587+
auto* copyFunc = ModuleUtils::copyFunction(func, wasm, copyName);
588+
wasm.getExport(exportName)->value = copyName;
589+
590+
// Remove the items we've evalled.
591+
Builder builder(wasm);
592+
auto* copyBlock = copyFunc->body->cast<Block>();
593+
for (Index i = 0; i < successes; i++) {
594+
copyBlock->list[i] = builder.makeNop();
595+
}
596+
597+
// Write out the values of locals, that is the local state after evalling
598+
// the things we've just nopped. For simplicity we just write out all of
599+
// locals, and leave it to the optimizer to remove redundant or
600+
// unnecessary operations.
601+
std::vector<Expression*> localSets;
602+
for (Index i = 0; i < copyFunc->getNumLocals(); i++) {
603+
auto value = appliedScope.locals[i];
604+
localSets.push_back(
605+
builder.makeLocalSet(i, builder.makeConstantExpression(value)));
606+
}
607+
608+
// Put the local sets at the front of the block. We know there must be a
609+
// nop in that position (since we've evalled at least one item in the
610+
// block, and replaced it with a nop), so we can overwrite it.
611+
copyBlock->list[0] = builder.makeBlock(localSets);
612+
613+
// Interesting optimizations may be possible both due to removing some but
614+
// not all of the code, and due to the locals we just added.
615+
PassRunner passRunner(&wasm,
616+
PassOptions::getWithDefaultOptimizationOptions());
617+
passRunner.addDefaultFunctionOptimizationPasses();
618+
passRunner.runOnFunction(copyFunc);
619+
}
620+
621+
// Return true if we evalled the entire block. Otherwise, even if we evalled
622+
// some of it, the caller must stop trying to eval further things.
623+
return successes == block->list.size();
624+
}
625+
626+
// Otherwise, we don't recognize a pattern that allows us to do partial
627+
// evalling. So simply call the entire function at once and see if we can
628+
// optimize that.
629+
try {
630+
instance.callFunction(funcName, LiteralList());
631+
} catch (FailToEvalException& fail) {
632+
std::cout << " ...stopping since could not eval: " << fail.why << "\n";
633+
return false;
634+
}
635+
636+
// Success! Apply the results.
637+
interface.applyToModule();
638+
return true;
639+
}
640+
641+
// Eval all ctors in a module.
478642
void evalCtors(Module& wasm, std::vector<std::string> ctors) {
479643
std::map<Name, std::shared_ptr<EvallingModuleInstance>> linkedInstances;
480644

@@ -505,26 +669,15 @@ void evalCtors(Module& wasm, std::vector<std::string> ctors) {
505669
if (!ex) {
506670
Fatal() << "export not found: " << ctor;
507671
}
508-
try {
509-
instance.callFunction(ex->value, LiteralList());
510-
} catch (FailToEvalException& fail) {
511-
// that's it, we failed, so stop here, cleaning up partial
512-
// memory changes first
513-
std::cout << " ...stopping since could not eval: " << fail.why << "\n";
672+
auto funcName = ex->value;
673+
if (!evalCtor(instance, interface, funcName, ctor)) {
674+
std::cout << " ...stopping\n";
514675
return;
515676
}
516-
std::cout << " ...success on " << ctor << ".\n";
517677

518-
// Success, the entire function was evalled! Apply the results of
519-
// execution to the module.
520-
interface.applyToModule();
521-
522-
// we can nop the function (which may be used elsewhere)
523-
// and remove the export
524-
auto* exp = wasm.getExport(ctor);
525-
auto* func = wasm.getFunction(exp->value);
526-
func->body = wasm.allocator.alloc<Nop>();
527-
wasm.removeExport(exp->name);
678+
// Success! Remove the export, and continue.
679+
std::cout << " ...success on " << ctor << ".\n";
680+
wasm.removeExport(ctor);
528681
}
529682
} catch (FailToEvalException& fail) {
530683
// that's it, we failed to even create the instance
@@ -534,6 +687,8 @@ void evalCtors(Module& wasm, std::vector<std::string> ctors) {
534687
}
535688
}
536689

690+
} // anonymous namespace
691+
537692
//
538693
// main
539694
//
@@ -629,6 +784,7 @@ int main(int argc, const char* argv[]) {
629784
{
630785
PassRunner passRunner(&wasm);
631786
passRunner.add("memory-packing"); // we flattened it, so re-optimize
787+
// TODO: just do -Os for the one function
632788
passRunner.add("remove-unused-names");
633789
passRunner.add("dce");
634790
passRunner.add("merge-blocks");

src/wasm-interpreter.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2551,7 +2551,7 @@ template<typename GlobalManager, typename SubType> class ModuleInstanceBase {
25512551

25522552
private:
25532553
// Keep a record of call depth, to guard against excessive recursion.
2554-
size_t callDepth;
2554+
size_t callDepth = 0;
25552555

25562556
// Function name stack. We maintain this explicitly to allow printing of
25572557
// stack traces.
@@ -2653,6 +2653,7 @@ template<typename GlobalManager, typename SubType> class ModuleInstanceBase {
26532653
}
26542654
}
26552655

2656+
public:
26562657
class FunctionScope {
26572658
public:
26582659
std::vector<Literals> locals;
@@ -3553,7 +3554,6 @@ template<typename GlobalManager, typename SubType> class ModuleInstanceBase {
35533554
}
35543555
};
35553556

3556-
public:
35573557
// Call a function, starting an invocation.
35583558
Literals callFunction(Name name, const LiteralList& arguments) {
35593559
// if the last call ended in a jump up the stack, it might have left stuff
@@ -3609,9 +3609,11 @@ template<typename GlobalManager, typename SubType> class ModuleInstanceBase {
36093609
return flow.values;
36103610
}
36113611

3612+
// The maximum call stack depth to evaluate into.
3613+
static const Index maxDepth = 250;
3614+
36123615
protected:
36133616
Address memorySize; // in pages
3614-
static const Index maxDepth = 250;
36153617

36163618
void trapIfGt(uint64_t lhs, uint64_t rhs, const char* msg) {
36173619
if (lhs > rhs) {

test/ctor-eval/no_partial.wast

Lines changed: 0 additions & 10 deletions
This file was deleted.

test/ctor-eval/no_partial.wast.out

Lines changed: 0 additions & 13 deletions
This file was deleted.

test/ctor-eval/params.wast

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(module
2+
(func "test1" (param $x i32)
3+
;; The presence of params stops us from evalling this function (at least
4+
;; for now).
5+
(nop)
6+
)
7+
)

test/ctor-eval/params.wast.out

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(module
2+
(type $i32_=>_none (func (param i32)))
3+
(export "test1" (func $0))
4+
(func $0 (param $x i32)
5+
(nop)
6+
)
7+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
(module
2+
(import "import" "import" (func $import (param i32 i32)))
3+
4+
(memory 256 256)
5+
(data (i32.const 10) "_________________")
6+
7+
(export "test1" $test1)
8+
9+
(func $test1
10+
(local $temp i32)
11+
12+
;; Increment $temp from 0 to 1, which we can eval.
13+
(local.set $temp
14+
(i32.add
15+
(local.get $temp)
16+
(i32.const 1)
17+
)
18+
)
19+
20+
;; A safe store that will be evalled and alter memory.
21+
(i32.store8 (i32.const 12) (i32.const 115))
22+
23+
;; A call to an import, which prevents evalling. We will stop here. The
24+
;; 'tee' instruction should *not* have any effect, that is, we should not
25+
;; partially eval this line in the block - we should eval none of it.
26+
;; TODO: We should support such partial line evalling, with more careful
27+
;; management of locals.
28+
(call $import
29+
(local.get $temp) ;; The value sent here should be '1'.
30+
(local.tee $temp
31+
(i32.const 50)
32+
)
33+
)
34+
35+
;; A safe store that we never reach
36+
(i32.store8 (i32.const 13) (i32.const 115))
37+
)
38+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test1

0 commit comments

Comments
 (0)