Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 13 additions & 6 deletions scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2157,13 +2157,19 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts):

# Modify the initial wat to get the pre-optimizations wasm.
pre_wasm = abspath('pre.wasm')
run([in_bin('wasm-opt'), input] + FEATURE_OPTS + [
gen_args = [
input,
'-ttf',
'--fuzz-preserve-imports-exports',
'--initial-fuzz=' + wat_file,
'-o', pre_wasm,
'-g',
])
]
# We do not copy all of GEN_ARGS, as we don't need e.g. legalization.
if not NANS:
# TODO: do we also need this in each reduction step?
gen_args += ['--denan']
run([in_bin('wasm-opt')] + gen_args + FEATURE_OPTS)

# We successfully generated pre_wasm; stash it for possible reduction
# purposes later.
Expand Down Expand Up @@ -2206,8 +2212,9 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts):
post_vm = random.choice(vms)
post = self.do_run(post_vm, js_file, post_wasm)

# Compare
compare(pre, post, 'PreserveImportsExportsJS')
# Compare, if we can.
if pre_vm.can_compare_to_other(post_vm):
compare(pre, post, 'PreserveImportsExportsJS')

def do_run(self, vm, js, wasm):
out = vm.run_js(js, wasm, checked=False)
Expand Down Expand Up @@ -2281,7 +2288,7 @@ def handle(self, wasm):
for line in out.splitlines():
if line.startswith(LOG_BRANCH_PREFIX):
# (1:-1 strips away the '[', ']' at the edges)
_, _, id_, hint, actual = line[1:-1].split(' ')
_, _, actual, hint, id_ = line[1:-1].split(' ')
all_ids.add(id_)
if hint != actual:
# This hint was misleading.
Expand Down Expand Up @@ -2443,7 +2450,7 @@ def handle(self, wasm):
continue
for line in group:
if line.startswith(LOG_BRANCH_PREFIX):
_, _, id_, hint, actual = line[1:-1].split(' ')
_, _, actual, hint, id_ = line[1:-1].split(' ')
hint = int(hint)
actual = int(actual)
assert hint in (0, 1)
Expand Down
5 changes: 3 additions & 2 deletions scripts/fuzz_shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,9 @@ var baseImports = {
});
},

'log-branch': (id, expected, actual) => {
console.log(`[LoggingExternalInterface log-branch ${id} ${expected} ${actual}]`);
'log-branch': (actual, expected, id) => {
console.log(`[LoggingExternalInterface log-branch ${actual} ${expected} ${id}]`);
return actual;
},
},
// Emscripten support.
Expand Down
4 changes: 2 additions & 2 deletions scripts/test/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def parse_args(args):
parser.add_argument(
'--no-auto-initial-contents', dest='auto_initial_contents',
action='store_false', default=True,
help='Select important initial contents automaticaly in fuzzer. '
'Default: disabled.')
help='Disables the automatic selection of important initial contents '
'in fuzzer.')

return parser.parse_args(args)

Expand Down
3 changes: 3 additions & 0 deletions src/binaryen-c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,9 @@ BinaryenFeatures BinaryenFeatureMultibyte(void) {
BinaryenFeatures BinaryenFeatureCustomPageSizes(void) {
return static_cast<BinaryenFeatures>(FeatureSet::CustomPageSizes);
}
BinaryenFeatures BinaryenFeatureWideArithmetic(void) {
return static_cast<BinaryenFeatures>(FeatureSet::WideArithmetic);
}
BinaryenFeatures BinaryenFeatureAll(void) {
return static_cast<BinaryenFeatures>(FeatureSet::All);
}
Expand Down
1 change: 1 addition & 0 deletions src/binaryen-c.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ BINARYEN_API BinaryenFeatures BinaryenFeatureCallIndirectOverlong(void);
BINARYEN_API BinaryenFeatures BinaryenFeatureRelaxedAtomics(void);
BINARYEN_API BinaryenFeatures BinaryenFeatureMultibyte(void);
BINARYEN_API BinaryenFeatures BinaryenFeatureCustomPageSizes(void);
BINARYEN_API BinaryenFeatures BinaryenFeatureWideArithmetic(void);
BINARYEN_API BinaryenFeatures BinaryenFeatureAll(void);

// Modules
Expand Down
14 changes: 14 additions & 0 deletions src/ir/effects.h
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,12 @@ class EffectAnalyzer {
}
}
void visitCallIndirect(CallIndirect* curr) {
if (auto it = parent.module.typeEffects.find(curr->heapType);
it != parent.module.typeEffects.end()) {
parent.mergeIn(*it->second);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It doesn't look like this accounts for the possible trap from a bad type check. I guess the previous code didn't need to handle it because setting calls implicitly includes trapping along with most other effects.

To make this as precise as possible, we could look at the type of the table and skip setting the trap effect if it is non-nullable and a subtype of the call_indirect expected type.

return;
}

parent.calls = true;
if (curr->isReturn) {
parent.branchesOut = true;
Expand Down Expand Up @@ -1040,6 +1046,14 @@ class EffectAnalyzer {
if (trapOnNull(curr->target)) {
return;
}

if (auto it =
parent.module.typeEffects.find(curr->target->type.getHeapType());
it != parent.module.typeEffects.end()) {
parent.mergeIn(*it->second);
return;
}

if (curr->isReturn) {
parent.branchesOut = true;
if (parent.features.hasExceptionHandling()) {
Expand Down
48 changes: 40 additions & 8 deletions src/ir/linear-execution.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ struct LinearExecutionWalker : public PostWalker<SubType, VisitorType> {
static void scan(SubType* self, Expression** currp) {
Expression* curr = *currp;

auto handleCall = [&](bool isReturn) {
auto handleCall = [&](bool mayThrow, bool isReturn) {
if (!self->connectAdjacentBlocks) {
// Control is nonlinear if we return, or if EH is enabled or may be.
if (isReturn || !self->getModule() ||
self->getModule()->features.hasExceptionHandling()) {
// Control is nonlinear if we return or throw. Traps don't need to be
// taken into account since they don't break control flow in a way
// that's observable.
if (mayThrow || isReturn) {
self->pushTask(SubType::doNoteNonLinear, currp);
}
}
Expand Down Expand Up @@ -153,12 +154,43 @@ struct LinearExecutionWalker : public PostWalker<SubType, VisitorType> {
break;
}
case Expression::Id::CallId: {
handleCall(curr->cast<Call>()->isReturn);
return;
auto* call = curr->cast<Call>();

bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();
if (mayThrow && self->getModule()) {
auto* effects =
self->getModule()->getFunction(call->target)->effects.get();

if (effects && !effects->throws_) {
mayThrow = false;
}
}

handleCall(mayThrow, call->isReturn);
break;
}
case Expression::Id::CallRefId: {
handleCall(curr->cast<CallRef>()->isReturn);
return;
auto* callRef = curr->cast<CallRef>();

// TODO: Effect analysis for indirect calls isn't implemented yet.
// Assume any indirect call may throw for now.
bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();

handleCall(mayThrow, callRef->isReturn);
break;
}
case Expression::Id::CallIndirectId: {
auto* callIndirect = curr->cast<CallIndirect>();

// TODO: Effect analysis for indirect calls isn't implemented yet.
// Assume any indirect call may throw for now.
bool mayThrow = !self->getModule() ||
self->getModule()->features.hasExceptionHandling();

handleCall(mayThrow, callIndirect->isReturn);
break;
}
case Expression::Id::TryId: {
self->pushTask(SubType::doVisitTry, currp);
Expand Down
22 changes: 20 additions & 2 deletions src/ir/properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,8 @@ inline MemoryOrder getMemoryOrder(Expression* curr) {
}

// Whether this instruction will be unwritable in the text and binary formats
// because it requires a type index immediate giving the type of a child that
// has unreachable or null type, and therefore does not have a type index.
// because it requires a type index immediate computed from an expression with
// unreachable or null type, and therefore no type index.
inline bool hasUnwritableTypeImmediate(Expression* curr) {
#define DELEGATE_ID curr->_id

Expand All @@ -534,6 +534,24 @@ inline bool hasUnwritableTypeImmediate(Expression* curr) {
} \
}

#define DELEGATE_IMMEDIATE_TYPED_RESULT(id) \
if (curr->type == Type::unreachable) { \
if constexpr (id::SpecificId == Expression::Id::RefCastId) { \
auto* cast = curr->cast<RefCast>(); \
if (!cast->desc) { \
return true; \
} \
if (!cast->desc->type.isRef()) { \
return true; \
} \
if (!cast->desc->type.getHeapType().getDescribedType()) { \
return true; \
} \
return false; \
} \
return true; \
}

#define DELEGATE_FIELD_CHILD(id, field)
#define DELEGATE_FIELD_CHILD_VECTOR(id, field)
#define DELEGATE_FIELD_INT(id, field)
Expand Down
1 change: 1 addition & 0 deletions src/js/binaryen.js-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ function initializeConstants() {
'CallIndirectOverlong',
'RelaxedAtomics',
'CustomPageSizes',
'WideArithmetic',
'All'
].forEach(name => {
Module['Features'][name] = Module['_BinaryenFeature' + name]();
Expand Down
98 changes: 84 additions & 14 deletions src/passes/GlobalEffects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
#include <ranges>

#include "ir/effects.h"
#include "ir/element-utils.h"
#include "ir/module-utils.h"
#include "pass.h"
#include "support/graph_traversal.h"
#include "support/strongly_connected_components.h"
#include "support/utilities.h"
#include "wasm.h"

namespace wasm {
Expand All @@ -47,6 +49,56 @@ struct FuncInfo {
std::unordered_set<HeapType> indirectCalledTypes;
};

/*
Only funcs that are 'addressed' may be the target of an indirect call. A
function is addressed if:
- It appears in a ref.func expression
- It appears in an `elem` segment (note that we already ignore `elem declare`
statements in our IR, but we check separately for funcs that appear in
`ref.func`).
- It's exported, because it may flow back to us as a reference.
- It's imported, which implies it is `elem declare`d.

If a function doesn't meet any of these criteria, it can't be the target of
an indirect call and we don't need to include its effects in indirect calls.
*/
std::unordered_set<Function*> getAddressedFuncs(Module& module) {
struct AddressedFuncsWalker : PostWalker<AddressedFuncsWalker> {
std::unordered_set<Function*>& addressedFuncs;

AddressedFuncsWalker(std::unordered_set<Function*>& addressedFuncs)
: addressedFuncs(addressedFuncs) {}

void visitRefFunc(RefFunc* refFunc) {
addressedFuncs.insert(getModule()->getFunction(refFunc->func));
}
};

std::unordered_set<Function*> addressedFuncs;
AddressedFuncsWalker walker(addressedFuncs);
walker.walkModule(&module);

ModuleUtils::iterImportedFunctions(
module, [&addressedFuncs, &module](Function* import) {
addressedFuncs.insert(module.getFunction(import->name));
});

for (const auto& export_ : module.exports) {
if (export_->kind != ExternalKind::Function) {
continue;
}

addressedFuncs.insert(module.getFunction(*export_->getInternalName()));
}

ElementUtils::iterAllElementFunctionNames(
&module, [&addressedFuncs, &module](Name func) {
addressedFuncs.insert(module.getFunction(func));
});

return addressedFuncs;
}

std::map<Function*, FuncInfo> analyzeFuncs(Module& module,
const PassOptions& passOptions) {
ModuleUtils::ParallelFunctionAnalysis<FuncInfo> analysis(
Expand Down Expand Up @@ -147,6 +199,7 @@ using CallGraph =

CallGraph buildCallGraph(const Module& module,
const std::map<Function*, FuncInfo>& funcInfos,
const std::unordered_set<Function*>& addressedFuncs,
bool closedWorld) {
CallGraph callGraph;
if (!closedWorld) {
Expand Down Expand Up @@ -182,7 +235,9 @@ CallGraph buildCallGraph(const Module& module,
}

// Type -> Function
callGraph[caller->type.getHeapType()].insert(caller);
if (addressedFuncs.contains(caller)) {
callGraph[caller->type.getHeapType()].insert(caller);
}
}

// Type -> Type
Expand Down Expand Up @@ -228,10 +283,13 @@ void mergeMaybeEffects(std::optional<EffectAnalyzer>& dest,
// - Merge all of the effects of functions within the CC
// - Also merge the (already computed) effects of each callee CC
// - Add trap effects for potentially recursive call chains
void propagateEffects(const Module& module,
const PassOptions& passOptions,
std::map<Function*, FuncInfo>& funcInfos,
const CallGraph& callGraph) {
void propagateEffects(
const Module& module,
const PassOptions& passOptions,
std::map<Function*, FuncInfo>& funcInfos,
std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>&
typeEffects,
const CallGraph& callGraph) {
// We only care about Functions that are roots, not types.
// A type would be a root if a function exists with that type, but no-one
// indirect calls the type.
Expand Down Expand Up @@ -320,12 +378,21 @@ void propagateEffects(const Module& module,
}

// Assign each function's effects to its CC effects.
for (Function* f : ccFuncs) {
if (!ccEffects) {
funcInfos.at(f).effects = UnknownEffects;
} else {
funcInfos.at(f).effects.emplace(*ccEffects);
}
for (auto node : cc) {
std::visit(overloaded{[&](HeapType type) {
if (ccEffects != UnknownEffects) {
typeEffects[type] =
std::make_shared<EffectAnalyzer>(*ccEffects);
}
},
[&](Function* f) {
if (!ccEffects) {
funcInfos.at(f).effects = UnknownEffects;
} else {
funcInfos.at(f).effects.emplace(*ccEffects);
}
}},
node);
}
}
}
Expand All @@ -346,10 +413,13 @@ struct GenerateGlobalEffects : public Pass {
std::map<Function*, FuncInfo> funcInfos =
analyzeFuncs(*module, getPassOptions());

auto callGraph =
buildCallGraph(*module, funcInfos, getPassOptions().closedWorld);
auto addressedFuncs = getAddressedFuncs(*module);

auto callGraph = buildCallGraph(
*module, funcInfos, addressedFuncs, getPassOptions().closedWorld);

propagateEffects(*module, getPassOptions(), funcInfos, callGraph);
propagateEffects(
*module, getPassOptions(), funcInfos, module->typeEffects, callGraph);

copyEffectsToFunctions(funcInfos);
}
Expand Down
Loading
Loading