This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
BrunoZockt's profile picture

[SOLVED] calling code from string

Started by BrunoZockt, 09 August 2018 - 06:58 PM
BrunoZockt #1
Posted 09 August 2018 - 08:58 PM
EDIT: Didn't want to open another thread because this is somewhat related, have a look at my current problem all the way down.

Hi Guys,

I need to run code from a string (e.g. "if true then print("bla") end"). loadstring() doesn't work for me since I need to call functions that are not global but only exist within my program. I found a script by Lupus590 which works perfectly, but somehow tables don't work.

This works

local foo =  "Hello World"
function test(text)
  print(text)
end
------------------Lupus script----------------------
local HOST_ENV = _ENV or getfenv() -- this is global
local OUR_ENV = {}
setmetatable(OUR_ENV, {__index = HOST_ENV, OUR_ENV = OUR_ENV})
setfenv(1, OUR_ENV) -- make the rest of this code use this environment
local function compile(chunk) -- returns compiled chunk or nil and error message
  if type(chunk) ~= "string" then
	error("expected string, got ".. type(chunk), 2)
  end
  local function findChunkName(var)
	for k,v in pairs(HOST_ENV) do
	  if v==var then
		return k
	  end
	end
	return "Unknown chunk"
  end
  return load(chunk, findChunkName(chunk), "t", OUR_ENV)
end
--------------------------------------------------
compile("test(foo)")()
This prints "Hello World" like I want it to.

[namedspoiler=This doesn't work][CODE]
local foo = {["bar"] = "Hello World"}
function test(text)
print(text)
end

——————Lupus script———————-
local HOST_ENV = _ENV or getfenv() – this is global
local OUR_ENV = {}
setmetatable(OUR_ENV, {__index = HOST_ENV, OUR_ENV = OUR_ENV})
setfenv(1, OUR_ENV) – make the rest of this code use this environment
local function compile(chunk) – returns compiled chunk or nil and error message
if type(chunk) ~= "string" then
error("expected string, got ".. type(chunk), 2)
end
local function findChunkName(var)
for k,v in pairs(HOST_ENV) do
if v==var then
return k
end
end
return "Unknown chunk"
end
return load(chunk, findChunkName(chunk), "t", OUR_ENV)
end
————————————————–
compile("test(foo.bar)")()
[/CODE][/namedspoiler]
This throws an [color=#ff0000][CODE]Unknown chunk:1: attempt to index ? (a nil value)[/CODE] [/color]error

I know it's not good to use scripts that you don't understand, but I tried to and couldn't because the documentation of load() is so bad. Does anybody understand why this is happening and if there is a way to fix it?

Thanks in advance!
Bruno
Edited on 20 August 2018 - 06:14 PM
KingofGamesYami #2
Posted 10 August 2018 - 01:23 AM
I need to run code from a string (e.g. "if true then print("bla") end"). loadstring() doesn't work for me since I need to call functions that are not global but only exist within my program.

First off, WHY? There is most likely a MUCH better solution to whatever problem you have than doing this (which is why the default behavior is to not allow it).

the documentation of load() is so bad

It's actually very good, IMO.

Finally, neither example #1 nor example #2 work for me. A minimal reproduction is easy:


local foo = "Hello World"
print( getfenv().foo ) --# nil

Unfortunately, it is completely impossible to get locally delcared variables in ComputerCraft. In regular Lua it is possible to do via the debug library, but this is disabled for security reasons.

The easiest workaround would be to not declare your stuff as local. But that is going to pollute the global env. and do various bad things. A better workaround is to pass all the required variables to the function when it is called. If this is impractical, you might want to consider a different approach to whatever problem you're attempting to solve.
Bomb Bloke #3
Posted 10 August 2018 - 02:21 AM
I need to run code from a string (e.g. "if true then print("bla") end").

First off, WHY? There is most likely a MUCH better solution to whatever problem you have than doing this (which is why the default behavior is to not allow it).

https://en.wikipedia.org/wiki/XY_problem

Does anybody understand why this is happening and if there is a way to fix it?

Lupus' snippet simply compiles your string to use the same environment table as the rest of your script does. The environment table holds global variables: but not local ones. As Yami points out, you're localising "foo", which is why neither of your examples work as written.

http://lua-users.org/wiki/ScopeTutorial

https://www.lua.org/pil/14.html

(Note that although _G is available to most all ComputerCraft scripts, they generally aren't using _G as their global environment table.)
BrunoZockt #4
Posted 10 August 2018 - 01:32 PM
I'm very confused now, I tested this several times yesterday, but apparently you are right. I can't get the first example to work anymore. However that at least explains that the Problem lies somewhere else, like you both explained.
Thanks!

My reason to do all this is that I want to make my program to resume after a server restart. To achieve this (without GPS) I need a list of every code that needs to be called to save it externally. So I'm rewriting my whole program to instead of directly calling code, adding it to the list, which gets called by a central loop.
Edited on 24 June 2019 - 09:17 PM
Bomb Bloke #5
Posted 10 August 2018 - 02:34 PM
Are you familiar with how ComputerCraft computers & turtles rely upon events to get information from the world? For example, if a turtle attempts to move, your script will typically yield until it's resumed with event data indicating whether the attempt was successful or not (yes, even if you're not calling os.pullEvent() yourself, many other functions are doing it for you!). You'll need to record this event information if you want to "replay" a list of old executed commands, or you'll have no idea which of them worked and which of them didn't.

If you only intend to store commands which haven't been executed, then I suppose my question would be: if you have time to record them, then why not just execute them?

You might consider taking a look at Lion4ever's old State Restore script. It's been a while since I last tested it, but it wouldn't surprise me if it'll straight-up do what you want. In any case, the basic principles behind it are probably the best for resuming arbitrary code - pay attention to how it works with events. It doesn't even need to record the code that's later replayed!

Any particular reason to avoid using a GPS, though? If you're on CC1.76 or later, you can even set one up using Ender Modems, thus providing coverage across an entire dimension.
BrunoZockt #6
Posted 11 August 2018 - 01:42 PM
Are you familiar with how ComputerCraft computers & turtles rely upon events to get information from the world? For example, if a turtle attempts to move, your script will typically yield until it's resumed with event data indicating whether the attempt was successful or not (yes, even if you're not calling os.pullEvent() yourself, many other functions are doing it for you!).
I was indeed familiar with this.

You'll need to record this event information if you want to "replay" a list of old executed commands, or you'll have no idea which of them worked and which of them didn't.
I agree, but I'm storing commands which haven't yet been executed.

If you only intend to store commands which haven't been executed, then I suppose my question would be: if you have time to record them, then why not just execute them?
Because if the program crashes and gets restarted I need to know which commands haven't yet been executed to then execute them.

You might consider taking a look at Lion4ever's old State Restore script. It's been a while since I last tested it, but it wouldn't surprise me if it'll straight-up do what you want. In any case, the basic principles behind it are probably the best for resuming arbitrary code - pay attention to how it works with events. It doesn't even need to record the code that's later replayed!
This is indeed very interesting, however I thought about doing something like this myself, and already have done it with some functions (e.g. instead of calling turtle.up(), I call a custom function which edits a variable to save that the level above bedrock did increase), but find it too complicated to extract the information of what still needs to be done out of the information what has already been done. If I'm not mistaken I'd need a complete list of every command that needs to be done, in which case I could also just use that list like I intended to do.

Any particular reason to avoid using a GPS, though? If you're on CC1.76 or later, you can even set one up using Ender Modems, thus providing coverage across an entire dimension.
Well, I haven't really worked with GPS yet, because I don't like the idea of needing to preinstall "satellites". If every Computercraft world had GPS "preinstalled" I would definitly use it, but I don't like the idea of depending on a system that probably isn't availabe. And as long as I haven't been proving wrong that it is possible to resume without GPS, I will try to achieve this for the sake of perfectionism.

However I really don't like my approach because it makes my code a total mess and seems very -don't know how to say it- unclean?
So I will definitly try out different approaches to see which satisfies me most.

Thanks for your extensive feedback!

Cheers
Lupus590 #7
Posted 11 August 2018 - 02:42 PM
You may also be interested in Checkpoint and LAMA.
Bomb Bloke #8
Posted 11 August 2018 - 03:47 PM
If you only intend to store commands which haven't been executed, then I suppose my question would be: if you have time to record them, then why not just execute them?

Because if the program crashes and gets restarted I need to know which commands haven't yet been executed to then execute them.

But how will you know that? That is to say, when resuming, what assurance will you have that the command at the top of the list hasn't been executed yet?

There's no way to write out your list to disk at the exact same time as a given task is performed, after all. You either have to write before or after. In the former case, you could be shut down before the task is performed, meaning it's struck off your list too early. In the latter case, you could be shut down after the task is performed, but before the list is updated: meaning you'll perform it a second time after the reboot.

However I really don't like my approach because it makes my code a total mess and seems very -don't know how to say it- unclean?

FWIW, bear in mind that you can stick function pointers into tables, and then index into those tables using strings. That's generally how a Lua library / API is already set up, conveniently. For example:

local funcToCall = "up"

turtle[funcToCall]()

You can also find all the main API tables inside of _G:

local funcToCall = {"turtle", "up"}

local func = _G

for i = 1, #funcToCall do
  func = func[funcToCall[i]]
end

func()
BrunoZockt #9
Posted 11 August 2018 - 06:21 PM
You may also be interested in Checkpoint and LAMA.
Thanks, LAMA was actually my first approach to orientate during ore excavation. Will check out Checkpoint!
BrunoZockt #10
Posted 11 August 2018 - 06:46 PM
There's no way to write out your list to disk at the exact same time as a given task is performed, after all. You either have to write before or after. In the former case, you could be shut down before the task is performed, meaning it's struck off your list too early. In the latter case, you could be shut down after the task is performed, but before the list is updated: meaning you'll perform it a second time after the reboot.
I am aware of that. However I'm willing to test this out and see how reliably it works. I decided to remove commands from the list after they've been executed, with the risk of it being executed twice rather than not at all. I do however believe that the probabilities are in my favor since the movement of the turtle takes ~0.4 seconds (just an educated guess, I don't know the exact time) and the editing of the file only takes a few milliseconds. Also I once tried to measure the time that it takes for a turtle to move with a small script, and it showed 1 tick meaning the line to get the time got executed instantly after the call of turtle.forward() and not after finishing of the movement. With that being said, I believe there is a high probability that the program terminates during such a movement (after the list update), which would be perfect. So I'm gonna try this out. Just to be save: Without GPS there is no way to guarantee that it works, right? Or is it possible to run the command and the documentation in parrallel?

FWIW, bear in mind that you can stick function pointers into tables, and then index into those tables using strings. That's generally how a Lua library / API is already set up, conveniently. For example:

local funcToCall = "up"

turtle[funcToCall]()

You can also find all the main API tables inside of _G:

local funcToCall = {"turtle", "up"}

local func = _G

for i = 1, #funcToCall do
  func = func[funcToCall[i]]
end

func()
I did know this was possible but didn't really think about this. This is indeed very helpful, thank you!
Edited on 11 August 2018 - 04:48 PM
Bomb Bloke #11
Posted 12 August 2018 - 05:53 AM
Or is it possible to run the command and the documentation in parrallel?

There's no way to write out your list to disk at the exact same time as a given task is performed, after all. You either have to write before or after.

You can sync things pretty closely, but there's no way to get a write done at the moment you're getting confirmation an action is complete. You're either doing one or the other.
Lyqyd #12
Posted 12 August 2018 - 06:14 AM
You'll really want to look at LAMA (linked earlier in the thread) to see how they do their persistent-across-reload location tracking. The fundamentals will apply directly to the problem you are working on.

Hint: Fuel levels make everything except turns much easier to track.
BrunoZockt #13
Posted 12 August 2018 - 04:36 PM
You can sync things pretty closely, but there's no way to get a write done at the moment you're getting confirmation an action is complete. You're either doing one or the other.
Thanks for the confirmation

You'll really want to look at LAMA (linked earlier in the thread) to see how they do their persistent-across-reload location tracking. The fundamentals will apply directly to the problem you are working on.

Hint: Fuel levels make everything except turns much easier to track.

Thank you for the reminder, I had almost forgotten about that solution!
BrunoZockt #14
Posted 19 August 2018 - 08:33 PM
Hi, I feel very stupid now because I still couldn't figure this out.

I'm still trying to write code to a table and then call every item on the table one by one. With the changed environment it should be able to call all the functions, but somehow I get this Error:
"Unknown chunk:3: attempt to call nil"

still Lupus script

local function compile(chunk) -- returns compiled chunk or nil and error message
  if type(chunk) ~= "string" then
	error("expected string, got ".. type(chunk), 2)
  end
  local function findChunkName(var)
	for k,v in pairs(HOST_ENV) do
	  if v==var then
		return k
	  end
	end
	return "Unknown chunk"
  end
  return load(chunk, findChunkName(chunk), "t", OUR_ENV)
end
The loop that is running

while #eventList > 0 do
	print(#eventList)
	os.pullEvent("key")
	compile(eventList[1])() --this is the interesting bit
	table.remove(eventList, 1)
	index = 2
	save("database/OCM/resume/eventList", eventList)
  end
And this is part of eventList.1

{
  "if level == 0 then\
	  if variables.floor == true then\
		insert('placeFloor()')\ --this throws the Error
	  end\
The called function. Notice that 'test' is never printed. It is located below the environment defining stuff by Lupus so that can't be the problem.

function insert(code)
  print("test")
  table.insert(eventList, index, code)
  index = index + 1
  save("database/OCM/resume/eventList", eventList)
end

What strikes me is that this is the only function that throws this error. I can't tell what is different about this one compared to the other functions I call.

What seems to me like the same, with the difference that it works:
Another chunk of EventList that makes no problems

{
  "while not turtle.forward() do\ --notice that I put a block in front of the turtle so that dig() is called
	if turtle.getFuelLevel == 0 then\
	  Fuel()\
	elseif dig() == false then\ --dig() is called
	  turtle.attack()\
	end\
  end"
the called function

function dig(gravel)
  local a = false
  OreCounter()
  stats["dug"] = stats["dug"] + 1
  if gravel then
	while turtle.dig() do
	  os.sleep(0.05)
	  a = true
	end
	return a
  else
	return turtle.dig()
  end
end

And just for your information, function insert() and function dig() are right next to each other in my code, nothing between them.

Maybe I've overlooked something but I don't know what else to do.
Edited on 19 August 2018 - 06:36 PM
Bomb Bloke #15
Posted 20 August 2018 - 05:56 AM
I'm scratching my head over how you're not getting an unfinished string error - your use of linebreaks should, well, break them. And what's with the backslashes? What are you intending to escape, exactly?

The way I'd typically define a multi-line string is:

{
  [[while not turtle.forward() do --notice that I put a block in front of the turtle so that dig() is called
        if turtle.getFuelLevel == 0 then
          Fuel()
        elseif dig() == false then --dig() is called
          turtle.attack()
        end
  end]]

Note that this still won't call dig() if the turtle is out of fuel, unless Fuel() successfully rectifies that for later iterations!

Why is your loop setting index to 2? Should it not be lowering its old value by one instead?

It'd be helpful to provide the full list of code, as I'm not sure you've provided enough information here to determine the cause of your reported error.
BrunoZockt #16
Posted 20 August 2018 - 06:25 AM
EDIT: Ok, now again. This time I will provide all the information so that there is no confusion.

Just repeating my last postError message: "Unknown chunk:3: attempt to call nil"
Lupus' script:

--This is at the very top of my program
local HOST_ENV = _ENV or getfenv() -- this is global
local OUR_ENV = {}

setmetatable(OUR_ENV, {__index = HOST_ENV, OUR_ENV = OUR_ENV})
setfenv(1, OUR_ENV) -- make the rest of this code use this environment

--This is below function insert() and function dig()
local function compile(chunk)
  if type(chunk) ~= "string" then
	    error("expected string, got ".. type(chunk), 2)
  end
  local function findChunkName(var)
	    for k,v in pairs(HOST_ENV) do
		  if v==var then
			    return k
		  end
	    end
	    return "Unknown chunk"
  end
  return load(chunk, findChunkName(chunk), "t", OUR_ENV) --interesting part
end
The loop that is running:

while #eventList > 0 do
	    compile(eventList[1])() --this is the interesting bit
	    table.remove(eventList, 1)
	    index = 2
	    save("database/OCM/resume/eventList", eventList) --irrelevant
  end

I'm scratching my head over how you're not getting an unfinished string error - your use of linebreaks should, well, break them. And what's with the backslashes? What are you intending to escape, exactly?

The way I'd typically define a multi-line string is:

{
  [[while not turtle.forward() do --notice that I put a block in front of the turtle so that dig() is called
		if turtle.getFuelLevel == 0 then
		  Fuel()
		elseif dig() == false then --dig() is called
		  turtle.attack()
		end
  end]]

I didn't provide the full string to simplify it, turns out that was a bad idea. The full string (referred to as "Unknown chunk" in the error message):
Spoiler

[[if level == 0 then
  if variables.floor == true then
    insert('placeFloor()') <---------------------line 3 throws the error (if I comment it out, line 5 throws the error and so on, meaning insert() can't be reached by compile() even though the environment is set correctly)
  end
  insert('digUp()')
  if ]]..tostring(first)..[[ and ]]..tostring(i)..[[ == 1 then
    insert('CompareAll(Compare, CompareBack, CompareUp)')
  else
    table.insert(toDoList, CompareDown)
  end
  insert('search()')
  insert('FreewayUp()')
  if ]]..tostring(first)..[[ and ]]..tostring(i)..[[ == 1 then
    insert('CompareAll(Compare, CompareBack, CompareDown)')
  else
    table.insert(toDoList, CompareUp)
  end
  insert('search()') 
elseif level == 1 then
  insert('digDown()')
  if ]]..tostring(first)..[[ and ]]..tostring(i)..[[ == 1 then
    insert('CompareAll(Compare, CompareBack, CompareDown)')
  else
    table.insert(toDoList, CompareUp)
  end
  insert('search()')
  if i == 2 and variables.mainTorches then
    insert('searching = false')
    insert('dig(true)')
  end
  insert('FreewayDown()')
  if ]]..tostring(first)..[[ and ]]..tostring(i)..[[ == 1 then
    insert('CompareAll(Compare, CompareBack, CompareUp)')
  else
    table.insert(toDoList, CompareDown)
  end
  insert('search()')
  if variables.floor == true then
    insert('placeFloor()')
  end
end]]
line 3 throws the error (if I comment it out, line 5 throws the error and so on, meaning insert() can't be reached by compile() even though the environment is set correctly)

Again, function insert():

function insert(code)
  table.insert(eventList, index, code)
  index = index + 1
  save("database/OCM/resume/eventList", eventList)
end

save-function, if interested (irrelevant)

function save(path, content)
local file = fs.open(path, "w")
  if type(content) == "table" then
    file.writeLine(textutils.serialize(content))
  else
   file.writeLine(content)
  end
file.close()
end
This is also where the backslashes come from, apparently they get written to the extern file because of the serialization. However that is irrelevant for now since that extern file isn't used anywhere else in the program yet.

Note that this still won't call dig() if the turtle is out of fuel, unless Fuel() successfully rectifies that for later iterations!
I don't really understand what you mean, this code works exactly like it's intended to.

[[while not turtle.forward() do
    if turtle.getFuelLevel == 0 then
	  Fuel()
    elseif dig() == false then
	  turtle.attack()
    end
  end]]
When called the turtle tries to move forward. If it has no fuel left, Fuel() is called and it tries again. If there is a block in the way it destroys it and tries again. If there is a mob it attacks it and tries again.
The point of this snippet only was to show that the calling of other functions like Fuel(), dig(), turtle.forward() and turtle.attack() works. Only insert() is making problems.

Why is your loop setting index to 2? Should it not be lowering its old value by one instead?
This is a little bit complicated and not relevant but let me try to explain:
SpoilerInsert() inserts a chunk of code into the toDoList. It uses index 2 and increasing because the inserted code needs to be called next. Index 1 however is the chunk of code that called insert() in the first place and needs to be removed after successfull execution. The index is set back to 2 now, so that insert() -if called- doesn't push away the chunk at index 1 which again needs to be removed.

It'd be helpful to provide the full list of code, as I'm not sure you've provided enough information here to determine the cause of your reported error.
I really don't recommend looking into it, it's horribly messy and I've provided everything that's related but I guess I can't stop you:
https://pastebin.com/fptxKN9n

I know this is a lot to read through and very difficult to wrap your head around but I really need the help :(/>
Edited on 20 August 2018 - 11:38 AM
Bomb Bloke #17
Posted 20 August 2018 - 03:14 PM
I don't really understand what you mean, this code works exactly like it's intended to.

As an example, this code won't error out:

if true then
  print("hello")
elseif error("goodbye") then
  print("argh")
end

… and this snippet won't attempt to evaluate tbl.key if tbl isn't a table (hence avoiding an "attempt to index into something that isn't a table" error):

if type(tbl) == "table" and tbl.key == "pie" then

Evaluation stops as soon as the final result is known - the Lua VM doesn't bother to parse the rest of your conditional statement if it doesn't need to. I mentioned it because without access to your full source, I had no way to know whether Fuel() could return prior to refuelling the turtle. If it could, then that could in turn prevent dig() from ever being called, as it's contingent on a certain "elseif" being reached within your code.

Insert() inserts a chunk of code into the toDoList. It uses index 2 and increasing because the inserted code needs to be called next. Index 1 however is the chunk of code that called insert() in the first place and needs to be removed after successfull execution. The index is set back to 2 now, so that insert() -if called- doesn't push away the chunk at index 1 which again needs to be removed.

Say you're calling chunk 1 from slot 1. It inserts chunk 2 into slot 2, and then chunk 3 into slot 3. "index" has been increased to 4 by this point, by the insert() function.

Chunk 1 returns and is removed from the list, chunk 2 goes into slot 1, chunk 3 goes into slot 2, index gets "set to 2" (as opposed to "reduced by 1").

Chunk 2 starts execution and inserts chunk 4 into slot 2, bumping chunk 3 back into slot 3 again…

The result isn't the first-in-first-out system I would expect.

I really don't recommend looking into it, it's horribly messy and I've provided everything that's related but I guess I can't stop you:

You're localising "insert" on line 99. If you assigned a function pointer to it in the global scope before doing that you'd technically have two different variables, but since you're doing it after, your function declaration is just overwriting the table pointer stored within the local variable.

Best to use separate names there.
Edited on 20 August 2018 - 01:30 PM
BrunoZockt #18
Posted 20 August 2018 - 05:06 PM
Say you're calling chunk 1 from slot 1. It inserts chunk 2 into slot 2, and then chunk 3 into slot 3. "index" has been increased to 4 by this point, by the insert() function.

Chunk 1 returns and is removed from the list, chunk 2 goes into slot 1, chunk 3 goes into slot 2, index gets "set to 2" (as opposed to "reduced by 1").

Chunk 2 starts execution and inserts chunk 4 into slot 2, bumping chunk 3 back into slot 3 again…

The result isn't the first-in-first-out system I would expect.
I know that it's a little bit confusing, but you understood it right, the newly added chunks are supposed to push all other entries (except the 1st since it is already running) "further down" because the most recent is always the next that needs to be called.

You're localising "insert" on line 99. If you assigned a function pointer to it in the global scope before doing that you'd technically have two different variables, but since you're doing it after, your function declaration is just overwriting the table pointer stored within the local variable.

Best to use separate names there.
Thank you soooo much, I wouldn't have found that in a thousend years, I was so focused on the "relevant" snippets… However I have no idea where that table declaration even comes from, I never used a table called "insert" :blink:/>