From 5b942ff9c1b8287efbfd0277c8f36c25f75b570d Mon Sep 17 00:00:00 2001 From: SquidDev Date: Sun, 10 Mar 2019 12:24:55 +0000 Subject: [PATCH] Some changes to Lua machines and string loading - Share the ILuaContext across all method calls, as well as shifting it into an anonymous class. - Move the load/loadstring prefixing into bios.lua - Be less militant in prefixing chunk names: - load will no longer do any auto-prefixing. - loadstring will not prefix when there no chunk name is supplied. Before we would do `"=" .. supplied_program`, which made no sense. --- .../computercraft/api/lua/ILuaContext.java | 10 +- .../core/lua/CobaltLuaMachine.java | 316 +++++++----------- .../assets/computercraft/lua/bios.lua | 17 + .../computercraft/lua/rom/programs/lua.lua | 4 +- src/test/resources/test-rom/mcfly.lua | 27 +- .../resources/test-rom/spec/base_spec.lua | 44 +++ 6 files changed, 216 insertions(+), 202 deletions(-) create mode 100644 src/test/resources/test-rom/spec/base_spec.lua diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaContext.java b/src/main/java/dan200/computercraft/api/lua/ILuaContext.java index c5e82dd36..62ed6cc33 100644 --- a/src/main/java/dan200/computercraft/api/lua/ILuaContext.java +++ b/src/main/java/dan200/computercraft/api/lua/ILuaContext.java @@ -32,7 +32,11 @@ public interface ILuaContext * intercepted, or the computer will leak memory and end up in a broken state. */ @Nonnull - Object[] pullEvent( @Nullable String filter ) throws LuaException, InterruptedException; + default Object[] pullEvent( @Nullable String filter ) throws LuaException, InterruptedException { + Object[] results = pullEventRaw( filter ); + if( results.length >= 1 && results[0].equals( "terminate" ) ) throw new LuaException( "Terminated", 0 ); + return results; + } /** * The same as {@link #pullEvent(String)}, except "terminated" events are ignored. Only use this if you want to @@ -47,7 +51,9 @@ public interface ILuaContext * @see #pullEvent(String) */ @Nonnull - Object[] pullEventRaw( @Nullable String filter ) throws InterruptedException; + default Object[] pullEventRaw( @Nullable String filter ) throws InterruptedException { + return yield( new Object[] { filter } ); + } /** * Yield the current coroutine with some arguments until it is resumed. This method is exactly equivalent to diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index 6f2799c49..e7f2eff27 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -21,7 +21,6 @@ import org.squiddev.cobalt.compiler.LoadState; import org.squiddev.cobalt.debug.DebugFrame; import org.squiddev.cobalt.debug.DebugHandler; import org.squiddev.cobalt.debug.DebugState; -import org.squiddev.cobalt.function.LibFunction; import org.squiddev.cobalt.function.LuaFunction; import org.squiddev.cobalt.function.VarArgFunction; import org.squiddev.cobalt.lib.*; @@ -37,7 +36,6 @@ import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import static org.squiddev.cobalt.Constants.NONE; import static org.squiddev.cobalt.ValueFactory.valueOf; import static org.squiddev.cobalt.ValueFactory.varargsOf; import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKED; @@ -55,6 +53,7 @@ public class CobaltLuaMachine implements ILuaMachine private final Computer m_computer; private final TimeoutState timeout; private final TimeoutDebugHandler debug; + private final ILuaContext context = new CobaltLuaContext(); private LuaState m_state; private LuaTable m_globals; @@ -99,10 +98,6 @@ public class CobaltLuaMachine implements ILuaMachine m_globals.load( state, new Bit32Lib() ); if( ComputerCraft.debug_enable ) m_globals.load( state, new DebugLib() ); - // Register custom load/loadstring provider which automatically adds prefixes. - m_globals.rawset( "load", new PrefixWrapperFunction( m_globals.rawget( "load" ), 0 ) ); - m_globals.rawset( "loadstring", new PrefixWrapperFunction( m_globals.rawget( "loadstring" ), 1 ) ); - // Remove globals we don't want to expose m_globals.rawset( "collectgarbage", Constants.NIL ); m_globals.rawset( "dofile", Constants.NIL ); @@ -238,148 +233,13 @@ public class CobaltLuaMachine implements ILuaMachine table.rawset( methodName, new VarArgFunction() { @Override - public Varargs invoke( final LuaState state, Varargs _args ) throws LuaError + public Varargs invoke( final LuaState state, Varargs args ) throws LuaError { - Object[] arguments = toObjects( _args, 1 ); + Object[] arguments = toObjects( args, 1 ); Object[] results; try { - results = apiObject.callMethod( new ILuaContext() - { - @Nonnull - @Override - public Object[] pullEvent( String filter ) throws LuaException, InterruptedException - { - Object[] results = pullEventRaw( filter ); - if( results.length >= 1 && results[0].equals( "terminate" ) ) - { - throw new LuaException( "Terminated", 0 ); - } - return results; - } - - @Nonnull - @Override - public Object[] pullEventRaw( String filter ) throws InterruptedException - { - return yield( new Object[] { filter } ); - } - - @Nonnull - @Override - public Object[] yield( Object[] yieldArgs ) throws InterruptedException - { - try - { - Varargs results = LuaThread.yieldBlocking( state, toValues( yieldArgs ) ); - return toObjects( results, 1 ); - } - catch( LuaError e ) - { - throw new IllegalStateException( e ); - } - } - - @Override - public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException - { - // Issue command - final long taskID = MainThread.getUniqueTaskID(); - final ITask iTask = new ITask() - { - @Nonnull - @Override - public Computer getOwner() - { - return m_computer; - } - - @Override - public void execute() - { - try - { - Object[] results = task.execute(); - if( results != null ) - { - Object[] eventArguments = new Object[results.length + 2]; - eventArguments[0] = taskID; - eventArguments[1] = true; - System.arraycopy( results, 0, eventArguments, 2, results.length ); - m_computer.queueEvent( "task_complete", eventArguments ); - } - else - { - m_computer.queueEvent( "task_complete", new Object[] { taskID, true } ); - } - } - catch( LuaException e ) - { - m_computer.queueEvent( "task_complete", new Object[] { - taskID, false, e.getMessage() - } ); - } - catch( Throwable t ) - { - if( ComputerCraft.logPeripheralErrors ) - { - ComputerCraft.log.error( "Error running task", t ); - } - m_computer.queueEvent( "task_complete", new Object[] { - taskID, false, "Java Exception Thrown: " + t.toString() - } ); - } - } - }; - if( MainThread.queueTask( iTask ) ) - { - return taskID; - } - else - { - throw new LuaException( "Task limit exceeded" ); - } - } - - @Override - public Object[] executeMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException, InterruptedException - { - // Issue task - final long taskID = issueMainThreadTask( task ); - - // Wait for response - while( true ) - { - Object[] response = pullEvent( "task_complete" ); - if( response.length >= 3 && response[1] instanceof Number && response[2] instanceof Boolean ) - { - if( ((Number) response[1]).intValue() == taskID ) - { - Object[] returnValues = new Object[response.length - 3]; - if( (Boolean) response[2] ) - { - // Extract the return values from the event and return them - System.arraycopy( response, 3, returnValues, 0, returnValues.length ); - return returnValues; - } - else - { - // Extract the error message from the event and raise it - if( response.length >= 4 && response[3] instanceof String ) - { - throw new LuaException( (String) response[3] ); - } - else - { - throw new LuaException(); - } - } - } - } - } - - } - }, method, arguments ); + results = apiObject.callMethod( context, method, arguments ); } catch( InterruptedException e ) { @@ -571,54 +431,6 @@ public class CobaltLuaMachine implements ILuaMachine return objects; } - private static class PrefixWrapperFunction extends VarArgFunction - { - private static final LuaString FUNCTION_STR = valueOf( "function" ); - private static final LuaString EQ_STR = valueOf( "=" ); - - private final LibFunction underlying; - - public PrefixWrapperFunction( LuaValue wrap, int opcode ) - { - LibFunction underlying = (LibFunction) wrap; - - this.underlying = underlying; - this.opcode = opcode; - this.name = underlying.debugName(); - this.env = underlying.getfenv(); - } - - @Override - public Varargs invoke( LuaState state, Varargs args ) throws LuaError, UnwindThrowable - { - switch( opcode ) - { - case 0: // "load", // ( func [,chunkname] ) -> chunk | nil, msg - { - LuaValue func = args.arg( 1 ).checkFunction(); - LuaString chunkname = args.arg( 2 ).optLuaString( FUNCTION_STR ); - if( !chunkname.startsWith( '@' ) && !chunkname.startsWith( '=' ) ) - { - chunkname = OperationHelper.concat( EQ_STR, chunkname ); - } - return underlying.invoke( state, varargsOf( func, chunkname ) ); - } - case 1: // "loadstring", // ( string [,chunkname] ) -> chunk | nil, msg - { - LuaString script = args.arg( 1 ).checkLuaString(); - LuaString chunkname = args.arg( 2 ).optLuaString( script ); - if( !chunkname.startsWith( '@' ) && !chunkname.startsWith( '=' ) ) - { - chunkname = OperationHelper.concat( EQ_STR, chunkname ); - } - return underlying.invoke( state, varargsOf( script, chunkname ) ); - } - } - - return NONE; - } - } - /** * A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly. */ @@ -700,6 +512,126 @@ public class CobaltLuaMachine implements ILuaMachine } } + private class CobaltLuaContext implements ILuaContext + { + @Nonnull + @Override + public Object[] yield( Object[] yieldArgs ) throws InterruptedException + { + try + { + LuaState state = m_state; + if( state == null ) throw new InterruptedException(); + Varargs results = LuaThread.yieldBlocking( state, toValues( yieldArgs ) ); + return toObjects( results, 1 ); + } + catch( LuaError e ) + { + throw new IllegalStateException( e.getMessage() ); + } + } + + @Override + public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException + { + // Issue command + final long taskID = MainThread.getUniqueTaskID(); + final ITask iTask = new ITask() + { + @Nonnull + @Override + public Computer getOwner() + { + return m_computer; + } + + @Override + public void execute() + { + try + { + Object[] results = task.execute(); + if( results != null ) + { + Object[] eventArguments = new Object[results.length + 2]; + eventArguments[0] = taskID; + eventArguments[1] = true; + System.arraycopy( results, 0, eventArguments, 2, results.length ); + m_computer.queueEvent( "task_complete", eventArguments ); + } + else + { + m_computer.queueEvent( "task_complete", new Object[] { taskID, true } ); + } + } + catch( LuaException e ) + { + m_computer.queueEvent( "task_complete", new Object[] { + taskID, false, e.getMessage() + } ); + } + catch( Throwable t ) + { + if( ComputerCraft.logPeripheralErrors ) + { + ComputerCraft.log.error( "Error running task", t ); + } + m_computer.queueEvent( "task_complete", new Object[] { + taskID, false, "Java Exception Thrown: " + t.toString() + } ); + } + } + }; + if( MainThread.queueTask( iTask ) ) + { + return taskID; + } + else + { + throw new LuaException( "Task limit exceeded" ); + } + } + + @Override + public Object[] executeMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException, InterruptedException + { + // Issue task + final long taskID = issueMainThreadTask( task ); + + // Wait for response + while( true ) + { + Object[] response = pullEvent( "task_complete" ); + if( response.length >= 3 && response[1] instanceof Number && response[2] instanceof Boolean ) + { + if( ((Number) response[1]).intValue() == taskID ) + { + Object[] returnValues = new Object[response.length - 3]; + if( (Boolean) response[2] ) + { + // Extract the return values from the event and return them + System.arraycopy( response, 3, returnValues, 0, returnValues.length ); + return returnValues; + } + else + { + // Extract the error message from the event and raise it + if( response.length >= 4 && response[3] instanceof String ) + { + throw new LuaException( (String) response[3] ); + } + else + { + throw new LuaException(); + } + } + } + } + } + + } + } + private static class HardAbortError extends Error { private static final long serialVersionUID = 7954092008586367501L; diff --git a/src/main/resources/assets/computercraft/lua/bios.lua b/src/main/resources/assets/computercraft/lua/bios.lua index 0b9719999..5cbc2267a 100644 --- a/src/main/resources/assets/computercraft/lua/bios.lua +++ b/src/main/resources/assets/computercraft/lua/bios.lua @@ -2,9 +2,23 @@ local nativegetfenv = getfenv if _VERSION == "Lua 5.1" then -- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it + local type = type local nativeload = load local nativeloadstring = loadstring local nativesetfenv = setfenv + + --- Historically load/loadstring would handle the chunk name as if it has + -- been prefixed with "=". We emulate that behaviour here. + local function prefix(chunkname) + if type(chunkname) ~= "string" then return chunkname end + local head = chunkname:sub(1, 1) + if head == "=" or head == "@" then + return chunkname + else + return "=" .. chunkname + end + end + function load( x, name, mode, env ) if type( x ) ~= "string" and type( x ) ~= "function" then error( "bad argument #1 (expected string or function, got " .. type( x ) .. ")", 2 ) @@ -62,7 +76,10 @@ if _VERSION == "Lua 5.1" then math.log10 = nil table.maxn = nil else + loadstring = function(string, chunkname) return nativeloadstring(string, prefix( chunkname )) + -- Inject a stub for the old bit library + end _G.bit = { bnot = bit32.bnot, band = bit32.band, diff --git a/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua b/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua index 758ca74ef..f48906b39 100644 --- a/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua +++ b/src/main/resources/assets/computercraft/lua/rom/programs/lua.lua @@ -49,8 +49,8 @@ while bRunning do end local nForcePrint = 0 - local func, e = load( s, "lua", "t", tEnv ) - local func2, e2 = load( "return _echo("..s..");", "lua", "t", tEnv ) + local func, e = load( s, "=lua", "t", tEnv ) + local func2, e2 = load( "return _echo("..s..");", "=lua", "t", tEnv ) if not func then if func2 then func = func2 diff --git a/src/test/resources/test-rom/mcfly.lua b/src/test/resources/test-rom/mcfly.lua index c7a209c2c..245b061bd 100644 --- a/src/test/resources/test-rom/mcfly.lua +++ b/src/test/resources/test-rom/mcfly.lua @@ -137,7 +137,7 @@ function expect_mt:type(exp_type) return self end -local function are_same(eq, left, right) +local function matches(eq, exact, left, right) if left == right then return true end local ty = type(left) @@ -150,12 +150,14 @@ local function are_same(eq, left, right) -- Verify all pairs in left are equal to those in right for k, v in pairs(left) do - if not are_same(eq, v, right[k]) then return false end + if not matches(eq, exact, v, right[k]) then return false end end - -- And verify all pairs in right are present in left - for k in pairs(right) do - if left[k] == nil then return false end + if exact then + -- And verify all pairs in right are present in left + for k in pairs(right) do + if left[k] == nil then return false end + end end return true @@ -167,13 +169,26 @@ end -- @param value The value to check for structural equivalence -- @raises If they are not equivalent function expect_mt:same(value) - if not are_same({}, self.value, value) then + if not matches({}, true, self.value, value) then fail(("Expected %s\n but got %s"):format(format(value), format(self.value))) end return self end +--- Assert that this expectation contains all fields mentioned +-- in the provided object. +-- +-- @param value The value to check against +-- @raises If this does not match the provided value +function expect_mt:matches(value) + if not matches({}, false, value, self.value) then + fail(("Expected %s\nto match %s"):format(format(self.value), format(value))) + end + + return self +end + --- Construct a new expectation from the provided value -- -- @param value The value to apply assertions to diff --git a/src/test/resources/test-rom/spec/base_spec.lua b/src/test/resources/test-rom/spec/base_spec.lua new file mode 100644 index 000000000..11aabb396 --- /dev/null +++ b/src/test/resources/test-rom/spec/base_spec.lua @@ -0,0 +1,44 @@ +describe("The Lua base library", function() + describe("loadfile", 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" } + end) + end) + + describe("loadstring", function() + it("prefixes the chunk name with '='", function() + local info = debug.getinfo(loadstring("return 1", "name"), "S") + expect(info):matches { short_src = "name", source = "=name" } + end) + + it("does not prefix for unnamed chunks", function() + local info = debug.getinfo(loadstring("return 1"), "S") + expect(info):matches { short_src = '[string "return 1"]', source = "return 1", } + end) + + it("does not prefix when already prefixed", function() + local info = debug.getinfo(loadstring("return 1", "@file.lua"), "S") + expect(info):matches { short_src = "file.lua", source = "@file.lua" } + + info = debug.getinfo(loadstring("return 1", "=file.lua"), "S") + expect(info):matches { short_src = "file.lua", source = "=file.lua" } + end) + end) + + describe("load", function() + local function generator(parts) + return coroutine.wrap(function() + for i = 1, #parts do coroutine.yield(parts[i]) end + end) + end + + it("does not prefix the chunk name with '='", function() + local info = debug.getinfo(load("return 1", "name"), "S") + expect(info):matches { short_src = "[string \"name\"]", source = "name" } + + info = debug.getinfo(load(generator { "return 1" }, "name"), "S") + expect(info):matches { short_src = "[string \"name\"]", source = "name" } + end) + end) +end)