From 3fdc6c01f0f7211ddb80e9cd8d8cb6fcf694af0e Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 23 Dec 2022 11:37:40 +0100 Subject: [PATCH] Added repeated instructions and action options. --- .github/workflows/deploy-docs2.yml | 39 ++++--- docs/src/guide/movescript/reference.md | 4 + docs/src/guide/movescript/spec.md | 16 ++- install.lua | 24 ----- src/movescript.lua | 136 +++++++++++++++++++++---- tester.lua | 25 +++++ 6 files changed, 179 insertions(+), 65 deletions(-) delete mode 100644 install.lua create mode 100644 tester.lua diff --git a/.github/workflows/deploy-docs2.yml b/.github/workflows/deploy-docs2.yml index 164d8b0..2432091 100644 --- a/.github/workflows/deploy-docs2.yml +++ b/.github/workflows/deploy-docs2.yml @@ -22,27 +22,6 @@ jobs: - name: Checkout uses: actions/checkout@v3 -# Disabled minification for now. - # - name: Install Lua - # uses: ljmf00/setup-lua@v1.0.0 - # with: - # lua-version: 5.3 - # install-luarocks: true - - # - name: Minify Scripts - # run: | - # script_dir=docs/src/.vuepress/public/scripts - # rm -rf $script_dir - # mkdir $script_dir - # lua minify.lua minify src/movescript.lua > $script_dir/movescript.lua - # lua minify.lua minify src/itemscript.lua > $script_dir/itemscript.lua - - # - name: Clean Up Lua Artifacts - # run: | - # rm -rf .lua - # rm -rf .luarocks - # rm -rf .source - - name: Copy scripts to Docs public assets run: | script_dir=docs/src/.vuepress/public/scripts @@ -50,6 +29,24 @@ jobs: mkdir $script_dir cp src/* $script_dir/ + - name: Install Lua + uses: ljmf00/setup-lua@v1.0.0 + with: + lua-version: 5.3 + install-luarocks: true + + - name: Minify Scripts + run: | + script_dir=docs/src/.vuepress/public/scripts + lua minify.lua minify src/movescript.lua > $script_dir/movescript-min.lua + lua minify.lua minify src/itemscript.lua > $script_dir/itemscript-min.lua + + - name: Clean Up Lua Artifacts + run: | + rm -rf .lua + rm -rf .luarocks + rm -rf .source + - name: Setup Node uses: actions/setup-node@v3 diff --git a/docs/src/guide/movescript/reference.md b/docs/src/guide/movescript/reference.md index b695665..31a2aec 100644 --- a/docs/src/guide/movescript/reference.md +++ b/docs/src/guide/movescript/reference.md @@ -7,6 +7,10 @@ local ms = require("movescript") ms.run("2F") ``` +## `parse(script, settings)` + +Parses the given `script` string and returns a table containing the parsed instructions to be executed. This is mostly useful for debugging your scripts. + ## `run(script, settings)` Runs the given `script` string as a movescript, and optionally a `settings` table can be provided. Otherwise, [default settings](settings.md) will be used. diff --git a/docs/src/guide/movescript/spec.md b/docs/src/guide/movescript/spec.md index e8e3ffd..9eb23a6 100644 --- a/docs/src/guide/movescript/spec.md +++ b/docs/src/guide/movescript/spec.md @@ -2,7 +2,7 @@ Every movescript must follow the outline defined in this specification. -Each script consists of zero or more **instructions**, separated by zero or more whitespace characters. +Each script consists of zero or more **instructions** or **repeated instructions**, separated by zero or more whitespace characters. ## Instructions @@ -10,11 +10,23 @@ An instruction consists of an optional positive integer number, followed by a re ```lua -- The regex used to parse instructions. -instruction = string.find(script, "%W*(%d*%u%l*)%W*") +instruction = string.find(script, "%s*(%d*%u%l*)%s*") ``` Each instruction can be split into two parts: the **action**, and the **count**. The action is the textual part of the instruction, and maps to a turtle behavior. The count is the optional numerical part of the instruction, and defaults to `1` if no number is provided. +Here are some examples of valid instructions: `3F`, `U`, `1R` + +Some instructions may allow you to specify additional options. These can be defined as key-value pairs in parentheses after the action part. + +For example: `4A(delay=0.25, file=tmp.txt)` + +## Repeated Instructions + +A repeated instruction is a grouping of instructions that are repeated a specified number of times. It's denoted as a positive integer number, followed by a series of [instructions](#instructions) within parentheses. + +For example: `22(AF)` - We execute the instructions `A` and `F` 22 times. + ## Actions The following table lists all actions that are available in Movescript. Attempting to invoke an action not listed here will result in an error that will terminate your script. diff --git a/install.lua b/install.lua deleted file mode 100644 index bd7d558..0000000 --- a/install.lua +++ /dev/null @@ -1,24 +0,0 @@ ---[[ -Installation script for installing all libraries. - -Run `wget run https://raw.githubusercontent.com/andrewlalis/movescript/main/install.lua` -to run the installer on your device. -]]-- - -BASE_URL = "https://raw.githubusercontent.com/andrewlalis/movescript/main/" - -SCRIPTS = { - "movescript.lua", - "itemscript.lua" -} - --- Create a local executable to re-install, instead of having to run this file via wget. -local f = io.open("install-movescript.lua", "w") -for _, script in pairs(SCRIPTS) do - url = BASE_URL .. script - cmd = "wget " .. url .. " " .. script - shell.run(cmd) - f:write("if fs.exists(\"" .. script .. "\") then fs.delete(\"" .. script .. "\") end") - f:write("shell.run(\"" .. cmd .. "\")") -end -f:close() diff --git a/src/movescript.lua b/src/movescript.lua index 1e32f57..5188896 100644 --- a/src/movescript.lua +++ b/src/movescript.lua @@ -10,6 +10,10 @@ that you don't need to get tired of typing "turtle.forward()" over and over. VERSION = "0.0.1" local t = turtle +-- For testing purposes, if the turtle API is not present, we inject our own. +if not t then t = { + getFuelLimit = function() return 1000000000 end +} end -- The movescript module. Functions defined within this table are exported. local movescript = {} @@ -167,25 +171,121 @@ local function executeInstruction(instruction, settings) end end +local INSTRUCTION_TYPES = { + repeated = 1, + instruction = 2 +} + +local function parseInstructionOptions(text, settings) + local idx, endIdx = string.find(text, "%b()") + if idx == nil or endIdx - idx < 4 then return nil end + local optionPairsText = string.sub(text, idx, endIdx) + debug("Parsing instruction options: " .. optionPairsText, settings) + local options = {} + local nextIdx = 1 + while nextIdx < string.len(optionPairsText) do + idx, endIdx = string.find(optionPairsText, "%w+=[%w_-%.]+", nextIdx) + if idx == nil then break end + local pairText = string.sub(optionPairsText, idx, endIdx) + local keyIdx, keyEndIdx = string.find(pairText, "%w+") + local key = string.sub(pairText, keyIdx, keyEndIdx) + local valueIdx, valueEndIdx = string.find(pairText, "[%w_-%.]+", keyEndIdx + 2) + local value = string.sub(pairText, valueIdx, valueEndIdx) + options[key] = value + debug(" Found option: key = " .. key .. ", value = " .. value, settings) + nextIdx = endIdx + 2 + end + return options +end + +local function parseRepeatedInstruction(match, settings) + debug("Parsing repeated instruction: " .. match, settings) + local instruction = {} + instruction.type = INSTRUCTION_TYPES.repeated + local countIdx, countEndIdx = string.find(match, "%d+") + instruction.count = tonumber(string.sub(match, countIdx, countEndIdx)) + if instruction.count < 0 then + error("Repeated instruction cannot have a negative count.") + end + local innerScriptIdx, innerScriptEndIdx = string.find(match, "%b()", countEndIdx + 1) + local innerScript = string.sub(match, innerScriptIdx + 1, innerScriptEndIdx - 1) + instruction.instructions = movescript.parse(innerScript, settings) + return instruction +end + +local function parseInstruction(match, settings) + debug("Parsing instruction: " .. match, settings) + local instruction = {} + instruction.type = INSTRUCTION_TYPES.instruction + local countIdx, countEndIdx = string.find(match, "%d+") + instruction.count = 1 + if countIdx ~= nil then + instruction.count = tonumber(string.sub(match, countIdx, countEndIdx)) + end + if instruction.count < 1 or instruction.count > t.getFuelLimit() then + error("Instruction at index " .. actionIdx .. " has an invalid count of " .. instruction.count .. ". It should be >= 1 and <= " .. t.getFuelLimit()) + end + local actionIdx, actionEndIdx = string.find(match, "%u%l*") + instruction.action = string.sub(match, actionIdx, actionEndIdx) + if actionMap[instruction.action] == nil then + error("Instruction at index " .. actionIdx .. ", \"" .. instruction.action .. "\", does not refer to a valid action.") + end + return instruction +end + -- Parses a movescript script into a series of instruction tables. -local function parseScript(script, settings) +--[[ + Movescript Grammar: +block: instruction | repeatedInstructions + +repeatedInstructions: count '(' {instruction | repeatedInstructions} ')' + regex: %d+%s*%b() +instruction: [count] action [actionOptions] <- Not yet implemented. + regex: %d*%u%l* +count: %d+ + +action: %u%l* + +actionOptions: '(' {optionPair ','} ')' + regex: %b() + +optionPair: optionKey '=' optionValue + +optionKey: %w+ + +optionValue: [%w_-]+ + +]]-- +function movescript.parse(script, settings) local instructions = {} - for instruction in string.gfind(script, "%W*(%d*%u%l*)%W*") do - local countIdx, countIdxEnd = string.find(instruction, "%d+") - local actionIdx, actionIdxEnd = string.find(instruction, "%u%l*") - local count = 1 - if countIdx ~= nil then - count = tonumber(string.sub(instruction, countIdx, countIdxEnd)) + local scriptIdx = 1 + while scriptIdx <= string.len(script) do + local instruction = {} + local repeatedMatchStartIdx, repeatedMatchEndIdx = string.find(script, "%d+%s*%b()", scriptIdx) + local instructionMatchStartIdx, instructionMatchEndIdx = string.find(script, "%d*%u%l*", scriptIdx) + -- Parse the first occurring matched pattern. + if repeatedMatchStartIdx ~= nil and (instructionMatchStartIdx == nil or repeatedMatchStartIdx < instructionMatchStartIdx) then + -- Parse repeated instructions. + local match = string.sub(script, repeatedMatchStartIdx, repeatedMatchEndIdx) + table.insert(instructions, parseRepeatedInstruction(match, settings)) + scriptIdx = repeatedMatchEndIdx + 1 + elseif instructionMatchStartIdx ~= nil and (repeatedMatchStartIdx == nil or instructionMatchStartIdx < repeatedMatchStartIdx) then + -- Parse single instruction. + local match = string.sub(script, instructionMatchStartIdx, instructionMatchEndIdx) + local instruction = parseInstruction(match, settings) + local optionsIdx, optionsEndIdx = string.find(script, "%s*%b()", instructionMatchEndIdx + 1) + if optionsIdx ~= nil then + -- Check that there's nothing but empty space between the instruction and the options text. + if not string.find(string.sub(script, instructionMatchEndIdx + 1, optionsIdx - 1), "%S+") then + local optionsText = string.sub(script, optionsIdx, optionsEndIdx) + instruction.options = parseInstructionOptions(optionsText, settings) + end + end + table.insert(instructions, instruction) + scriptIdx = instructionMatchEndIdx + 1 + else + error("Invalid script characters found at index " .. scriptIdx) end - local action = string.sub(instruction, actionIdx, actionIdxEnd) - if count < 1 or count > t.getFuelLimit() then - error("Instruction at index " .. actionIdx .. " has an invalid count of " .. count .. ". It should be >= 1 and <= " .. t.getFuelLimit()) - end - if actionMap[action] == nil then - error("Instruction at index " .. actionIdx .. ", \"" .. action .. "\", does not refer to a valid action.") - end - table.insert(instructions, {action = action, count = count}) - debug("Parsed instruction: " .. instruction, settings) end return instructions end @@ -194,7 +294,7 @@ function movescript.run(script, settings) settings = settings or movescript.defaultSettings script = script or "" debug("Executing script: " .. script, settings) - local instructions = parseScript(script, settings) + local instructions = movescript.parse(script, settings) for idx, instruction in pairs(instructions) do executeInstruction(instruction, settings) end @@ -208,7 +308,7 @@ function movescript.runFile(filename, settings) end function movescript.validate(script, settings) - return pcall(function () parseScript(script, settings) end) + return pcall(function () movescript.parse(script, settings) end) end return movescript diff --git a/tester.lua b/tester.lua new file mode 100644 index 0000000..2b9c138 --- /dev/null +++ b/tester.lua @@ -0,0 +1,25 @@ +-- http://lua-users.org/wiki/TableSerialization +function print_r (t, name, indent) + local tableList = {} + function table_r (t, name, indent, full) + local serial=string.len(full) == 0 and name + or type(name)~="number" and '["'..tostring(name)..'"]' or '['..name..']' + io.write(indent,serial,' = ') + if type(t) == "table" then + if tableList[t] ~= nil then io.write('{}; -- ',tableList[t],' (self reference)\n') + else + tableList[t]=full..serial + if next(t) then -- Table not empty + io.write('{\n') + for key,value in pairs(t) do table_r(value,key,indent..'\t',full..serial) end + io.write(indent,'};\n') + else io.write('{};\n') end + end + else io.write(type(t)~="number" and type(t)~="boolean" and '"'..tostring(t)..'"' + or tostring(t),';\n') end + end + table_r(t,name or '__unnamed__',indent or '','') + end + +local ms = require("src/movescript") +print_r(ms.parse("35(2F(safe=false)R 3(L(delay=0.25, file=file.txt)UB))", {debug=true}))