diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java b/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java index 80e41d2d9..cb7b8d088 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoParser.java @@ -15,34 +15,23 @@ */ package org.teavm.backend.wasm.debug.parser; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.teavm.backend.wasm.debug.info.LineInfo; -import org.teavm.common.Promise; +import org.teavm.backend.wasm.parser.ModuleParser; +import org.teavm.common.AsyncInputStream; +import org.teavm.common.ByteArrayAsyncInputStream; -public class DebugInfoParser { - private static final int WASM_HEADER_SIZE = 8; - private DebugInfoReader reader; - private int pos; - private int posBefore; - private byte[] buffer = new byte[256]; - private int posInBuffer; - private int bufferLimit; - private int currentLEB; - private int currentLEBShift; - private boolean eof; - private int bytesInLEB; - private int sectionCode; - private List chunks = new ArrayList<>(); +public class DebugInfoParser extends ModuleParser { private Map sectionParsers = new HashMap<>(); private DebugLinesParser lines; - public DebugInfoParser(DebugInfoReader reader) { - this.reader = reader; - + public DebugInfoParser(AsyncInputStream reader) { + super(reader); var strings = addSection(new DebugStringParser()); var files = addSection(new DebugFileParser(strings)); var packages = addSection(new DebugPackageParser(strings)); @@ -60,225 +49,31 @@ public class DebugInfoParser { return lines.getLineInfo(); } - public Promise parse() { - return parseHeader().thenAsync(v -> parseSections()); + @Override + protected Consumer getSectionConsumer(int code, int pos, String name) { + if (code == 0) { + var parser = sectionParsers.get(name); + return parser != null ? parser::parse : null; + } else if (code == 10) { + lines.setOffset(pos); + } + return null; } - private Promise parseHeader() { - return fillAtLeast(16).thenVoid(v -> { - if (remaining() < WASM_HEADER_SIZE) { - error("Invalid WebAssembly header"); - } - if (readInt32() != 0x6d736100) { - error("Invalid WebAssembly magic number"); - } - if (readInt32() != 1) { - error("Unsupported WebAssembly version"); - } - }); - } - - private Promise parseSections() { - return readLEB().thenAsync(n -> { - if (n == null) { - return Promise.VOID; - } - sectionCode = n; - return continueParsingSection().thenAsync(v -> parseSections()); - }); - } - - private Promise continueParsingSection() { - return requireLEB("Unexpected end of file reading section length").thenAsync(sectionSize -> { - if (sectionCode == 0) { - return parseSection(pos + sectionSize); - } else { - if (sectionCode == 10) { - lines.setOffset(pos); - } - return skip(sectionSize, "Error skipping section " + sectionCode + " of size " + sectionSize); - } - }); - } - - private Promise parseSection(int limit) { - return readString("Error reading custom section name").thenAsync(name -> { - var sectionParser = sectionParsers.get(name); - if (sectionParser != null) { - return readBytes(limit - pos, "Error reading section '" + name + "' content") - .thenVoid(sectionParser::parse); - } else { - return skip(limit - pos, "Error skipping section '" + name + "'"); - } - }); - } - - private Promise readString(String error) { - return readBytes(error).then(b -> new String(b, StandardCharsets.UTF_8)); - } - - private Promise readBytes(String error) { - return requireLEB(error + ": error parsing size") - .thenAsync(size -> readBytes(size, error + ": error reading content")); - } - - private Promise readBytes(int count, String error) { - return readBytesImpl(count, error).then(v -> { - var result = new byte[count]; - var i = 0; - for (var chunk : chunks) { - System.arraycopy(chunk, 0, result, i, chunk.length); - i += chunk.length; - } - chunks.clear(); - return result; - }); - } - - private Promise readBytesImpl(int count, String error) { - posBefore = pos; - var min = Math.min(count, remaining()); - var chunk = new byte[min]; - System.arraycopy(buffer, posInBuffer, chunk, 0, min); - chunks.add(chunk); - pos += min; - posInBuffer += min; - if (count > min) { - if (eof) { - error(error); - } - return fill().thenAsync(x -> readBytesImpl(count - min, error)); + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Pass single argument - path to wasm file"); + System.exit(1); + } + var file = new File(args[0]); + var input = new ByteArrayAsyncInputStream(Files.readAllBytes(file.toPath())); + var parser = new DebugInfoParser(input); + input.readFully(parser::parse); + var lineInfo = parser.getLineInfo(); + if (lineInfo != null) { + lineInfo.dump(System.out); } else { - return Promise.VOID; + System.out.println("No debug information found"); } } - - private Promise requireLEB(String errorMessage) { - return readLEB().then(n -> { - if (n == null) { - error(errorMessage); - } - return n; - }); - } - - private Promise readLEB() { - posBefore = pos; - currentLEB = 0; - bytesInLEB = 0; - currentLEBShift = 0; - return continueLEB(); - } - - private Promise continueLEB() { - while (posInBuffer < bufferLimit) { - var b = buffer[posInBuffer++]; - ++pos; - ++bytesInLEB; - var digit = b & 0x7F; - if (((digit << currentLEBShift) >> currentLEBShift) != digit) { - error("LEB represents too big number"); - } - currentLEB |= digit << currentLEBShift; - if ((b & 0x80) == 0) { - return Promise.of(currentLEB); - } - currentLEBShift += 7; - } - return fill().thenAsync(bytesRead -> { - if (eof) { - if (bytesInLEB > 0) { - error("Unexpected end of file reached reading LEB"); - } - return Promise.of(null); - } else { - return continueLEB(); - } - }); - } - - private int readInt32() { - posBefore = pos; - var result = (buffer[posInBuffer] & 255) - | ((buffer[posInBuffer + 1] & 255) << 8) - | ((buffer[posInBuffer + 2] & 255) << 16) - | ((buffer[posInBuffer + 3] & 255) << 24); - posInBuffer += 4; - pos += 4; - return result; - } - - private int remaining() { - return bufferLimit - posInBuffer; - } - - private Promise skip(int bytes, String error) { - if (bytes <= remaining()) { - posInBuffer += bytes; - pos += bytes; - return Promise.VOID; - } else { - posBefore = pos; - pos += remaining(); - bytes -= remaining(); - posInBuffer = 0; - bufferLimit = 0; - return skipImpl(bytes, error); - } - } - - private Promise skipImpl(int bytes, String error) { - return reader.skip(bytes).thenAsync(bytesSkipped -> { - if (bytesSkipped < 0) { - error(error); - } - pos += bytesSkipped; - return bytes > bytesSkipped ? skipImpl(bytes - bytesSkipped, error) : Promise.VOID; - }); - } - - private Promise fillAtLeast(int least) { - return fill().thenAsync(x -> fillAtLeastImpl(least)); - } - - private Promise fillAtLeastImpl(int least) { - if (eof || least <= remaining()) { - return Promise.VOID; - } - return reader.read(buffer, bufferLimit, Math.min(least, buffer.length - bufferLimit)) - .thenAsync(bytesRead -> { - if (bytesRead < 0) { - eof = true; - } else { - bufferLimit += bytesRead; - } - return fillAtLeastImpl(least); - }); - } - - private Promise fill() { - return readNext(buffer.length - remaining()); - } - - private Promise readNext(int bytes) { - if (eof) { - return Promise.VOID; - } - if (posInBuffer > 0 && posInBuffer < bufferLimit) { - System.arraycopy(buffer, posInBuffer, buffer, 0, bufferLimit - posInBuffer); - } - bufferLimit -= posInBuffer; - posInBuffer = 0; - return reader.read(buffer, bufferLimit, bytes).thenVoid(bytesRead -> { - if (bytesRead < 0) { - eof = true; - } else { - bufferLimit += bytesRead; - } - }); - } - - private void error(String message) { - throw new ParseException(message, posBefore); - } } diff --git a/core/src/main/java/org/teavm/backend/wasm/parser/ModuleParser.java b/core/src/main/java/org/teavm/backend/wasm/parser/ModuleParser.java new file mode 100644 index 000000000..91f3306c9 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/parser/ModuleParser.java @@ -0,0 +1,272 @@ +/* + * Copyright 2022 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.parser; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.teavm.common.AsyncInputStream; +import org.teavm.common.Promise; + +public class ModuleParser { + private static final int WASM_HEADER_SIZE = 8; + private AsyncInputStream reader; + private int pos; + private int posBefore; + private byte[] buffer = new byte[256]; + private int posInBuffer; + private int bufferLimit; + private int currentLEB; + private int currentLEBShift; + private boolean eof; + private int bytesInLEB; + private int sectionCode; + private List chunks = new ArrayList<>(); + + public ModuleParser(AsyncInputStream inputStream) { + this.reader = inputStream; + } + + public Promise parse() { + return parseHeader().thenAsync(v -> parseSections()); + } + + private Promise parseHeader() { + return fillAtLeast(16).thenVoid(v -> { + if (remaining() < WASM_HEADER_SIZE) { + error("Invalid WebAssembly header"); + } + if (readInt32() != 0x6d736100) { + error("Invalid WebAssembly magic number"); + } + if (readInt32() != 1) { + error("Unsupported WebAssembly version"); + } + }); + } + + private Promise parseSections() { + return readLEB().thenAsync(n -> { + if (n == null) { + return Promise.VOID; + } + sectionCode = n; + return continueParsingSection().thenAsync(v -> parseSections()); + }); + } + + private Promise continueParsingSection() { + return requireLEB("Unexpected end of file reading section length").thenAsync(sectionSize -> { + if (sectionCode == 0) { + return parseSection(pos + sectionSize); + } else { + var consumer = getSectionConsumer(sectionCode, pos, null); + if (consumer == null) { + return skip(sectionSize, "Error skipping section " + sectionCode + " of size " + sectionSize); + } else { + return readBytes(sectionSize, "Error reading section " + sectionCode + " content") + .thenVoid(consumer); + } + } + }); + } + + private Promise parseSection(int limit) { + return readString("Error reading custom section name").thenAsync(name -> { + var consumer = getSectionConsumer(0, pos, name); + if (consumer != null) { + return readBytes(limit - pos, "Error reading section '" + name + "' content").thenVoid(consumer); + } else { + return skip(limit - pos, "Error skipping section '" + name + "'"); + } + }); + } + + protected Consumer getSectionConsumer(int code, int pos, String name) { + return null; + } + + private Promise readString(String error) { + return readBytes(error).then(b -> new String(b, StandardCharsets.UTF_8)); + } + + private Promise readBytes(String error) { + return requireLEB(error + ": error parsing size") + .thenAsync(size -> readBytes(size, error + ": error reading content")); + } + + private Promise readBytes(int count, String error) { + return readBytesImpl(count, error).then(v -> { + var result = new byte[count]; + var i = 0; + for (var chunk : chunks) { + System.arraycopy(chunk, 0, result, i, chunk.length); + i += chunk.length; + } + chunks.clear(); + return result; + }); + } + + private Promise readBytesImpl(int count, String error) { + posBefore = pos; + var min = Math.min(count, remaining()); + var chunk = new byte[min]; + System.arraycopy(buffer, posInBuffer, chunk, 0, min); + chunks.add(chunk); + pos += min; + posInBuffer += min; + if (count > min) { + if (eof) { + error(error); + } + return fill().thenAsync(x -> readBytesImpl(count - min, error)); + } else { + return Promise.VOID; + } + } + + private Promise requireLEB(String errorMessage) { + return readLEB().then(n -> { + if (n == null) { + error(errorMessage); + } + return n; + }); + } + + private Promise readLEB() { + posBefore = pos; + currentLEB = 0; + bytesInLEB = 0; + currentLEBShift = 0; + return continueLEB(); + } + + private Promise continueLEB() { + while (posInBuffer < bufferLimit) { + var b = buffer[posInBuffer++]; + ++pos; + ++bytesInLEB; + var digit = b & 0x7F; + if (((digit << currentLEBShift) >> currentLEBShift) != digit) { + error("LEB represents too big number"); + } + currentLEB |= digit << currentLEBShift; + if ((b & 0x80) == 0) { + return Promise.of(currentLEB); + } + currentLEBShift += 7; + } + return fill().thenAsync(bytesRead -> { + if (eof) { + if (bytesInLEB > 0) { + error("Unexpected end of file reached reading LEB"); + } + return Promise.of(null); + } else { + return continueLEB(); + } + }); + } + + private int readInt32() { + posBefore = pos; + var result = (buffer[posInBuffer] & 255) + | ((buffer[posInBuffer + 1] & 255) << 8) + | ((buffer[posInBuffer + 2] & 255) << 16) + | ((buffer[posInBuffer + 3] & 255) << 24); + posInBuffer += 4; + pos += 4; + return result; + } + + private int remaining() { + return bufferLimit - posInBuffer; + } + + private Promise skip(int bytes, String error) { + if (bytes <= remaining()) { + posInBuffer += bytes; + pos += bytes; + return Promise.VOID; + } else { + posBefore = pos; + pos += remaining(); + bytes -= remaining(); + posInBuffer = 0; + bufferLimit = 0; + return skipImpl(bytes, error); + } + } + + private Promise skipImpl(int bytes, String error) { + return reader.skip(bytes).thenAsync(bytesSkipped -> { + if (bytesSkipped < 0) { + error(error); + } + pos += bytesSkipped; + return bytes > bytesSkipped ? skipImpl(bytes - bytesSkipped, error) : Promise.VOID; + }); + } + + private Promise fillAtLeast(int least) { + return fill().thenAsync(x -> fillAtLeastImpl(least)); + } + + private Promise fillAtLeastImpl(int least) { + if (eof || least <= remaining()) { + return Promise.VOID; + } + return reader.read(buffer, bufferLimit, Math.min(least, buffer.length - bufferLimit)) + .thenAsync(bytesRead -> { + if (bytesRead < 0) { + eof = true; + } else { + bufferLimit += bytesRead; + } + return fillAtLeastImpl(least); + }); + } + + private Promise fill() { + return readNext(buffer.length - remaining()); + } + + private Promise readNext(int bytes) { + if (eof) { + return Promise.VOID; + } + if (posInBuffer > 0 && posInBuffer < bufferLimit) { + System.arraycopy(buffer, posInBuffer, buffer, 0, bufferLimit - posInBuffer); + } + bufferLimit -= posInBuffer; + posInBuffer = 0; + return reader.read(buffer, bufferLimit, bytes).thenVoid(bytesRead -> { + if (bytesRead < 0) { + eof = true; + } else { + bufferLimit += bytesRead; + } + }); + } + + private void error(String message) { + throw new ParseException(message, posBefore); + } + +} diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/ParseException.java b/core/src/main/java/org/teavm/backend/wasm/parser/ParseException.java similarity index 78% rename from core/src/main/java/org/teavm/backend/wasm/debug/parser/ParseException.java rename to core/src/main/java/org/teavm/backend/wasm/parser/ParseException.java index 4d9554f11..0f06cab00 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/parser/ParseException.java +++ b/core/src/main/java/org/teavm/backend/wasm/parser/ParseException.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.teavm.backend.wasm.debug.parser; +package org.teavm.backend.wasm.parser; -class ParseException extends RuntimeException { - final String message; - final int pos; +public class ParseException extends RuntimeException { + public final String message; + public final int pos; - ParseException(String message, int pos) { + public ParseException(String message, int pos) { super("Error parsing at " + pos + ": " + message); this.message = message; this.pos = pos; diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoReader.java b/core/src/main/java/org/teavm/common/AsyncInputStream.java similarity index 86% rename from core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoReader.java rename to core/src/main/java/org/teavm/common/AsyncInputStream.java index cd094aa95..6437be376 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/parser/DebugInfoReader.java +++ b/core/src/main/java/org/teavm/common/AsyncInputStream.java @@ -13,11 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.teavm.backend.wasm.debug.parser; +package org.teavm.common; -import org.teavm.common.Promise; - -public interface DebugInfoReader { +public interface AsyncInputStream { Promise skip(int amount); Promise read(byte[] buffer, int offset, int count); diff --git a/core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java b/core/src/main/java/org/teavm/common/ByteArrayAsyncInputStream.java similarity index 55% rename from core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java rename to core/src/main/java/org/teavm/common/ByteArrayAsyncInputStream.java index 8e7777d24..8cc65008b 100644 --- a/core/src/main/java/org/teavm/debugging/DebugInfoReaderImpl.java +++ b/core/src/main/java/org/teavm/common/ByteArrayAsyncInputStream.java @@ -13,34 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.teavm.debugging; +package org.teavm.common; -import org.teavm.backend.wasm.debug.parser.DebugInfoParser; -import org.teavm.backend.wasm.debug.parser.DebugInfoReader; -import org.teavm.common.CompletablePromise; -import org.teavm.common.Promise; +import java.util.function.Supplier; -class DebugInfoReaderImpl implements DebugInfoReader { +public class ByteArrayAsyncInputStream implements AsyncInputStream { private byte[] data; private int ptr; private CompletablePromise promise; private byte[] target; private int offset; private int count; + private Throwable errorOccurred; - DebugInfoReaderImpl(byte[] data) { + public ByteArrayAsyncInputStream(byte[] data) { this.data = data; } - DebugInfoParser read() { - var debugInfoParser = new DebugInfoParser(this); - Promise.runNow(() -> { - debugInfoParser.parse().catchVoid(Throwable::printStackTrace); - complete(); - }); - return debugInfoParser; - } - @Override public Promise skip(int amount) { promise = new CompletablePromise<>(); @@ -57,20 +46,31 @@ class DebugInfoReaderImpl implements DebugInfoReader { return promise; } - private void complete() { - while (promise != null) { - var p = promise; - count = Math.min(count, data.length - ptr); - promise = null; - if (target != null) { - System.arraycopy(data, ptr, target, offset, count); - target = null; + public void readFully(Supplier> command) { + Promise.runNow(() -> { + command.get().catchError(e -> { + errorOccurred = e; + throw new RuntimeException(e); + }); + while (promise != null) { + var p = promise; + count = Math.min(count, data.length - ptr); + promise = null; + if (target != null) { + System.arraycopy(data, ptr, target, offset, count); + target = null; + } + ptr += count; + if (count == 0) { + count = -1; + } + p.complete(count); } - ptr += count; - if (count == 0) { - count = -1; - } - p.complete(count); + }); + var e = errorOccurred; + errorOccurred = null; + if (e != null) { + throw new RuntimeException(e); } } } \ No newline at end of file diff --git a/core/src/main/java/org/teavm/debugging/Debugger.java b/core/src/main/java/org/teavm/debugging/Debugger.java index 6b9201cae..bdfb4ed12 100644 --- a/core/src/main/java/org/teavm/debugging/Debugger.java +++ b/core/src/main/java/org/teavm/debugging/Debugger.java @@ -28,6 +28,8 @@ import java.util.Set; import org.teavm.backend.wasm.debug.info.LineInfo; import org.teavm.backend.wasm.debug.info.LineInfoFileCommand; import org.teavm.backend.wasm.debug.info.MethodInfo; +import org.teavm.backend.wasm.debug.parser.DebugInfoParser; +import org.teavm.common.ByteArrayAsyncInputStream; import org.teavm.common.Promise; import org.teavm.debugging.information.DebugInformation; import org.teavm.debugging.information.DebugInformationProvider; @@ -316,6 +318,9 @@ public class Debugger { for (var wasmLineInfo : wasmLineInfoBySource(location.getFileName())) { for (var sequence : wasmLineInfo.sequences()) { for (var loc : sequence.unpack().locations()) { + if (loc.location() == null) { + continue; + } if (loc.location().line() == location.getLine() && loc.location().file().fullName().equals(location.getFileName())) { var jsLocation = new JavaScriptLocation(wasmScriptMap.get(wasmLineInfo), @@ -513,16 +518,23 @@ public class Debugger { return; } var decoder = Base64.getDecoder(); - var reader = new DebugInfoReaderImpl(decoder.decode(source)); - var parser = reader.read(); + var reader = new ByteArrayAsyncInputStream(decoder.decode(source)); + var parser = new DebugInfoParser(reader); + try { + reader.readFully(parser::parse); + } catch (Throwable e) { + e.printStackTrace(); + } if (parser.getLineInfo() != null) { wasmLineInfoMap.put(script, parser.getLineInfo()); wasmScriptMap.put(parser.getLineInfo(), script); for (var sequence : parser.getLineInfo().sequences()) { for (var command : sequence.commands()) { if (command instanceof LineInfoFileCommand) { - addWasmInfoFile(((LineInfoFileCommand) command).file().fullName(), - parser.getLineInfo()); + var file = ((LineInfoFileCommand) command).file(); + if (file != null) { + addWasmInfoFile(file.fullName(), parser.getLineInfo()); + } } } }