Creating New Scripts Dynamically in Lua

Posted by bazola on Programmers See other posts from Programmers or by bazola
Published on 2014-08-22T23:08:49Z Indexed on 2014/08/23 10:34 UTC
Read the original article Hit count: 364

Right now this is just a crazy idea that I had, but I was able to implement the code and get it working properly. I am not entirely sure of what the use cases would be just yet.

What this code does is create a new Lua script file in the project directory. The ScriptWriter takes as arguments the file name, a table containing any arguments that the script should take when created, and a table containing any instance variables to create by default. My plan is to extend this code to create new functions based on inputs sent in during its creation as well.

What makes this cool is that the new file is both generated and loaded dynamically on the fly. Theoretically you could get this code to generate and load any script imaginable. One use case I can think of is an AI that creates scripts to map out it's functions, and creates new scripts for new situations or environments. At this point, this is all theoretical, though.

Here is the test code that is creating the new script and then immediately loading it and calling functions from it:

function Card:doScriptWriterThing()
    local scriptName = "ScriptIAmMaking"

    local scripter = scriptWriter:new(scriptName, {"argumentName"}, {name = "'test'", one = 1})
    scripter:makeFileForLoadedSettings()

    local loadedScript = require (scriptName)
    local scriptInstance = loadedScript:new("sayThis")
    print(scriptInstance:get_name()) --will print test
    print(scriptInstance:get_one()) -- will print 1
    scriptInstance:set_one(10000)
    print(scriptInstance:get_one()) -- will print 10000
    print(scriptInstance:get_argumentName()) -- will print sayThis
    scriptInstance:set_argumentName("saySomethingElse")
    print(scriptInstance:get_argumentName()) --will print saySomethingElse
end

Here is ScriptWriter.lua

local ScriptWriter = {}

local twoSpaceIndent = "  "
local equalsWithSpaces = " = "
local newLine = "\n"

--scriptNameToCreate must be a string
--argumentsForNew and instanceVariablesToCreate must be tables and not nil
function ScriptWriter:new(scriptNameToCreate, argumentsForNew, instanceVariablesToCreate)

    local instance = setmetatable({}, { __index = self })

    instance.name = scriptNameToCreate
    instance.newArguments = argumentsForNew
    instance.instanceVariables = instanceVariablesToCreate
    instance.stringList = {}

    return instance
end
function ScriptWriter:makeFileForLoadedSettings()
    self:buildInstanceMetatable()
    self:buildInstanceCreationMethod()
    self:buildSettersAndGetters()
    self:buildReturn()
    self:writeStringsToFile()
end

--very first line of any script that will have instances
function ScriptWriter:buildInstanceMetatable()
    table.insert(self.stringList, "local " .. self.name .. " = {}" .. newLine)
    table.insert(self.stringList, newLine)
end

--every script made this way needs a new method to create its instances
function ScriptWriter:buildInstanceCreationMethod()
    --new() function declaration
    table.insert(self.stringList, ("function " .. self.name .. ":new("))
    self:buildNewArguments()
    table.insert(self.stringList, ")" .. newLine)

    --first line inside :new() function
    table.insert(self.stringList, twoSpaceIndent .. "local instance = setmetatable({}, { __index = self })" .. newLine)

    --add designated arguments inside :new()
    self:buildNewArgumentVariables()

    --create the instance variables with the loaded values
    for key,value in pairs(self.instanceVariables) do
      table.insert(self.stringList, twoSpaceIndent .. "instance." .. key .. equalsWithSpaces .. value .. newLine)
    end

    --close the :new() function
    table.insert(self.stringList, twoSpaceIndent .. "return instance" .. newLine)
    table.insert(self.stringList, "end" .. newLine)
    table.insert(self.stringList, newLine)
end
function ScriptWriter:buildNewArguments()
     --if there are arguments for :new(), add them
    for key,value in ipairs(self.newArguments) do
      table.insert(self.stringList, value)
      table.insert(self.stringList, ", ") 
    end
    if next(self.newArguments) ~= nil then --makes sure the table is not empty first
      table.remove(self.stringList) --remove the very last element, which will be the extra ", "
    end
end
function ScriptWriter:buildNewArgumentVariables()
    --add the designated arguments to :new()
    for key, value in ipairs(self.newArguments) do
      table.insert(self.stringList, twoSpaceIndent .. "instance." .. value .. equalsWithSpaces .. value .. newLine)
    end
end

--the instance variables need separate code because their names have to be the key and not the argument name
function ScriptWriter:buildSettersAndGetters()
    for key,value in ipairs(self.newArguments) do
        self:buildArgumentSetter(value)
        self:buildArgumentGetter(value)
        table.insert(self.stringList, newLine)
    end
    for key,value in pairs(self.instanceVariables) do
        self:buildInstanceVariableSetter(key, value)
        self:buildInstanceVariableGetter(key, value)
        table.insert(self.stringList, newLine)
    end
end
--code for arguments passed in
function ScriptWriter:buildArgumentSetter(variable)
    table.insert(self.stringList, "function " .. self.name .. ":set_" .. variable .. "(newValue)" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "self." .. variable .. equalsWithSpaces ..  "newValue" .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end
function ScriptWriter:buildArgumentGetter(variable)
    table.insert(self.stringList, "function " .. self.name .. ":get_" .. variable .. "()" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "return " .. "self." .. variable .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end
--code for instance variable values passed in
function ScriptWriter:buildInstanceVariableSetter(key, variable)
    table.insert(self.stringList, "function " .. self.name .. ":set_" .. key .. "(newValue)" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "self." .. key .. equalsWithSpaces .. "newValue" .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end
function ScriptWriter:buildInstanceVariableGetter(key, variable)
    table.insert(self.stringList, "function " .. self.name .. ":get_" .. key .. "()" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "return " .. "self." .. key .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end

--last line of any script that will have instances
function ScriptWriter:buildReturn()
    table.insert(self.stringList, "return " .. self.name)
end

function ScriptWriter:writeStringsToFile()
    local fileName = (self.name .. ".lua")
    file = io.open(fileName, 'w')
    for key,value in ipairs(self.stringList) do
      file:write(value)
    end
    file:close()
end

return ScriptWriter

And here is what the code provided will generate:

local ScriptIAmMaking = {}

function ScriptIAmMaking:new(argumentName)
    local instance = setmetatable({}, { __index = self })
    instance.argumentName = argumentName
    instance.name = 'test'
    instance.one = 1
    return instance
end

function ScriptIAmMaking:set_argumentName(newValue)
    self.argumentName = newValue
end
function ScriptIAmMaking:get_argumentName()
    return self.argumentName
end

function ScriptIAmMaking:set_name(newValue)
    self.name = newValue
end
function ScriptIAmMaking:get_name()
    return self.name
end

function ScriptIAmMaking:set_one(newValue)
    self.one = newValue
end
function ScriptIAmMaking:get_one()
    return self.one
end

return ScriptIAmMaking

All of this is generated with these calls:

local scripter = scriptWriter:new(scriptName, {"argumentName"}, {name = "'test'", one = 1})
scripter:makeFileForLoadedSettings()

I am not sure if I am correct that this could be useful in certain situations. What I am looking for is feedback on the readability of the code, and following Lua best practices. I would also love to hear whether this approach is a valid one, and whether the way that I have done things will be extensible.

© Programmers or respective owner

Related posts about artificial-intelligence

Related posts about code-generation