Skip to content

Refactor expression runner so it can be used via the C and JS APIs #2702

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
132cb1d
Derive standalone expression runner from precompute pass
dcodeIO Mar 18, 2020
bbe1dc5
handle traps, format
dcodeIO Mar 18, 2020
3f3c02d
fix?
dcodeIO Mar 18, 2020
461576d
update tests
dcodeIO Mar 18, 2020
f5e3837
refactor replaceExpresion to an enum
dcodeIO Mar 19, 2020
2ccc81d
fix expressions[0] in tracing, track temporary local values
dcodeIO Mar 19, 2020
30bd10c
simplify
dcodeIO Mar 20, 2020
ca3ed47
implement preset local/global values
dcodeIO Mar 20, 2020
a853db9
could need some format on save
dcodeIO Mar 20, 2020
66d23f1
address comments
dcodeIO Mar 20, 2020
35d09c2
more documentation for getValues
dcodeIO Mar 20, 2020
f57cbdc
refactor runner to its own cpp file
dcodeIO Mar 21, 2020
63d7520
traverse into simple functions
dcodeIO Mar 21, 2020
5432156
deal with non-determinism
dcodeIO Mar 21, 2020
ec0a93d
update API, add test
dcodeIO Mar 21, 2020
1ceb5b5
retrigger CI
dcodeIO Mar 21, 2020
d145cfc
fix comment
dcodeIO Mar 23, 2020
153d10f
address review comments
dcodeIO Mar 24, 2020
7d51f30
address review comments
dcodeIO Mar 25, 2020
cabf038
Merge branch 'master' into expressionrunner
dcodeIO Apr 1, 2020
ed62e30
refactor
dcodeIO Apr 1, 2020
803cd22
reuse existing runner
dcodeIO Apr 2, 2020
597c5fa
revert trap on invalid
dcodeIO Apr 2, 2020
b4ca977
address comments
dcodeIO Apr 3, 2020
dcbab6a
address comments
dcodeIO Apr 3, 2020
4c8fc7c
address (most) comments
dcodeIO Apr 7, 2020
8586c57
mention interaction between TraverseCalls and PreserveSideEffects
dcodeIO Apr 7, 2020
1184f58
Merge branch 'master' into expressionrunner
dcodeIO Apr 15, 2020
17e5de0
address comments
dcodeIO Apr 16, 2020
84c27ac
Merge branch 'master' into expressionrunner
dcodeIO Apr 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions src/binaryen-c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ std::map<BinaryenGlobalRef, size_t> globals;
std::map<BinaryenEventRef, size_t> events;
std::map<BinaryenExportRef, size_t> exports;
std::map<RelooperBlockRef, size_t> relooperBlocks;
std::map<ExpressionRunnerRef, size_t> expressionRunners;

static bool isBasicAPIType(BinaryenType type) {
return type == BinaryenTypeAuto() || type <= Type::_last_value_type;
Expand Down Expand Up @@ -208,6 +209,26 @@ size_t noteExpression(BinaryenExpressionRef expression) {
return id;
}

// We would normally use the size of `expressionRunners` as the next index, but
// since we are going to delete runners the same address can become reused,
// which would result in unpredictable sizes (indexes) due to undefined
// behavior. Use a sequential id instead.
static size_t nextExpressionRunnerId = 0;

// Even though unlikely, it is possible that we are trying to use an id that is
// still in use after wrapping around, which we must prevent.
std::unordered_set<size_t> usedExpressionRunnerIds;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be static, too. You could get extra fancy by making both of these helpers static variables inside of noteExpressionRunner to limit their scope, but I'll leave that up to you. OTOH, it would probably be better to just say we don't support making more than max size_t expression runners and get rid of all this logic, especially since it is literally impossible to have than many expression runners recorded in expressionRunners.

Copy link
Contributor Author

@dcodeIO dcodeIO Apr 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly thinking in terms of a very long lived process using Binaryen, let's say where modules are being created as-a-service. While we can't store max size_t in the structure, we might at some point overflow, where the likely scenario is that this is just fine, yet guarding for not reusing something left over (i.e. from a module created and never disposed) seems like a good precaution to have. Unlikely that someone will do this with tracing enabled, ofc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, I had missed that the ExpressionRunners were removed from the expressionRunners map when they were destroyed 👍


size_t noteExpressionRunner(ExpressionRunnerRef runner) {
size_t id;
do {
id = nextExpressionRunnerId++;
} while (usedExpressionRunnerIds.find(id) != usedExpressionRunnerIds.end());
expressionRunners[runner] = id;
usedExpressionRunnerIds.insert(id);
return id;
}

std::string getTemp() {
static size_t n = 0;
return "t" + std::to_string(n++);
Expand Down Expand Up @@ -604,6 +625,7 @@ void BinaryenModuleDispose(BinaryenModuleRef module) {
std::cout << " events.clear();\n";
std::cout << " exports.clear();\n";
std::cout << " relooperBlocks.clear();\n";
std::cout << " expressionRunners.clear();\n";
types.clear();
expressions.clear();
functions.clear();
Expand Down Expand Up @@ -4954,6 +4976,22 @@ BinaryenExpressionRef RelooperRenderAndDispose(RelooperRef relooper,
// ========= ExpressionRunner =========
//

namespace wasm {

class CExpressionRunner final : public ExpressionRunner<CExpressionRunner> {
public:
CExpressionRunner(Module* module,
CExpressionRunner::Flags flags,
Index maxDepth,
Index maxLoopIterations)
: ExpressionRunner<CExpressionRunner>(
module, flags, maxDepth, maxLoopIterations) {}

void trap(const char* why) override { throw NonconstantException(); }
};

} // namespace wasm

ExpressionRunnerFlags ExpressionRunnerFlagsDefault() {
return CExpressionRunner::FlagValues::DEFAULT;
}
Expand All @@ -4970,21 +5008,25 @@ ExpressionRunnerRef ExpressionRunnerCreate(BinaryenModuleRef module,
ExpressionRunnerFlags flags,
BinaryenIndex maxDepth,
BinaryenIndex maxLoopIterations) {
if (tracing) {
std::cout << " the_runner = ExpressionRunnerCreate(the_module, " << flags
<< ", " << maxDepth << ", " << maxLoopIterations << ");\n";
}
auto* wasm = (Module*)module;
return ExpressionRunnerRef(
auto* runner = ExpressionRunnerRef(
new CExpressionRunner(wasm, flags, maxDepth, maxLoopIterations));
if (tracing) {
auto id = noteExpressionRunner(runner);
std::cout << " expressionRunners[" << id
<< "] = ExpressionRunnerCreate(the_module, " << flags << ", "
<< maxDepth << ", " << maxLoopIterations << ");\n";
}
return runner;
}

int ExpressionRunnerSetLocalValue(ExpressionRunnerRef runner,
BinaryenIndex index,
BinaryenExpressionRef value) {
if (tracing) {
std::cout << " ExpressionRunnerSetLocalValue(the_runner, " << index
<< ", expressions[" << expressions[value] << "]);\n";
std::cout << " ExpressionRunnerSetLocalValue(expressionRunners["
<< expressionRunners[runner] << "], " << index << ", expressions["
<< expressions[value] << "]);\n";
}

auto* R = (CExpressionRunner*)runner;
Expand All @@ -5000,7 +5042,8 @@ int ExpressionRunnerSetGlobalValue(ExpressionRunnerRef runner,
const char* name,
BinaryenExpressionRef value) {
if (tracing) {
std::cout << " ExpressionRunnerSetGlobalValue(the_runner, ";
std::cout << " ExpressionRunnerSetGlobalValue(expressionRunners["
<< expressionRunners[runner] << "], ";
traceNameOrNULL(name);
std::cout << ", expressions[" << expressions[value] << "]);\n";
}
Expand Down Expand Up @@ -5034,8 +5077,10 @@ ExpressionRunnerRunAndDispose(ExpressionRunnerRef runner,
} else {
std::cout << " ";
}
std::cout << "ExpressionRunnerRunAndDispose(the_runner, expressions["
<< expressions[expr] << "]);\n";
auto id = expressionRunners[runner];
std::cout << "ExpressionRunnerRunAndDispose(expressionRunners[" << id
<< "], expressions[" << expressions[expr] << "]);\n";
usedExpressionRunnerIds.erase(id);
}

delete R;
Expand All @@ -5062,9 +5107,9 @@ void BinaryenSetAPITracing(int on) {
" std::map<size_t, BinaryenEventRef> events;\n"
" std::map<size_t, BinaryenExportRef> exports;\n"
" std::map<size_t, RelooperBlockRef> relooperBlocks;\n"
" std::map<size_t, ExpressionRunnerRef> expressionRunners;\n"
" BinaryenModuleRef the_module = NULL;\n"
" RelooperRef the_relooper = NULL;\n"
" ExpressionRunnerRef the_runner = NULL;\n";
" RelooperRef the_relooper = NULL;\n";
} else {
std::cout << " return 0;\n";
std::cout << "}\n";
Expand Down
19 changes: 13 additions & 6 deletions src/binaryen-c.h
Original file line number Diff line number Diff line change
Expand Up @@ -1662,14 +1662,17 @@ typedef uint32_t ExpressionRunnerFlags;
// side effects like those of a `local.tee`.
BINARYEN_API ExpressionRunnerFlags ExpressionRunnerFlagsDefault();

// Be very careful to preserve any side effects, like those of a `local.tee`,
// for example when we are going to replace the expression afterwards.
// Be very careful to preserve any side effects. For example, if we are
// intending to replace the expression with a constant afterwards, even if we
// can technically evaluate down to a constant, we still cannot replace the
// expression if it also sets a local, which must be preserved in this scenario
// so subsequent code keeps functioning.
BINARYEN_API ExpressionRunnerFlags ExpressionRunnerFlagsPreserveSideeffects();

// Traverse through function calls, attempting to compute their concrete value.
// Must not be used in function-parallel scenarios, where the called function
// might or might not have been optimized already to something we can traverse
// successfully, in turn leading to non-deterministic behavior.
// might be concurrently modified, leading to undefined behavior. Traversing
// another function reuses all of this runner's flags.
BINARYEN_API ExpressionRunnerFlags ExpressionRunnerFlagsTraverseCalls();

// Creates an ExpressionRunner instance
Expand All @@ -1680,13 +1683,17 @@ ExpressionRunnerCreate(BinaryenModuleRef module,
BinaryenIndex maxLoopIterations);

// Sets a known local value to use. Order matters if expressions have side
// effects. Returns `true` if the expression actually evaluates to a constant.
// effects. For example, if the expression also sets a local, this side effect
// will also happen (not affected by any flags). Returns `true` if the
// expression actually evaluates to a constant.
BINARYEN_API int ExpressionRunnerSetLocalValue(ExpressionRunnerRef runner,
BinaryenIndex index,
BinaryenExpressionRef value);

// Sets a known global value to use. Order matters if expressions have side
// effects. Returns `true` if the expression actually evaluates to a constant.
// effects. For example, if the expression also sets a local, this side effect
// will also happen (not affected by any flags). Returns `true` if the
// expression actually evaluates to a constant.
BINARYEN_API int ExpressionRunnerSetGlobalValue(ExpressionRunnerRef runner,
const char* name,
BinaryenExpressionRef value);
Expand Down
3 changes: 0 additions & 3 deletions src/passes/Precompute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ class PrecomputingExpressionRunner
MAX_LOOP_ITERATIONS),
getValues(getValues) {}

struct NonconstantException {
}; // TODO: use a flow with a special name, as this is likely very slow

Flow visitLocalGet(LocalGet* curr) {
auto iter = getValues.find(curr);
if (iter != getValues.end()) {
Expand Down
44 changes: 16 additions & 28 deletions src/wasm-interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,15 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
// whether it computes down to a concrete value, where it is not necessary
// to preserve side effects like those of a `local.tee`.
DEFAULT = 0,
// Be very careful to preserve any side effects, like those of a
// `local.tee`, for example when we are going to replace the expression
// afterwards.
// Be very careful to preserve any side effects. For example, if we are
// intending to replace the expression with a constant afterwards, even if
// we can technically evaluate down to a constant, we still cannot replace
// the expression if it also sets a local, which must be preserved in this
// scenario so subsequent code keeps functioning.
PRESERVE_SIDEEFFECTS = 1 << 0,
// Traverse through function calls, attempting to compute their concrete
// value. Must not be used in function-parallel scenarios, where the called
// function might or might not have been optimized already to something we
// can traverse successfully, in turn leading to non-deterministic behavior.
// function might be concurrently modified, leading to undefined behavior.
TRAVERSE_CALLS = 1 << 1
};

Expand Down Expand Up @@ -220,6 +221,9 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
: module(module), flags(flags), maxDepth(maxDepth),
maxLoopIterations(maxLoopIterations) {}

struct NonconstantException {
}; // TODO: use a flow with a special name, as this is likely very slow

// Gets the module this runner is operating on.
Module* getModule() { return module; }

Expand Down Expand Up @@ -1256,16 +1260,16 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
NOTE_ENTER("LocalSet");
NOTE_EVAL1(curr->index);
if (!(flags & FlagValues::PRESERVE_SIDEEFFECTS)) {
// If we are evaluating and not replacing the expression, see if there is
// a value flowing through a tee.
// If we are evaluating and not replacing the expression, remember the
// constant value set, if any, and see if there is a value flowing through
// a tee.
auto setFlow = visit(curr->value);
if (curr->type.isConcrete()) {
assert(curr->isTee());
return setFlow;
}
// Otherwise remember the constant value set, if any, for subsequent gets.
if (!setFlow.breaking()) {
setLocalValue(curr->index, setFlow.values);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't there be subsequent gets if this is a tee, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, updated the code accordingly

if (curr->type.isConcrete()) {
assert(curr->isTee());
return setFlow;
}
return Flow();
}
}
Expand Down Expand Up @@ -2532,22 +2536,6 @@ class ModuleInstance
: ModuleInstanceBase(wasm, externalInterface) {}
};

// Expression runner exposed by the C-API
class CExpressionRunner final : public ExpressionRunner<CExpressionRunner> {
public:
CExpressionRunner(Module* module,
CExpressionRunner::Flags flags,
Index maxDepth,
Index maxLoopIterations)
: ExpressionRunner<CExpressionRunner>(
module, flags, maxDepth, maxLoopIterations) {}

struct NonconstantException {
}; // TODO: use a flow with a special name, as this is likely very slow

void trap(const char* why) override { throw NonconstantException(); }
};

} // namespace wasm

#endif // wasm_wasm_interpreter_h
2 changes: 1 addition & 1 deletion test/binaryen.js/custom-section.js.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ int main() {
std::map<size_t, BinaryenEventRef> events;
std::map<size_t, BinaryenExportRef> exports;
std::map<size_t, RelooperBlockRef> relooperBlocks;
std::map<size_t, ExpressionRunnerRef> expressionRunners;
BinaryenModuleRef the_module = NULL;
RelooperRef the_relooper = NULL;
ExpressionRunnerRef the_runner = NULL;
the_module = BinaryenModuleCreate();
expressions[size_t(NULL)] = BinaryenExpressionRef(NULL);
{
Expand Down
64 changes: 58 additions & 6 deletions test/binaryen.js/expressionrunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ console.log("// ExpressionRunner.Flags.TraverseCalls = " + Flags.TraverseCalls);

binaryen.setAPITracing(true);

function assertDeepEqual(x, y) {
if (typeof x === "object") {
for (var i in x) assertDeepEqual(x[i], y[i]);
for (i in y) assertDeepEqual(x[i], y[i]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do var i here, too, or would that be unidiomatic or bad? Seeing the variable reused like this gives me the heebie jeebies.

} else {
assert(x === y);
}
}

var module = new binaryen.Module();
module.addGlobal("aGlobal", binaryen.i32, true, module.i32.const(0));

Expand All @@ -16,7 +25,14 @@ var expr = runner.runAndDispose(
module.i32.const(2)
)
);
assert(JSON.stringify(binaryen.getExpressionInfo(expr)) === '{"id":14,"type":2,"value":3}');
assertDeepEqual(
binaryen.getExpressionInfo(expr),
{
id: binaryen.ExpressionIds.Const,
type: binaryen.i32,
value: 3
}
);

// Should traverse control structures
runner = new binaryen.ExpressionRunner(module);
Expand All @@ -30,7 +46,14 @@ expr = runner.runAndDispose(
)
),
);
assert(JSON.stringify(binaryen.getExpressionInfo(expr)) === '{"id":14,"type":2,"value":4}');
assertDeepEqual(
binaryen.getExpressionInfo(expr),
{
id: binaryen.ExpressionIds.Const,
type: binaryen.i32,
value: 4
}
);

// Should be unable to evaluate a local if not explicitly specified
runner = new binaryen.ExpressionRunner(module);
Expand All @@ -57,7 +80,14 @@ expr = runner.runAndDispose(
module.i32.const(1)
)
);
assert(JSON.stringify(binaryen.getExpressionInfo(expr)) === '{"id":14,"type":2,"value":5}');
assertDeepEqual(
binaryen.getExpressionInfo(expr),
{
id: binaryen.ExpressionIds.Const,
type: binaryen.i32,
value: 5
}
);

// Should preserve any side-effects if explicitly requested
runner = new binaryen.ExpressionRunner(module, Flags.PreserveSideeffects);
Expand All @@ -83,7 +113,14 @@ expr = runner.runAndDispose(
], binaryen.i32)
)
);
assert(JSON.stringify(binaryen.getExpressionInfo(expr)) === '{"id":14,"type":2,"value":6}');
assertDeepEqual(
binaryen.getExpressionInfo(expr),
{
id: binaryen.ExpressionIds.Const,
type: binaryen.i32,
value: 6
}
);

// Should pick up explicitly preset values
runner = new binaryen.ExpressionRunner(module, Flags.PreserveSideeffects);
Expand All @@ -95,7 +132,14 @@ expr = runner.runAndDispose(
module.global.get("aGlobal", binaryen.i32)
)
);
assert(JSON.stringify(binaryen.getExpressionInfo(expr)) === '{"id":14,"type":2,"value":7}');
assertDeepEqual(
binaryen.getExpressionInfo(expr),
{
id: binaryen.ExpressionIds.Const,
type: binaryen.i32,
value: 7
}
);

// Should traverse into (simple) functions if requested
runner = new binaryen.ExpressionRunner(module, Flags.TraverseCalls);
Expand All @@ -120,7 +164,14 @@ expr = runner.runAndDispose(
module.local.get(0, binaryen.i32)
)
);
assert(JSON.stringify(binaryen.getExpressionInfo(expr)) === '{"id":14,"type":2,"value":8}');
assertDeepEqual(
binaryen.getExpressionInfo(expr),
{
id: binaryen.ExpressionIds.Const,
type: binaryen.i32,
value: 8
}
);

// Should not attempt to traverse into functions if not explicitly set
runner = new binaryen.ExpressionRunner(module);
Expand Down Expand Up @@ -153,4 +204,5 @@ expr = runner.runAndDispose(
);
assert(expr === 0);

module.dispose();
binaryen.setAPITracing(false);
Loading