diff --git a/auto_update_tests.py b/auto_update_tests.py index 7a14f6521aa..98c1c246c31 100755 --- a/auto_update_tests.py +++ b/auto_update_tests.py @@ -21,6 +21,8 @@ cmd += ['-O0'] # test that -O0 does nothing else: cmd += ['-O'] + if 'debugInfo' in asm: + cmd += ['-g'] if precise and opts: # test mem init importing open('a.mem', 'wb').write(asm) diff --git a/check.py b/check.py index 7be44e63b54..e31ad0f0a2c 100755 --- a/check.py +++ b/check.py @@ -107,6 +107,8 @@ cmd += ['-O0'] # test that -O0 does nothing else: cmd += ['-O'] + if 'debugInfo' in asm: + cmd += ['-g'] if precise and opts: # test mem init importing open('a.mem', 'wb').write(asm) diff --git a/src/asm2wasm.h b/src/asm2wasm.h index d23e811ca20..712019d7a65 100644 --- a/src/asm2wasm.h +++ b/src/asm2wasm.h @@ -34,6 +34,7 @@ #include "ast_utils.h" #include "wasm-builder.h" #include "wasm-emscripten.h" +#include "wasm-printing.h" #include "wasm-validator.h" #include "wasm-module-building.h" @@ -109,7 +110,8 @@ Name I32_CTTZ("i32_cttz"), FTCALL("ftCall_"), MFTCALL("mftCall_"), MAX_("max"), - MIN_("min"); + MIN_("min"), + EMSCRIPTEN_DEBUGINFO("emscripten_debuginfo"); // Utilities @@ -160,6 +162,12 @@ struct Asm2WasmPreProcessor { std::vector debugInfoFileNames; std::unordered_map debugInfoFileIndices; + char* allocatedCopy = nullptr; + + ~Asm2WasmPreProcessor() { + if (allocatedCopy) free(allocatedCopy); + } + char* process(char* input) { // emcc --separate-asm modules can look like // @@ -222,10 +230,10 @@ struct Asm2WasmPreProcessor { // and is usually decently accurate with them. auto size = strlen(input); auto upperBound = Index(size * 1.25) + 100; - char* copy = (char*)malloc(upperBound); + char* copy = allocatedCopy = (char*)malloc(upperBound); char* end = copy + upperBound; char* out = copy; - std::string DEBUGINFO_INTRINSIC = "emscripten_debuginfo"; + std::string DEBUGINFO_INTRINSIC = EMSCRIPTEN_DEBUGINFO.str; auto DEBUGINFO_INTRINSIC_SIZE = DEBUGINFO_INTRINSIC.size(); bool seenUseAsm = false; while (input[0]) { @@ -313,7 +321,7 @@ class Asm2WasmBuilder { // function table std::map functionTableStarts; // each asm function table gets a range in the one wasm table, starting at a location - bool memoryGrowth; + Asm2WasmPreProcessor& preprocessor; bool debug; bool imprecise; PassOptions passOptions; @@ -419,11 +427,11 @@ class Asm2WasmBuilder { } public: - Asm2WasmBuilder(Module& wasm, bool memoryGrowth, bool debug, bool imprecise, PassOptions passOptions, bool runOptimizationPasses, bool wasmOnly) + Asm2WasmBuilder(Module& wasm, Asm2WasmPreProcessor& preprocessor, bool debug, bool imprecise, PassOptions passOptions, bool runOptimizationPasses, bool wasmOnly) : wasm(wasm), allocator(wasm.allocator), builder(wasm), - memoryGrowth(memoryGrowth), + preprocessor(preprocessor), debug(debug), imprecise(imprecise), passOptions(passOptions), @@ -641,6 +649,16 @@ class Asm2WasmBuilder { } Function* processFunction(Ref ast); + +public: + CallImport* isDebugInfo(Expression* curr) { + if (auto* call = curr->dynCast()) { + if (call->target == EMSCRIPTEN_DEBUGINFO) { + return call; + } + } + return nullptr; + } }; void Asm2WasmBuilder::processAsm(Ref ast) { @@ -1090,6 +1108,43 @@ void Asm2WasmBuilder::processAsm(Ref ast) { } }; + // apply debug info, reducing intrinsic calls into annotations on the ast nodes + struct ApplyDebugInfo : public WalkerPass>> { + bool isFunctionParallel() override { return true; } + + Pass* create() override { return new ApplyDebugInfo(parent); } + + Asm2WasmBuilder* parent; + + ApplyDebugInfo(Asm2WasmBuilder* parent) : parent(parent) { + name = "apply-debug-info"; + } + + Expression* lastExpression = nullptr; + + void visitExpression(Expression* curr) { + if (auto* call = parent->isDebugInfo(curr)) { + // this is a debuginfo node. turn it into an annotation on the last stack + auto* last = lastExpression; + lastExpression = nullptr; + auto& annotations = getFunction()->annotations; + if (last) { + auto fileIndex = call->operands[0]->cast()->value.geti32(); + auto lineNumber = call->operands[1]->cast()->value.geti32(); + annotations[last] = parent->preprocessor.debugInfoFileNames[fileIndex] + ":" + std::to_string(lineNumber); + } + // eliminate the debug info call + ExpressionManipulator::nop(curr); + return; + } + // ignore const nodes, as they may be the children of the debug info calls, and they + // don't really need debug info anyhow + if (!curr->is()) { + lastExpression = curr; + } + } + }; + PassRunner passRunner(&wasm); if (debug) { passRunner.setDebug(true); @@ -1106,13 +1161,17 @@ void Asm2WasmBuilder::processAsm(Ref ast) { passRunner.add("optimize-instructions"); passRunner.add("post-emscripten"); } + if (preprocessor.debugInfo) { + passRunner.add(this); + passRunner.add("vacuum"); // FIXME maybe just remove the nops that were debuginfo nodes, if not optimizing? + } // make sure to not emit unreachable code at all, even in -O0, as wasm rules for it are complex // and changing. passRunner.add("dce"); passRunner.run(); // apply memory growth, if relevant - if (memoryGrowth) { + if (preprocessor.memoryGrowth) { emscripten::generateMemoryGrowthFunction(wasm); wasm.memory.max = Memory::kMaxSize; } @@ -2349,6 +2408,27 @@ Function* Asm2WasmBuilder::processFunction(Ref ast) { }; // body function->body = processStatements(body, start); + // debug info cleanup: we add debug info calls after each instruction; as + // a result, + // return 0; //@line file.cpp + // will have code after the return. if the function body is a block, + // it will be forced to the return type of the function, and then + // the unreachable type of the return makes things work, which we break + // if we add a none debug intrinsic call afterwards. so we need to fix + // that up. + if (preprocessor.debugInfo) { + if (function->result != none) { + if (auto* block = function->body->dynCast()) { + if (block->list.size() > 0) { + if (isDebugInfo(block->list.back())) { + // add an unreachable. both the debug info and it could be dce'd, + // but it makes us validate properly. + block->list.push_back(builder.makeUnreachable()); + } + } + } + } + } // cleanups/checks assert(breakStack.size() == 0 && continueStack.size() == 0); assert(parentLabel.isNull()); diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 3c847809ff7..e5ff03fae9f 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -45,6 +45,19 @@ struct PrintSExpression : public Visitor { } } + void visit(Expression* curr) { + if (currFunction) { + // show an annotation, if there is one + auto& annotations = currFunction->annotations; + auto iter = annotations.find(curr); + if (iter != annotations.end()) { + o << ";; " << iter->second << '\n'; + doIndent(o, indent); + } + } + Visitor::visit(curr); + } + void setMinify(bool minify_) { minify = minify_; maybeSpace = minify ? "" : " "; diff --git a/src/tools/asm2wasm.cpp b/src/tools/asm2wasm.cpp index 3517a3ec5cf..e23b3f39342 100644 --- a/src/tools/asm2wasm.cpp +++ b/src/tools/asm2wasm.cpp @@ -84,7 +84,7 @@ int main(int argc, const char *argv[]) { [&wasmOnly](Options *o, const std::string &) { wasmOnly = true; }) - .add("--debuginfo", "-g", "Emit names section and debug info", + .add("--debuginfo", "-g", "Emit names section and debug info (you must emit text, -S, for this to work)", Options::Arguments::Zero, [&](Options *o, const std::string &arguments) { debugInfo = true; }) .add("--symbolmap", "-s", "Emit a symbol map (indexes => names)", @@ -99,6 +99,16 @@ int main(int argc, const char *argv[]) { }); options.parse(argc, argv); + // finalize arguments + if (options.extra["output"].size() == 0) { + // when no output file is specified, we emit text to stdout + emitBinary = false; + } + if (emitBinary) { + // wasm binaries can't contain debug info yet + debugInfo = false; + } + const auto &tm_it = options.extra.find("total memory"); size_t totalMemory = tm_it == options.extra.end() ? 16 * 1024 * 1024 : atoi(tm_it->second.c_str()); @@ -121,7 +131,7 @@ int main(int argc, const char *argv[]) { if (options.debug) std::cerr << "wasming..." << std::endl; Module wasm; wasm.memory.initial = wasm.memory.max = totalMemory / Memory::kPageSize; - Asm2WasmBuilder asm2wasm(wasm, pre.memoryGrowth, options.debug, imprecise, passOptions, runOptimizationPasses, wasmOnly); + Asm2WasmBuilder asm2wasm(wasm, pre, options.debug, imprecise, passOptions, runOptimizationPasses, wasmOnly); asm2wasm.processAsm(asmjs); // import mem init file, if provided diff --git a/src/wasm.h b/src/wasm.h index 75d6a174c52..31dafb84bcc 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -1385,6 +1385,9 @@ class Function { std::vector localNames; std::map localIndices; + // node annotations, printed alongside the node in the text format + std::unordered_map annotations; + Function() : result(none) {} size_t getNumParams() { diff --git a/test/debugInfo.asm.js b/test/debugInfo.asm.js new file mode 100644 index 00000000000..929d7980452 --- /dev/null +++ b/test/debugInfo.asm.js @@ -0,0 +1,26 @@ +function () { + "use asm"; + function add(x, y) { + x = x | 0; + y = y | 0; + x = x; //@line 5 "tests/hello_world.c" + y = y; //@line 6 "tests/hello_world.c" + x = y; //@line 314159 "tests/other_file.cpp" + return x + y | 0; + } + function ret(x) { + x = x | 0; + x = x << 1; //@line 50 "return.cpp" + return x + 1 | 0; //@line 100 "return.cpp" + } + function opts(x, y) { + x = x | 0; + y = y | 0; + x = (x + y) | 0; //@line 1 "even-opted.cpp" + y = y >> x; //@line 2 "even-opted.cpp" + x = (x | 0) % (y | 0); //@line 3 "even-opted.cpp" + return x + y | 0; + } + return { add: add, ret: ret, opts: opts }; +} + diff --git a/test/debugInfo.fromasm b/test/debugInfo.fromasm new file mode 100644 index 00000000000..07ae6fb44ff --- /dev/null +++ b/test/debugInfo.fromasm @@ -0,0 +1,54 @@ +(module + (type $FUNCSIG$iii (func (param i32 i32) (result i32))) + (type $FUNCSIG$vii (func (param i32 i32))) + (import "env" "emscripten_debuginfo" (func $emscripten_debuginfo (param i32 i32))) + (import "asm2wasm" "i32s-rem" (func $i32s-rem (param i32 i32) (result i32))) + (import "env" "memory" (memory $0 256 256)) + (import "env" "table" (table 0 0 anyfunc)) + (import "env" "memoryBase" (global $memoryBase i32)) + (import "env" "tableBase" (global $tableBase i32)) + (data (get_global $memoryBase) "debugInfo.asm.js") + (export "add" (func $add)) + (export "ret" (func $ret)) + (export "opts" (func $opts)) + (func $add (param $0 i32) (param $1 i32) (result i32) + (i32.add + (get_local $1) + (get_local $1) + ) + ) + (func $ret (param $0 i32) (result i32) + (i32.add + (i32.shl + (get_local $0) + (i32.const 1) + ) + (i32.const 1) + ) + ) + (func $opts (param $0 i32) (param $1 i32) (result i32) + ;; even-opted.cpp:2 + (set_local $1 + (i32.shr_s + (get_local $1) + (tee_local $0 + (i32.add + (get_local $0) + (get_local $1) + ) + ) + ) + ) + ;; even-opted.cpp:3 + (set_local $0 + (call $i32s-rem + (get_local $0) + (get_local $1) + ) + ) + (i32.add + (get_local $0) + (get_local $1) + ) + ) +) diff --git a/test/debugInfo.fromasm.imprecise b/test/debugInfo.fromasm.imprecise new file mode 100644 index 00000000000..ad881983cc1 --- /dev/null +++ b/test/debugInfo.fromasm.imprecise @@ -0,0 +1,47 @@ +(module + (type $FUNCSIG$vii (func (param i32 i32))) + (import "env" "emscripten_debuginfo" (func $emscripten_debuginfo (param i32 i32))) + (import "env" "memory" (memory $0 256 256)) + (import "env" "table" (table 0 0 anyfunc)) + (import "env" "memoryBase" (global $memoryBase i32)) + (import "env" "tableBase" (global $tableBase i32)) + (export "add" (func $add)) + (export "ret" (func $ret)) + (export "opts" (func $opts)) + (func $add (param $0 i32) (param $1 i32) (result i32) + (i32.add + (get_local $1) + (get_local $1) + ) + ) + (func $ret (param $0 i32) (result i32) + (i32.add + (i32.shl + (get_local $0) + (i32.const 1) + ) + (i32.const 1) + ) + ) + (func $opts (param $0 i32) (param $1 i32) (result i32) + ;; even-opted.cpp:2 + (set_local $1 + (i32.shr_s + (get_local $1) + (tee_local $0 + (i32.add + (get_local $0) + (get_local $1) + ) + ) + ) + ) + (i32.add + (i32.rem_s + (get_local $0) + (get_local $1) + ) + (get_local $1) + ) + ) +) diff --git a/test/debugInfo.fromasm.imprecise.no-opts b/test/debugInfo.fromasm.imprecise.no-opts new file mode 100644 index 00000000000..8433e161c26 --- /dev/null +++ b/test/debugInfo.fromasm.imprecise.no-opts @@ -0,0 +1,76 @@ +(module + (type $FUNCSIG$vii (func (param i32 i32))) + (import "env" "emscripten_debuginfo" (func $emscripten_debuginfo (param i32 i32))) + (import "env" "memory" (memory $0 256 256)) + (import "env" "table" (table 0 0 anyfunc)) + (import "env" "memoryBase" (global $memoryBase i32)) + (import "env" "tableBase" (global $tableBase i32)) + (export "add" (func $add)) + (export "ret" (func $ret)) + (export "opts" (func $opts)) + (func $add (param $x i32) (param $y i32) (result i32) + ;; tests/hello_world.c:5 + (set_local $x + (get_local $x) + ) + ;; tests/hello_world.c:6 + (set_local $y + (get_local $y) + ) + ;; tests/other_file.cpp:314159 + (set_local $x + (get_local $y) + ) + (return + (i32.add + (get_local $x) + (get_local $y) + ) + ) + ) + (func $ret (param $x i32) (result i32) + ;; return.cpp:50 + (set_local $x + (i32.shl + (get_local $x) + (i32.const 1) + ) + ) + ;; return.cpp:100 + (return + (i32.add + (get_local $x) + (i32.const 1) + ) + ) + ) + (func $opts (param $x i32) (param $y i32) (result i32) + ;; even-opted.cpp:1 + (set_local $x + (i32.add + (get_local $x) + (get_local $y) + ) + ) + ;; even-opted.cpp:2 + (set_local $y + (i32.shr_s + (get_local $y) + (get_local $x) + ) + ) + ;; even-opted.cpp:3 + (set_local $x + (i32.rem_s + (get_local $x) + (get_local $y) + ) + ) + (return + (i32.add + (get_local $x) + (get_local $y) + ) + ) + ) +) diff --git a/test/debugInfo.fromasm.no-opts b/test/debugInfo.fromasm.no-opts new file mode 100644 index 00000000000..78b1a2b071c --- /dev/null +++ b/test/debugInfo.fromasm.no-opts @@ -0,0 +1,78 @@ +(module + (type $FUNCSIG$iii (func (param i32 i32) (result i32))) + (type $FUNCSIG$vii (func (param i32 i32))) + (import "env" "emscripten_debuginfo" (func $emscripten_debuginfo (param i32 i32))) + (import "asm2wasm" "i32s-rem" (func $i32s-rem (param i32 i32) (result i32))) + (import "env" "memory" (memory $0 256 256)) + (import "env" "table" (table 0 0 anyfunc)) + (import "env" "memoryBase" (global $memoryBase i32)) + (import "env" "tableBase" (global $tableBase i32)) + (export "add" (func $add)) + (export "ret" (func $ret)) + (export "opts" (func $opts)) + (func $add (param $x i32) (param $y i32) (result i32) + ;; tests/hello_world.c:5 + (set_local $x + (get_local $x) + ) + ;; tests/hello_world.c:6 + (set_local $y + (get_local $y) + ) + ;; tests/other_file.cpp:314159 + (set_local $x + (get_local $y) + ) + (return + (i32.add + (get_local $x) + (get_local $y) + ) + ) + ) + (func $ret (param $x i32) (result i32) + ;; return.cpp:50 + (set_local $x + (i32.shl + (get_local $x) + (i32.const 1) + ) + ) + ;; return.cpp:100 + (return + (i32.add + (get_local $x) + (i32.const 1) + ) + ) + ) + (func $opts (param $x i32) (param $y i32) (result i32) + ;; even-opted.cpp:1 + (set_local $x + (i32.add + (get_local $x) + (get_local $y) + ) + ) + ;; even-opted.cpp:2 + (set_local $y + (i32.shr_s + (get_local $y) + (get_local $x) + ) + ) + ;; even-opted.cpp:3 + (set_local $x + (call $i32s-rem + (get_local $x) + (get_local $y) + ) + ) + (return + (i32.add + (get_local $x) + (get_local $y) + ) + ) + ) +) diff --git a/test/passes/dce.txt b/test/passes/dce.txt index 71b46bd9409..84757294ac2 100644 --- a/test/passes/dce.txt +++ b/test/passes/dce.txt @@ -332,4 +332,9 @@ (func $global (type $1) (unreachable) ) + (func $ret (type $2) (result i32) + (return + (i32.const 0) + ) + ) ) diff --git a/test/passes/dce.wast b/test/passes/dce.wast index ad1d1d3c0d7..b7d903e6bf4 100644 --- a/test/passes/dce.wast +++ b/test/passes/dce.wast @@ -435,4 +435,10 @@ (drop (get_global $x)) (set_global $x (i32.const 1)) ) + (func $ret (result i32) + (return + (i32.const 0) + ) + (nop) + ) )