1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-30 13:13:00 +00:00

Improve display of runtime errors (#1320)

- Bump Cobalt to 0.6.0. We now track both line and column numbers of
   each bytecode instruction, allowing us to map an error to a concrete
   position.

 - `loadfile` (and similar functions) now use the full path, rather than
   the file name. Cobalt truncates this to 30 characters (rather than
   the previous 60) so this should be less noisy.

 - The shell, edit and Lua REPL now display the corresponding source
   code alongside an error.

   Note this is incredibly limited right now - it won't cope with errors
   which cross coroutine boundaries. Supporting this is on the roadmap,
   but requires some careful API design.
This commit is contained in:
Jonathan Coates
2023-02-09 20:53:50 +00:00
committed by GitHub
parent 62e3c5f9aa
commit 5502412181
10 changed files with 204 additions and 75 deletions

View File

@@ -6,6 +6,7 @@
package dan200.computercraft.core;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntMaps;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.slf4j.Logger;
@@ -17,19 +18,14 @@ import org.squiddev.cobalt.compiler.LuaC;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Map;
import java.util.Queue;
class LuaCoverage {
private static final Logger LOG = LoggerFactory.getLogger(LuaCoverage.class);
private static final Path ROOT = new File("src/main/resources/data/computercraft/lua").toPath();
private static final Path BIOS = ROOT.resolve("bios.lua");
private static final Path APIS = ROOT.resolve("rom/apis");
private static final Path STARTUP = ROOT.resolve("rom/startup.lua");
private static final Path SHELL = ROOT.resolve("rom/programs/shell.lua");
private static final Path MULTISHELL = ROOT.resolve("rom/programs/advanced/multishell.lua");
private static final Path TREASURE = ROOT.resolve("treasure");
private final Map<String, Int2IntMap> coverage;
private final String blank;
@@ -49,26 +45,20 @@ class LuaCoverage {
}
void write(Writer out) throws IOException {
Files.find(ROOT, Integer.MAX_VALUE, (path, attr) -> attr.isRegularFile() && !path.startsWith(TREASURE)).forEach(path -> {
Files.find(ROOT, Integer.MAX_VALUE, (path, attr) -> attr.isRegularFile()).forEach(path -> {
var relative = ROOT.relativize(path);
var full = relative.toString().replace('\\', '/');
if (!full.endsWith(".lua")) return;
var possiblePaths = Stream.of(
coverage.remove("/" + full),
path.equals(BIOS) ? coverage.remove("bios.lua") : null,
path.equals(STARTUP) ? coverage.remove("startup.lua") : null,
path.equals(SHELL) ? coverage.remove("shell.lua") : null,
path.equals(MULTISHELL) ? coverage.remove("multishell.lua") : null,
path.startsWith(APIS) ? coverage.remove(path.getFileName().toString()) : null
);
var files = possiblePaths
.filter(Objects::nonNull)
.flatMap(x -> x.int2IntEntrySet().stream())
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
var possiblePaths = coverage.remove("/" + full);
if (possiblePaths == null) possiblePaths = coverage.remove(full);
if (possiblePaths == null) {
possiblePaths = Int2IntMaps.EMPTY_MAP;
LOG.warn("{} has no coverage data", full);
}
try {
writeCoverageFor(out, path, files);
writeCoverageFor(out, path, possiblePaths);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -116,24 +106,26 @@ class LuaCoverage {
private static IntSet getActiveLines(File file) throws IOException {
IntSet activeLines = new IntOpenHashSet();
Queue<Prototype> queue = new ArrayDeque<>();
try (InputStream stream = new FileInputStream(file)) {
var proto = LuaC.compile(stream, "@" + file.getPath());
Queue<Prototype> queue = new ArrayDeque<>();
queue.add(proto);
while ((proto = queue.poll()) != null) {
var lines = proto.lineinfo;
if (lines != null) {
for (var line : lines) {
activeLines.add(line);
}
}
if (proto.p != null) Collections.addAll(queue, proto.p);
}
} catch (CompileException e) {
throw new IllegalStateException("Cannot compile", e);
}
Prototype proto;
while ((proto = queue.poll()) != null) {
var lines = proto.lineInfo;
if (lines != null) {
for (var line : lines) {
activeLines.add(line);
}
}
if (proto.children != null) Collections.addAll(queue, proto.children);
}
return activeLines;
}
}

View File

@@ -23,7 +23,7 @@ public class ComputerTest {
try {
ComputerBootstrap.run("print('Hello') while true do end", ComputerBootstrap.MAX_TIME);
} catch (AssertionError e) {
if (e.getMessage().equals("test.lua:1: Too long without yielding")) return;
if (e.getMessage().equals("/test.lua:1: Too long without yielding")) return;
throw e;
}

View File

@@ -39,7 +39,9 @@ describe("The parallel library", function()
end)
it("passes errors to the caller", function()
expect.error(parallel.waitForAny, function() error("Test error") end):str_match("Test error$")
local ok, err = pcall(parallel.waitForAny, function() error("Test error") end)
if ok then fail("Expected function to error") end
expect(tostring(err)):str_match("Test error$")
end)
it("returns the number of the function that exited first", function()
@@ -98,7 +100,9 @@ describe("The parallel library", function()
end)
it("passes errors to the caller", function()
expect.error(parallel.waitForAll, function() error("Test error") end):str_match("Test error$")
local ok, err = pcall(parallel.waitForAll, function() error("Test error") end)
if ok then fail("Expected function to error") end
expect(tostring(err)):str_match("Test error$")
end)
it("completes all functions before exiting", function()

View File

@@ -28,8 +28,6 @@ describe("The Lua base library", function()
end)
describe("loadfile", function()
local loadfile = _G.native_loadfile or loadfile
local function make_file()
local tmp = fs.open("test-files/out.lua", "w")
tmp.write("return _ENV")
@@ -48,7 +46,7 @@ describe("The Lua base library", function()
it("prefixes the filename with @", function()
local info = debug.getinfo(loadfile("/rom/startup.lua"), "S")
expect(info):matches { short_src = "startup.lua", source = "@startup.lua" }
expect(info):matches { short_src = "/rom/startup.lua", source = "@/rom/startup.lua" }
end)
it("loads a file with the global environment", function()