diff --git a/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/LuaVm.java b/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/LuaVm.java index 5d6516f..ea3fd1f 100644 --- a/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/LuaVm.java +++ b/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/LuaVm.java @@ -1,18 +1,29 @@ package fi.benjami.code4jvm.lua; +import java.util.ArrayList; import java.util.List; +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.DefaultErrorStrategy;import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.atn.ATNConfigSet; +import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.misc.ParseCancellationException; import fi.benjami.code4jvm.lua.compiler.IrCompiler; import fi.benjami.code4jvm.lua.compiler.LuaScope; +import fi.benjami.code4jvm.lua.compiler.LuaSyntaxException; import fi.benjami.code4jvm.lua.ir.LuaLocalVar; import fi.benjami.code4jvm.lua.ir.LuaModule; import fi.benjami.code4jvm.lua.ir.LuaType; import fi.benjami.code4jvm.lua.ir.UpvalueTemplate; import fi.benjami.code4jvm.lua.parser.LuaLexer; import fi.benjami.code4jvm.lua.parser.LuaParser; +import fi.benjami.code4jvm.lua.parser.LuaParser.ChunkContext; import fi.benjami.code4jvm.lua.runtime.LuaFunction; import fi.benjami.code4jvm.lua.runtime.LuaTable; @@ -49,7 +60,32 @@ public LuaModule compile(String name, String chunk) { // Tokenize and parse the chunk var lexer = new LuaLexer(CharStreams.fromString(chunk)); var parser = new LuaParser(new CommonTokenStream(lexer)); - var tree = parser.chunk(); + parser.setErrorHandler(new BailErrorStrategy()); + + ChunkContext tree; + try { + tree = parser.chunk(); + } catch (ParseCancellationException e) { + // Parse error; try again, this time with error recovery and no IR generation + lexer = new LuaLexer(CharStreams.fromString(chunk)); + parser = new LuaParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + + var errors = new ArrayList(); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(Recognizer recognizer, + Object offendingSymbol, + int line, + int charPositionInLine, + String msg, + RecognitionException e) { + errors.add(new LuaSyntaxException.Error(name, line, charPositionInLine, msg)); + } + }); + tree = parser.chunk(); + throw new LuaSyntaxException(errors); + } // Perform semantic analysis and compile to IR var rootScope = LuaScope.chunkRoot(); diff --git a/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/compiler/LuaSyntaxException.java b/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/compiler/LuaSyntaxException.java new file mode 100644 index 0000000..5792211 --- /dev/null +++ b/lua4jvm/src/main/java/fi/benjami/code4jvm/lua/compiler/LuaSyntaxException.java @@ -0,0 +1,45 @@ +package fi.benjami.code4jvm.lua.compiler; + +import java.util.List; +import java.util.stream.Collectors; + +import fi.benjami.code4jvm.lua.LuaVm; + +/** + * Thrown by the {@link LuaVm Lua VM} when source code given to it contains + * syntax errors. Although the VM will refuse to load such code, it will + * attempt to parse through the entire chunk and report all identified syntax + * errors. + * + */ +public class LuaSyntaxException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final List errors; + + public LuaSyntaxException(List errors) { + super(makeMsg(errors)); + if (errors.isEmpty()) { + throw new IllegalArgumentException("cannot to create syntax error without errors"); + } + this.errors = errors; + } + + private static String makeMsg(List errors) { + return errors.stream() + .map(error -> error.chunkName + ":" + error.line + ":" + error.posInLine + " " + error.message) + .collect(Collectors.joining("\n")); + } + + public record Error( + String chunkName, + int line, + int posInLine, + String message + ) {} + + public List errors() { + return errors; + } +} diff --git a/lua4jvm/src/test/java/fi/benjami/code4jvm/lua/test/CompilerErrorTest.java b/lua4jvm/src/test/java/fi/benjami/code4jvm/lua/test/CompilerErrorTest.java new file mode 100644 index 0000000..1dcb890 --- /dev/null +++ b/lua4jvm/src/test/java/fi/benjami/code4jvm/lua/test/CompilerErrorTest.java @@ -0,0 +1,56 @@ +package fi.benjami.code4jvm.lua.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +import fi.benjami.code4jvm.lua.LuaVm; +import fi.benjami.code4jvm.lua.compiler.LuaSyntaxException; + +public class CompilerErrorTest { + + private final LuaVm vm = new LuaVm(); + + @Test + public void syntaxError() { + try { + vm.compile("\nfunction #() end"); + } catch (LuaSyntaxException e) { + var errors = e.errors(); + assertEquals(1, errors.size()); + var err = errors.get(0); + assertEquals("unknown", err.chunkName()); + assertEquals(2, err.line()); + assertEquals(9, err.posInLine()); + // Don't check the message; it is currently whatever antlr 4 sees fit + return; + } + fail(); + } + + @Test + public void multipleErrors() { + try { + vm.compile(""" + function #() end + + function #() end + """); + } catch (LuaSyntaxException e) { + var errors = e.errors(); + assertEquals(2, errors.size()); + var first = errors.get(0); + assertEquals("unknown", first.chunkName()); + assertEquals(1, first.line()); + assertEquals(9, first.posInLine()); + + var second = errors.get(1); + assertEquals("unknown", second.chunkName()); + assertEquals(3, second.line()); + assertEquals(9, second.posInLine()); + return; + } + fail(); + } +}