From 7d6fd53ff859093c245fa5d5865de0403a8797a6 Mon Sep 17 00:00:00 2001 From: Fierelier Date: Fri, 1 Sep 2023 14:30:21 +0200 Subject: [PATCH] Remove Python build-dependency --- README.txt | 3 +- compile | 4 +- lua_translate | 202 ++++++++++------ src/toml.lua | 655 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 782 insertions(+), 82 deletions(-) create mode 100644 src/toml.lua diff --git a/README.txt b/README.txt index 0e10f7c..4960b98 100644 --- a/README.txt +++ b/README.txt @@ -3,11 +3,10 @@ A software accelerated (CPU-only) game engine for older computers. It uses a lay Can be compiled for Linux, OpenBSD and Windows. > Prerequisites, Debian/Ubuntu: -sudo apt install python3-toml libsdl2-dev liblua5.3-dev +sudo apt install gcc libsdl2-dev liblua5.3-dev > Prerequisites, OpenBSD: * Enable ports: https://www.openbsd.org/faq/ports/ports.html -doas pkg_add py3-toml cd /usr/ports/devel/sdl2 doas make doas make install diff --git a/compile b/compile index 0fed802..b711e49 100755 --- a/compile +++ b/compile @@ -26,11 +26,11 @@ fi if [ "$target_os" = "windows" ]; then echo "* Compiling: windows ..." CC="${CC:=gcc}" - PYTHON="${PYTHON:=py}" # https://www.python.org/downloads/release/python-344/ MINGWPATH="${MINGWPATH:=C:\\mingw32}" # https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/8.1.0/threads-posix/dwarf/ SDLPATH="${SDLPATH:=SDL2}" # https://github.com/libsdl-org/SDL/releases LUAPATH="${LUAPATH:=lua5.3/src}" # https://www.lua.org/ftp/lua-5.3.6.tar.gz - "$PYTHON" lua_translate + LUA="${LUA:="$LUAPATH/lua.exe"}" + "$LUA" lua_translate "$CC" -D_WIN32_WINNT=0x0501 -std=gnu89 src/main.c -g -L"$MINGWPATH\\lib" -w -Wl,-subsystem,windows -lmingw32 -L"$LUAPATH" -I"$LUAPATH" -I"$SDLPATH/include" -L"$SDLPATH/lib" -lSDL2main -lSDL2 -llua53 -lm -o engine.exe -O3 -Werror -Wall $CFLAGS exit fi diff --git a/lua_translate b/lua_translate index 9e8cfdc..530ab6f 100755 --- a/lua_translate +++ b/lua_translate @@ -1,116 +1,162 @@ -#!/usr/bin/env python3 -import sys -import os -import toml -typesIn = { - "__unknown": "lua_touserdata", - "char": "luaL_checkinteger", - "int": "luaL_checkinteger", - "float": "luaL_checknumber", - "long long": "luaL_checkinteger", - "unsigned char": "luaL_checkinteger", - "char *": "(char *)luaL_checkstring" +#!/usr/bin/env lua5.3 +basepath = (debug.getinfo(1, "S").source:sub(2):match("(.*[/\\])") or "./"):sub(1,-2) +package.path = basepath .. "/src/?.lua;" ..basepath .. "/src/?/init.lua" .. ";" .. package.path +local toml = require "toml" + +local typesIn = { + ["__unknown"] = "lua_touserdata", + ["char"] = "luaL_checkinteger", + ["int"] = "luaL_checkinteger", + ["float"] = "luaL_checknumber", + ["long long"] = "luaL_checkinteger", + ["unsigned char"] = "luaL_checkinteger", + ["char *"] = "(char *)luaL_checkstring" } -typesOut = { - "__unknown": "lua_pushlightuserdata", - "char": "lua_pushinteger", - "int": "lua_pushinteger", - "long long": "lua_pushinteger", - "unsigned char": "lua_pushinteger", - "char *": "lua_pushstring" +local typesOut = { + ["__unknown"] = "lua_pushlightuserdata", + ["char"] = "lua_pushinteger", + ["int"] = "lua_pushinteger", + ["long long"] = "lua_pushinteger", + ["unsigned char"] = "lua_pushinteger", + ["char *"] = "lua_pushstring" } -statics = toml.loads(open("src/values/statics.toml").read()) -functions = toml.loads(open("src/values/functions.toml").read()) -ofile = open("src/lua.c","w") +local function readFile(path) + local file = io.open(path,"rb") + local output = file:read("*a") + file:close() + return output +end -supportedTarget = False -if os.environ["target_os"] == "linux": - ofile.write('''\ +local function bp(path) + return basepath .. "/" .. path +end + +local function stringJoin(lst,sep) + local nstring = "" + for _,str in ipairs(lst) do + nstring = nstring .. sep .. str + end + return string.sub(nstring,1 + string.len(sep)) +end + +local statics = toml.parse(readFile(bp("src/values/statics.toml"))) +local functions = toml.parse(readFile(bp("src/values/functions.toml"))) +local ofile = io.open(bp("src/lua.c"),"wb") + +local supportedTarget = false +if os.getenv("target_os") == "linux" then + ofile:write([[ #include #include #include -''') - supportedTarget = True +]]) + supportedTarget = true +end -if os.environ["target_os"] == "openbsd": - ofile.write('''\ +if os.getenv("target_os") == "openbsd" then + ofile:write([[ #include #include #include -''') - supportedTarget = True +]]) + supportedTarget = true +end -if os.environ["target_os"] == "windows": - ofile.write('''\ +if os.getenv("target_os") == "windows" then + ofile:write([[ #include #include #include -''') - supportedTarget = True +]]) + supportedTarget = true +end -if supportedTarget == False: - print("Platform " +os.environ["target_os"]+ " unsupported by lua_translate.") - sys.exit(1) +if supportedTarget == false then + print("Platform " ..tostring(os.getenv("target_os")).. " unsupported by lua_translate.") + os.exit(1) +end -ofile.write('''lua_State * engine_lua_state; +ofile:write([[ +lua_State * engine_lua_state; #include "lua_manual.c" -''') +]]) -for func in functions: - if "lua" in functions[func]: - if functions[func]["lua"] in ["no","manual"]: continue - invarCount = 1 - funcnew = "engine_luaf_" +func.replace("engine_","",1) - ofile.write('int ' +funcnew+ '(lua_State *L) {\n') - for arg in functions[func]["arguments"]: - if not arg in typesIn: - checkfunc = typesIn["__unknown"] - else: +for func,_ in pairs(functions) do + if functions[func].lua == "no" or + functions[func].lua == "manual" then + goto continue + end + + local invarCount = 1 + local funcnew,_ = string.gsub(func,"engine_","",1) + funcnew = "engine_luaf_" ..funcnew + ofile:write('int ' ..funcnew.. '(lua_State *L) {\n') + for _,arg in ipairs(functions[func].arguments) do + local checkfunc = false + if typesIn[arg] == nil then + checkfunc = typesIn.__unknown + else checkfunc = typesIn[arg] - ofile.write("\t" +arg+ ' ' +functions[func]["argNames"][invarCount - 1]+ ' = ' +checkfunc+ '(L,' +str(invarCount)+ ');\n') - invarCount += 1 + end + ofile:write('\t' ..arg.. ' ' ..functions[func].argNames[invarCount].. ' = ' ..checkfunc.. '(L,' ..tostring(invarCount).. ');\n') + invarCount = invarCount + 1 + end - argstring = "(" +",".join(functions[func]["argNames"])+ ")" + local argstring = "(" ..stringJoin(functions[func].argNames,",").. ")" - outtype = functions[func]["type"] - if outtype == "void": - ofile.write('\t' + func + argstring + ";") - ofile.write('\n\treturn 0;') - else: - if not outtype in typesOut: - pushfunc = typesOut["__unknown"] - else: + local outtype = functions[func].type + if outtype == "void" then + ofile:write('\t' .. func .. argstring .. ";") + ofile:write('\n\treturn 0;') + else + local pushfunc = false + if typesOut[outtype] == nil then + pushfunc = typesOut.__unknown + else pushfunc = typesOut[outtype] + end - ofile.write('\t' +outtype+ ' outvar = ' +func + argstring + ";") - ofile.write("\n\t" +pushfunc+ '(L,outvar);') - ofile.write('\n\treturn 1;') + ofile:write('\t' ..outtype.. ' outvar = ' ..func .. argstring.. ';') + ofile:write('\n\t' ..pushfunc.. '(L,outvar);') + ofile:write('\n\treturn 1;') + end - ofile.write('\n}\n\n') + ofile:write('\n}\n\n') + ::continue:: +end -ofile.write('''\ +ofile:write([[ void engine_lua_init() { engine_lua_state = luaL_newstate(); luaL_openlibs(engine_lua_state); -''') +]]) -for static in statics: - ofile.write('\tlua_pushinteger(engine_lua_state,' +static+ ');\n') - ofile.write('\tlua_setglobal(engine_lua_state,"' +static+ '");\n') +for static,_ in pairs(statics) do + ofile:write('\tlua_pushinteger(engine_lua_state,' ..static.. ');\n') + ofile:write('\tlua_setglobal(engine_lua_state,"' ..static.. '");\n') +end -for func in functions: - if "lua" in functions[func]: - if functions[func]["lua"] in ["no","manual"]: continue - funcnew = "engine_luaf_" +func.replace("engine_","",1) - ofile.write('\tlua_pushcfunction(engine_lua_state,' +funcnew+ ');\n') - ofile.write('\tlua_setglobal (engine_lua_state,"' +func+ '");\n') +for func,_ in pairs(functions) do + if functions[func].lua == "no" or + functions[func].lua == "manual" then + goto continue + end + + local funcnew,_ = string.gsub(func,"engine_","",1) + funcnew = "engine_luaf_" ..funcnew + ofile:write('\tlua_pushcfunction(engine_lua_state,' ..funcnew.. ');\n') + ofile:write('\tlua_setglobal (engine_lua_state,"' ..func.. '");\n') + ::continue:: +end -ofile.write('''\ +ofile:write([[ engine_lua_init_manual(); luaL_loadfile(engine_lua_state,"mods/main/script/main.lua"); lua_call(engine_lua_state,0,0); -}''') +}]]) + +ofile:close() diff --git a/src/toml.lua b/src/toml.lua new file mode 100644 index 0000000..4e0d7f4 --- /dev/null +++ b/src/toml.lua @@ -0,0 +1,655 @@ +--[[ +Sourced from: https://github.com/jonstoler/lua-toml + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]]-- + +local TOML = { + -- denotes the current supported TOML version + version = 0.40, + + -- sets whether the parser should follow the TOML spec strictly + -- currently, no errors are thrown for the following rules if strictness is turned off: + -- tables having mixed keys + -- redefining a table + -- redefining a key within a table + strict = true, +} + +-- converts TOML data into a lua table +TOML.parse = function(toml, options) + options = options or {} + local strict = (options.strict ~= nil and options.strict or TOML.strict) + + -- the official TOML definition of whitespace + local ws = "[\009\032]" + + -- the official TOML definition of newline + local nl = "[\10" + do + local crlf = "\13\10" + nl = nl .. crlf + end + nl = nl .. "]" + + -- stores text data + local buffer = "" + + -- the current location within the string to parse + local cursor = 1 + + -- the output table + local out = {} + + -- the current table to write to + local obj = out + + -- returns the next n characters from the current position + local function char(n) + n = n or 0 + return toml:sub(cursor + n, cursor + n) + end + + -- moves the current position forward n (default: 1) characters + local function step(n) + n = n or 1 + cursor = cursor + n + end + + -- move forward until the next non-whitespace character + local function skipWhitespace() + while(char():match(ws)) do + step() + end + end + + -- remove the (Lua) whitespace at the beginning and end of a string + local function trim(str) + return str:gsub("^%s*(.-)%s*$", "%1") + end + + -- divide a string into a table around a delimiter + local function split(str, delim) + if str == "" then return {} end + local result = {} + local append = delim + if delim:match("%%") then + append = delim:gsub("%%", "") + end + for match in (str .. append):gmatch("(.-)" .. delim) do + table.insert(result, match) + end + return result + end + + -- produce a parsing error message + -- the error contains the line number of the current position + local function err(message, strictOnly) + if not strictOnly or (strictOnly and strict) then + local line = 1 + local c = 0 + for l in toml:gmatch("(.-)" .. nl) do + c = c + l:len() + if c >= cursor then + break + end + line = line + 1 + end + error("TOML: " .. message .. " on line " .. line .. ".", 4) + end + end + + -- prevent infinite loops by checking whether the cursor is + -- at the end of the document or not + local function bounds() + return cursor <= toml:len() + end + + local function parseString() + local quoteType = char() -- should be single or double quote + + -- this is a multiline string if the next 2 characters match + local multiline = (char(1) == char(2) and char(1) == char()) + + -- buffer to hold the string + local str = "" + + -- skip the quotes + step(multiline and 3 or 1) + + while(bounds()) do + if multiline and char():match(nl) and str == "" then + -- skip line break line at the beginning of multiline string + step() + end + + -- keep going until we encounter the quote character again + if char() == quoteType then + if multiline then + if char(1) == char(2) and char(1) == quoteType then + step(3) + break + end + else + step() + break + end + end + + if char():match(nl) and not multiline then + err("Single-line string cannot contain line break") + end + + -- if we're in a double-quoted string, watch for escape characters! + if quoteType == '"' and char() == "\\" then + if multiline and char(1):match(nl) then + -- skip until first non-whitespace character + step(1) -- go past the line break + while(bounds()) do + if not char():match(ws) and not char():match(nl) then + break + end + step() + end + else + -- all available escape characters + local escape = { + b = "\b", + t = "\t", + n = "\n", + f = "\f", + r = "\r", + ['"'] = '"', + ["\\"] = "\\", + } + -- utf function from http://stackoverflow.com/a/26071044 + -- converts \uXXX into actual unicode + local function utf(char) + local bytemarkers = {{0x7ff, 192}, {0xffff, 224}, {0x1fffff, 240}} + if char < 128 then return string.char(char) end + local charbytes = {} + for bytes, vals in pairs(bytemarkers) do + if char <= vals[1] then + for b = bytes + 1, 2, -1 do + local mod = char % 64 + char = (char - mod) / 64 + charbytes[b] = string.char(128 + mod) + end + charbytes[1] = string.char(vals[2] + char) + break + end + end + return table.concat(charbytes) + end + + if escape[char(1)] then + -- normal escape + str = str .. escape[char(1)] + step(2) -- go past backslash and the character + elseif char(1) == "u" then + -- utf-16 + step() + local uni = char(1) .. char(2) .. char(3) .. char(4) + step(5) + uni = tonumber(uni, 16) + if (uni >= 0 and uni <= 0xd7ff) and not (uni >= 0xe000 and uni <= 0x10ffff) then + str = str .. utf(uni) + else + err("Unicode escape is not a Unicode scalar") + end + elseif char(1) == "U" then + -- utf-32 + step() + local uni = char(1) .. char(2) .. char(3) .. char(4) .. char(5) .. char(6) .. char(7) .. char(8) + step(9) + uni = tonumber(uni, 16) + if (uni >= 0 and uni <= 0xd7ff) and not (uni >= 0xe000 and uni <= 0x10ffff) then + str = str .. utf(uni) + else + err("Unicode escape is not a Unicode scalar") + end + else + err("Invalid escape") + end + end + else + -- if we're not in a double-quoted string, just append it to our buffer raw and keep going + str = str .. char() + step() + end + end + + return {value = str, type = "string"} + end + + local function parseNumber() + local num = "" + local exp + local date = false + while(bounds()) do + if char():match("[%+%-%.eE_0-9]") then + if not exp then + if char():lower() == "e" then + -- as soon as we reach e or E, start appending to exponent buffer instead of + -- number buffer + exp = "" + elseif char() ~= "_" then + num = num .. char() + end + elseif char():match("[%+%-0-9]") then + exp = exp .. char() + else + err("Invalid exponent") + end + elseif char():match(ws) or char() == "#" or char():match(nl) or char() == "," or char() == "]" or char() == "}" then + break + elseif char() == "T" or char() == "Z" then + -- parse the date (as a string, since lua has no date object) + date = true + while(bounds()) do + if char() == "," or char() == "]" or char() == "#" or char():match(nl) or char():match(ws) then + break + end + num = num .. char() + step() + end + else + err("Invalid number") + end + step() + end + + if date then + return {value = num, type = "date"} + end + + local float = false + if num:match("%.") then float = true end + + exp = exp and tonumber(exp) or 0 + num = tonumber(num) + + if not float then + return { + -- lua will automatically convert the result + -- of a power operation to a float, so we have + -- to convert it back to an int with math.floor + value = math.floor(num * 10^exp), + type = "int", + } + end + + return {value = num * 10^exp, type = "float"} + end + + local parseArray, getValue + + function parseArray() + step() -- skip [ + skipWhitespace() + + local arrayType + local array = {} + + while(bounds()) do + if char() == "]" then + break + elseif char():match(nl) then + -- skip + step() + skipWhitespace() + elseif char() == "#" then + while(bounds() and not char():match(nl)) do + step() + end + else + -- get the next object in the array + local v = getValue() + if not v then break end + + -- set the type if it hasn't been set before + if arrayType == nil then + arrayType = v.type + elseif arrayType ~= v.type then + err("Mixed types in array", true) + end + + array = array or {} + table.insert(array, v.value) + + if char() == "," then + step() + end + skipWhitespace() + end + end + step() + + return {value = array, type = "array"} + end + + local function parseInlineTable() + step() -- skip opening brace + + local buffer = "" + local quoted = false + local tbl = {} + + while bounds() do + if char() == "}" then + break + elseif char() == "'" or char() == '"' then + buffer = parseString().value + quoted = true + elseif char() == "=" then + if not quoted then + buffer = trim(buffer) + end + + step() -- skip = + skipWhitespace() + + if char():match(nl) then + err("Newline in inline table") + end + + local v = getValue().value + tbl[buffer] = v + + skipWhitespace() + + if char() == "," then + step() + elseif char():match(nl) then + err("Newline in inline table") + end + + quoted = false + buffer = "" + else + buffer = buffer .. char() + step() + end + end + step() -- skip closing brace + + return {value = tbl, type = "array"} + end + + local function parseBoolean() + local v + if toml:sub(cursor, cursor + 3) == "true" then + step(4) + v = {value = true, type = "boolean"} + elseif toml:sub(cursor, cursor + 4) == "false" then + step(5) + v = {value = false, type = "boolean"} + else + err("Invalid primitive") + end + + skipWhitespace() + if char() == "#" then + while(not char():match(nl)) do + step() + end + end + + return v + end + + -- figure out the type and get the next value in the document + function getValue() + if char() == '"' or char() == "'" then + return parseString() + elseif char():match("[%+%-0-9]") then + return parseNumber() + elseif char() == "[" then + return parseArray() + elseif char() == "{" then + return parseInlineTable() + else + return parseBoolean() + end + -- date regex (for possible future support): + -- %d%d%d%d%-[0-1][0-9]%-[0-3][0-9]T[0-2][0-9]%:[0-6][0-9]%:[0-6][0-9][Z%:%+%-%.0-9]* + end + + -- track whether the current key was quoted or not + local quotedKey = false + + -- parse the document! + while(cursor <= toml:len()) do + + -- skip comments and whitespace + if char() == "#" then + while(not char():match(nl)) do + step() + end + end + + if char():match(nl) then + -- skip + end + + if char() == "=" then + step() + skipWhitespace() + + -- trim key name + buffer = trim(buffer) + + if buffer:match("^[0-9]*$") and not quotedKey then + buffer = tonumber(buffer) + end + + if buffer == "" and not quotedKey then + err("Empty key name") + end + + local v = getValue() + if v then + -- if the key already exists in the current object, throw an error + if obj[buffer] then + err('Cannot redefine key "' .. buffer .. '"', true) + end + obj[buffer] = v.value + end + + -- clear the buffer + buffer = "" + quotedKey = false + + -- skip whitespace and comments + skipWhitespace() + if char() == "#" then + while(bounds() and not char():match(nl)) do + step() + end + end + + -- if there is anything left on this line after parsing a key and its value, + -- throw an error + if not char():match(nl) and cursor < toml:len() then + err("Invalid primitive") + end + elseif char() == "[" then + buffer = "" + step() + local tableArray = false + + -- if there are two brackets in a row, it's a table array! + if char() == "[" then + tableArray = true + step() + end + + obj = out + + local function processKey(isLast) + isLast = isLast or false + buffer = trim(buffer) + + if not quotedKey and buffer == "" then + err("Empty table name") + end + + if isLast and obj[buffer] and not tableArray and #obj[buffer] > 0 then + err("Cannot redefine table", true) + end + + -- set obj to the appropriate table so we can start + -- filling it with values! + if tableArray then + -- push onto cache + if obj[buffer] then + obj = obj[buffer] + if isLast then + table.insert(obj, {}) + end + obj = obj[#obj] + else + obj[buffer] = {} + obj = obj[buffer] + if isLast then + table.insert(obj, {}) + obj = obj[1] + end + end + else + obj[buffer] = obj[buffer] or {} + obj = obj[buffer] + end + end + + while(bounds()) do + if char() == "]" then + if tableArray then + if char(1) ~= "]" then + err("Mismatching brackets") + else + step() -- skip inside bracket + end + end + step() -- skip outside bracket + + processKey(true) + buffer = "" + break + elseif char() == '"' or char() == "'" then + buffer = parseString().value + quotedKey = true + elseif char() == "." then + step() -- skip period + processKey() + buffer = "" + else + buffer = buffer .. char() + step() + end + end + + buffer = "" + quotedKey = false + elseif (char() == '"' or char() == "'") then + -- quoted key + buffer = parseString().value + quotedKey = true + end + + buffer = buffer .. (char():match(nl) and "" or char()) + step() + end + + return out +end + +TOML.encode = function(tbl) + local toml = "" + + local cache = {} + + local function parse(tbl) + for k, v in pairs(tbl) do + if type(v) == "boolean" then + toml = toml .. k .. " = " .. tostring(v) .. "\n" + elseif type(v) == "number" then + toml = toml .. k .. " = " .. tostring(v) .. "\n" + elseif type(v) == "string" then + local quote = '"' + v = v:gsub("\\", "\\\\") + + -- if the string has any line breaks, make it multiline + if v:match("^\n(.*)$") then + quote = quote:rep(3) + v = "\\n" .. v + elseif v:match("\n") then + quote = quote:rep(3) + end + + v = v:gsub("\b", "\\b") + v = v:gsub("\t", "\\t") + v = v:gsub("\f", "\\f") + v = v:gsub("\r", "\\r") + v = v:gsub('"', '\\"') + v = v:gsub("/", "\\/") + toml = toml .. k .. " = " .. quote .. v .. quote .. "\n" + elseif type(v) == "table" then + local array, arrayTable = true, true + local first = {} + for kk, vv in pairs(v) do + if type(kk) ~= "number" then array = false end + if type(vv) ~= "table" then + v[kk] = nil + first[kk] = vv + arrayTable = false + end + end + + if array then + if arrayTable then + -- double bracket syntax go! + table.insert(cache, k) + for kk, vv in pairs(v) do + toml = toml .. "[[" .. table.concat(cache, ".") .. "]]\n" + for k3, v3 in pairs(vv) do + if type(v3) ~= "table" then + vv[k3] = nil + first[k3] = v3 + end + end + parse(first) + parse(vv) + end + table.remove(cache) + else + -- plain ol boring array + toml = toml .. k .. " = [\n" + for kk, vv in pairs(first) do + toml = toml .. tostring(vv) .. ",\n" + end + toml = toml .. "]\n" + end + else + -- just a key/value table, folks + table.insert(cache, k) + toml = toml .. "[" .. table.concat(cache, ".") .. "]\n" + parse(first) + parse(v) + table.remove(cache) + end + end + end + end + + parse(tbl) + + return toml:sub(1, -2) +end + +return TOML +