Login

Subversion Repositories NedoOS

Rev

Rev 539 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

/*

  SjASMPlus Z80 Cross Compiler - modified - lua scripting module

  Copyright (c) 2006 Sjoerd Mastijn (original SW)
  Copyright (c) 2022 Peter Ped Helcmanovsky (lua scripting module)

  This software is provided 'as-is', without any express or implied warranty.
  In no event will the authors be held liable for any damages arising from the
  use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it freely,
  subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not claim
         that you wrote the original software. If you use this software in a product,
         an acknowledgment in the product documentation would be appreciated but is
         not required.

  2. Altered source versions must be plainly marked as such, and must not be
         misrepresented as being the original software.

  3. This notice may not be removed or altered from any source distribution.

*/


// lua_sjasm.cpp

#include "sjdefs.h"

#ifdef USE_LUA

#include "lua.hpp"
#include "LuaBridge/LuaBridge.h"

static lua_State *LUA = nullptr;

static const char* lua_err_prefix = "[LUA] ";

// extra lua script inserting interface (sj.something) entry functions
// for functions with optional arguments, like error and warning
// (as LuaBridge2.6 doesn't offer that type of binding as far as I can tell)
// Sidestepping LuaBridge write-protection by "rawset" the end point into it
static const char* binding_script_name = "lua_sjasm.cpp";
static constexpr int binding_script_line = __LINE__;
static const std::string lua_impl_init_bindings_script = R"BINDING_LUA(
rawset(sj,"error",function(m,v)sj.error_i(m or "no message",v)end)
rawset(sj,"warning",function(m,v)sj.warning_i(m or "no message",v)end)
rawset(sj,"insert_define",function(n,v)return sj.insert_define_i(n,v)end)
rawset(sj,"exit",function(e)return sj.exit_i(e or 1)end)
rawset(sj,"set_device",function(i,t)return sj.set_device_i(i or "NONE",t or 0)end)
rawset(zx,"trdimage_create",function(n,l)return zx.trdimage_create_i(n,l)end)
rawset(zx,"trdimage_add_file",function(t,f,s,l,a,r)return zx.trdimage_add_file_i(t,f,s,l,a or -1,r or false)end)
)BINDING_LUA"
;

static void lua_impl_fatalError(lua_State *L) {
        Error((char *)lua_tostring(L, -1), NULL, FATAL);
}

static std::vector<TextFilePos> scripts_origin;

static char internal_script_name[LINEMAX];

static const char* lua_impl_get_script_name(const TextFilePos & srcPos) {
        sprintf(internal_script_name, "script %u", uint32_t(scripts_origin.size()));
        scripts_origin.push_back(srcPos);
        return internal_script_name;
}

static bool isInlinedScript(TextFilePos & errorPos, const char* script_name) {
        // return false when the script is external (real file name)
        if (script_name != strstr(script_name, "[string \"script ")) return false;
        // inlined script, find it's origin and add line number to that
        int scriptNumber = atoi(script_name + 16);
        if (scriptNumber < 0 || int(scripts_origin.size()) <= scriptNumber) return false;
        errorPos = scripts_origin.at(scriptNumber);
        return true;
}

// adds current source position in lua script + full stack depth onto sourcePosStack
// = makes calls to Error/Warning API to display more precise error lines in lua scripts
static int addLuaSourcePositions() {
        // find all *known* inlined/standalone script names and line numbers on the lua stack
        assert(LUA);
        lua_Debug ar;
        source_positions_t luaPosTemp;
        luaPosTemp.reserve(16);
        int level = 1;                  // level 0 is "C" space, ignore that always
        while (lua_getstack(LUA, level, &ar)) {                 // as long lua stack level are available
                if (!lua_getinfo(LUA, "Sl", &ar)) break;        // not enough info about current level

                //assert(level || !strcmp("[C]", ar.short_src));        //TODO: verify if ever upgrading to newer lua
                //if (!strcmp("[C]", ar.short_src)) { ++level; continue; }

                TextFilePos levelErrorPos;
                if (isInlinedScript(levelErrorPos, ar.short_src)) {
                        levelErrorPos.line += ar.currentline;
                } else {
                        levelErrorPos.newFile(ArchiveFilename(ar.short_src));
                        levelErrorPos.line = ar.currentline;
                }
                luaPosTemp.push_back(levelErrorPos);
                ++level;
        }

        // add all lua positions in reversed order to sourcePosStack (hide binding script if anything else is available)
        bool hide_binding = (2 <= luaPosTemp.size()) && !strcmp(binding_script_name, luaPosTemp[0].filename);
        source_positions_t::iterator stop = hide_binding ? luaPosTemp.begin() + 1 : luaPosTemp.begin();
        source_positions_t::iterator i = luaPosTemp.end();
        while (i-- != stop) sourcePosStack.push_back(*i);

        return luaPosTemp.end() - stop;
}

static void removeLuaSourcePositions(int to_remove) {
        while (to_remove--) sourcePosStack.pop_back();  // restore sourcePosStack to original state
}

// skips file+line_number info (but will adjust global LuaStartPos data for Error output)
static TextFilePos lua_impl_splitLuaErrorMessage(const char*& LuaError) {
        TextFilePos luaErrorPos("?");
        if (!sourcePosStack.empty()) luaErrorPos = sourcePosStack.back();
        if (nullptr == LuaError) return luaErrorPos;

        const char* colonPos = strchr(LuaError, ':');
        const char* colon2Pos = nullptr != colonPos ? strchr(colonPos+1, ':') : nullptr;
        if (nullptr == colonPos || nullptr == colon2Pos) return luaErrorPos;    // error, format not recognized

        int lineNumber = atoi(colonPos + 1);
        if (isInlinedScript(luaErrorPos, LuaError)) {
                luaErrorPos.line += lineNumber;
        } else {
                // standalone script, use file name and line number as is (if provided by lua error)
                STRNCPY(internal_script_name, LINEMAX, LuaError, colonPos - LuaError);
                luaErrorPos.newFile(ArchiveFilename(internal_script_name));
                luaErrorPos.line = lineNumber;
        }

        // advance beyond filename and line in LuaError pointer
        LuaError = colon2Pos + 1;
        while (White(*LuaError)) ++LuaError;
        return luaErrorPos;
}

static void lua_impl_showLoadError(const EStatus type) {
        const char *msgp = lua_tostring(LUA, -1);
        sourcePosStack.push_back(lua_impl_splitLuaErrorMessage(msgp));
        Error(msgp, nullptr, type);
        sourcePosStack.pop_back();
        lua_pop(LUA, 1);
}

static aint lua_sj_calc(const char *str) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector

        // substitute defines + macro_args in the `str` first (preserve original global variables)
        char* const oldSubstitutedLine = substitutedLine;
        const int oldComlin = comlin;
        comlin = 0;
        char* tmp = nullptr, * tmp2 = nullptr;
        if (sline[0]) {
                tmp = STRDUP(sline);
                if (nullptr == tmp) ErrorOOM();
        }
        if (sline2[0]) {
                tmp2 = STRDUP(sline2);
                if (nullptr == tmp2) ErrorOOM();
        }
        // non-const copy of input string for ReplaceDefine argument
        //TODO: v2.x, rewrite whole parser of sjasmplus to start with const input to avoid such copies
        char* luaInput = STRDUP(str ? str : "");
        char* substitutedStr = ReplaceDefine(luaInput);

        // evaluate the expression
        aint val;
        int parseResult = ParseExpression(substitutedStr, val);
        free(luaInput);

        // restore any global values affected by substitution
        sline[0] = 0;
        if (tmp) {
                STRCPY(sline, LINEMAX2, tmp);
                free(tmp);
        }
        sline2[0] = 0;
        if (tmp2) {
                STRCPY(sline2, LINEMAX2, tmp2);
                free(tmp2);
        }
        substitutedLine = oldSubstitutedLine;
        comlin = oldComlin;

        removeLuaSourcePositions(positionsAdded);
        return parseResult ? val : 0;
}

static void parse_line(const char* str, bool parseLabels) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector

        // preserve current actual line which will be parsed next
        char *oldLine = STRDUP(line);
        if (nullptr == oldLine) ErrorOOM();
        char *oldEolComment = eolComment;

        // inject new line from Lua call and assemble it
        STRCPY(line, LINEMAX, str ? str : "");
        eolComment = nullptr;
        ParseLineSafe(parseLabels);

        // restore the original line
        STRCPY(line, LINEMAX, oldLine);
        eolComment = oldEolComment;
        free(oldLine);

        removeLuaSourcePositions(positionsAdded);
}

static void lua_sj_parse_line(const char *str) {
        parse_line(str, true);
}

static void lua_sj_parse_code(const char *str) {
        parse_line(str, false);
}

static void lua_sj_error(const char* message, const char* value = nullptr) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        Error(message, value, ALL);
        removeLuaSourcePositions(positionsAdded);
}

static void lua_sj_warning(const char* message, const char* value = nullptr) {
        int positionsAdded = addLuaSourcePositions();                   // add known script positions to sourcePosStack vector
        Warning(message, value, W_ALL);
        removeLuaSourcePositions(positionsAdded);
}

static const char* lua_sj_get_define(const char* name, bool macro_args = false) {
        const char* macro_res = (macro_args && macrolabp) ? MacroDefineTable.getverv(name) : nullptr;
        return macro_res ? macro_res : DefineTable.Get(name);
}

static bool lua_sj_insert_define(const char* name, const char* value) {
        // wrapper to resolve member-function call (without std::function wrapping lambda, just to KISS)
        char* lua_name = const_cast<char*>(name);               //TODO v2.x avoid const_cast like this
        char* id = name ? GetID(lua_name) : nullptr;
        if (nullptr == id) return false;
        return DefineTable.Replace(id, value ? value : "");
}

static int lua_sj_get_label(const char *name) {
        if (nullptr == name) return -1;
        aint val;
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        char* n = const_cast<char*>(name);      //TODO try to get rid of const_cast, LuaBridge requires const char* to understand it as lua string
        if (!GetLabelValue(n, val)) val = -1;
        removeLuaSourcePositions(positionsAdded);
        return val;
}

static bool lua_sj_insert_label(const char *name, int address) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        std::unique_ptr<char[]> fullName(ValidateLabel(name, false, false));
        removeLuaSourcePositions(positionsAdded);
        if (nullptr == fullName.get()) return false;
        return LabelTable.Insert(name, address);
}

static void lua_sj_shellexec(const char *command) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        LuaShellExec(command);
        removeLuaSourcePositions(positionsAdded);
}

static bool lua_sj_set_page(aint n) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        if (!DeviceID) Warning("sj.set_page: only allowed in real device emulation mode (See DEVICE)");
        bool result = DeviceID && dirPageImpl("sj.set_page", n);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static bool lua_sj_set_slot(aint n) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        bool result = false;
        if (!DeviceID) {
                Warning("sj.set_slot: only allowed in real device emulation mode (See DEVICE)");
        } else {
                n = Device->SlotNumberFromPreciseAddress(n);
                result = Device->SetSlot(n);
                if (!result) {
                        char buf[LINEMAX];
                        SPRINTF1(buf, LINEMAX, "sj.set_slot: Slot number must be in range 0..%u", Device->SlotsCount - 1);
                        Error(buf);
                }
        }
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static bool lua_sj_set_device(const char* id, const aint ramtop = 0) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        // refresh source position of first DEVICE directive (to make global-device detection work correctly)
        if (1 == ++deviceDirectivesCount) {
                assert(!sourcePosStack.empty());
                globalDeviceSourcePos = sourcePosStack.back();
        }
        // check for nullptr id??
        bool result = SetDevice(id, ramtop);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static void lua_sj_add_byte(int byte) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        EmitByte(byte);
        removeLuaSourcePositions(positionsAdded);
}

static void lua_sj_add_word(int word) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        EmitWord(word);
        removeLuaSourcePositions(positionsAdded);
}

static unsigned char lua_sj_get_byte(unsigned int address) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        auto result = MemGetByte(address);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static unsigned int lua_sj_get_word(unsigned int address) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        auto result = MemGetWord(address);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static bool lua_zx_trdimage_create(const char* trdname, const char* label = nullptr) {
        // setup label to truncated 8 char array padded with spaces
        char l8[9] = "        ";
        char* l8_ptr = l8;
        while (label && *label && (l8_ptr - l8) < 8) *l8_ptr++ = *label++;
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        bool result = TRD_SaveEmpty(trdname, l8);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

bool lua_zx_trdimage_add_file(const char* trd, const char* file, int start, int length, int autostart = -1, bool replace = false) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        bool result = nullptr != trd && nullptr != file && TRD_AddFile(trd, file, start, length, autostart, replace, false);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static bool lua_zx_save_snapshot_sna(const char* fname, word start) {
        int positionsAdded = addLuaSourcePositions();   // add known script positions to sourcePosStack vector
        bool result = SaveSNA_ZX(fname, start);
        removeLuaSourcePositions(positionsAdded);
        return result;
}

static void lua_impl_init() {
        assert(nullptr == LUA);

        scripts_origin.reserve(64);

        // initialise Lua (regular Lua, without sjasmplus bindings/extensions)
        LUA = luaL_newstate();
        lua_atpanic(LUA, (lua_CFunction)lua_impl_fatalError);

        // for manual testing of lua_atpanic handler
//      { lua_error(LUA); }
//      { lua_pushstring(LUA, "testing at panic handler"); lua_error(LUA); }

        luaL_openlibs(LUA);

        // initialise sjasmplus bindings/extensions
        luabridge::getGlobalNamespace(LUA)
                .addFunction("_c", lua_sj_calc)
                .addFunction("_pl", lua_sj_parse_line)
                .addFunction("_pc", lua_sj_parse_code)
                .beginNamespace("sj")
                        .addProperty("current_address", &CurAddress, false)     // read-only
                        .addProperty("warning_count", &WarningCount, false)     // read-only
                        .addProperty("error_count", &ErrorCount, false) // read-only
                        // internal functions which are lua-wrapped to enable optional arguments
                        .addFunction("error_i", lua_sj_error)
                        .addFunction("warning_i", lua_sj_warning)
                        .addFunction("insert_define_i", lua_sj_insert_define)
                        .addFunction("exit_i", ExitASM)
                        .addFunction("set_device_i", lua_sj_set_device)
                        // remaining public functions with all arguments mandatory (boolean args seems to default to false?)
                        .addFunction("get_define", lua_sj_get_define)
                        .addFunction("get_label", lua_sj_get_label)
                        .addFunction("insert_label", lua_sj_insert_label)
                        .addFunction("shellexec", lua_sj_shellexec)
                        .addFunction("calc", lua_sj_calc)
                        .addFunction("parse_line", lua_sj_parse_line)
                        .addFunction("parse_code", lua_sj_parse_code)
                        .addFunction("add_byte", lua_sj_add_byte)
                        .addFunction("add_word", lua_sj_add_word)
                        .addFunction("get_byte", lua_sj_get_byte)
                        .addFunction("get_word", lua_sj_get_word)
                        .addFunction("get_device", GetDeviceName)               // no error/warning, can be called directly
                        .addFunction("set_page", lua_sj_set_page)
                        .addFunction("set_slot", lua_sj_set_slot)
                        // MMU API will be not added, it is too dynamic, and _pc("MMU ...") works
                        .addFunction("file_exists", FileExists)
                .endNamespace()
                .beginNamespace("zx")
                        .addFunction("trdimage_create_i", lua_zx_trdimage_create)
                        .addFunction("trdimage_add_file_i", lua_zx_trdimage_add_file)
                        .addFunction("save_snapshot_sna", lua_zx_save_snapshot_sna)
                .endNamespace();

                TextFilePos binding_script_pos(binding_script_name, binding_script_line);
                if (luaL_loadbuffer(LUA, lua_impl_init_bindings_script.c_str(), lua_impl_init_bindings_script.size(), lua_impl_get_script_name(binding_script_pos))
                        || lua_pcall(LUA, 0, LUA_MULTRET, 0)) {
                        lua_impl_showLoadError(FATAL);                          // unreachable (I hope) // manual testing: damage binding script
                }
}

void dirENDLUA() {
        Error("[ENDLUA] End of lua script without script");
}

void dirLUA() {
        // lazy init of Lua scripting upon first hit of LUA directive
        if (nullptr == LUA) lua_impl_init();
        assert(LUA);

        constexpr size_t luaBufferSize = 32768;
        char* id, * buff = nullptr, * bp = nullptr;

        int passToExec = LASTPASS;
        if ((id = GetID(lp)) && strlen(id) > 0) {
                if (cmphstr(id, "pass1")) {
                        passToExec = 1;
                } else if (cmphstr(id, "pass2")) {
                        passToExec = 2;
                } else if (cmphstr(id, "pass3")) {
                        passToExec = LASTPASS;
                } else if (cmphstr(id, "allpass")) {
                        passToExec = -1;
                } else {
                        Error("[LUA] Syntax error", id);
                }
        }

        const EStatus errorType = (1 == passToExec || 2 == passToExec) ? EARLY : PASS3;
        const bool execute = (-1 == passToExec) || (passToExec == pass);
        // remember warning suppression also from block start
        bool showWarning = !suppressedById(W_LUA_MC_PASS);

        assert(!sourcePosStack.empty());
        TextFilePos luaStartPos = sourcePosStack.back();        // position of LUA directive (not ENDLUA)
        if (execute) {
                buff = new char[luaBufferSize];
                bp = buff;
        }
        ListFile();

        while (1) {
                if (!ReadLine(false)) {
                        Error("[LUA] Unexpected end of lua script");
                        break;
                }
                lp = line;
                SkipBlanks(lp);
                const int isEndLua = cmphstr(lp, "endlua");
                const size_t lineLen = isEndLua ? (lp - 6 - line) : strlen(line);
                if (execute) {
                        if (luaBufferSize < (bp - buff) + lineLen + 4) {
                                ErrorInt("[LUA] Maximum byte-size of Lua script is", luaBufferSize-4, FATAL);
                        }
                        memcpy(bp, line, lineLen);
                        bp += lineLen;
                        *bp++ = '\n';
                }
                if (isEndLua) {         // eat also any trailing eol-type of comment
                        skipEmitMessagePos = sourcePosStack.back();
                        ++CompiledCurrentLine;
                        lp = ReplaceDefine(lp);         // skip any empty substitutions and comments
                        substitutedLine = line;         // override substituted listing for ENDLUA
                        // take into account also warning suppression used at end of block
                        showWarning = showWarning && !suppressedById(W_LUA_MC_PASS);
                        break;
                }
                ListFile(true);
        }

        if (execute) {
                extraErrorWarningPrefix = lua_err_prefix;
                *bp = 0;
                DidEmitByte();                  // reset the flag before running lua script
                if (luaL_loadbuffer(LUA, buff, bp-buff, lua_impl_get_script_name(luaStartPos)) || lua_pcall(LUA, 0, LUA_MULTRET, 0)) {
                        lua_impl_showLoadError(errorType);
                }
                extraErrorWarningPrefix = nullptr;
                delete[] buff;
                if (DidEmitByte() && (-1 != passToExec) && showWarning) {
                        const EWStatus warningType = (1 == passToExec || 2 == passToExec) ? W_EARLY : W_PASS3;
                        WarningById(W_LUA_MC_PASS, nullptr, warningType);
                }
        }

        ++CompiledCurrentLine;
        substitutedLine = line;         // override substituted list line for ENDLUA
}

void dirINCLUDELUA() {
        // lazy init of Lua scripting upon first hit of INCLUDELUA directive
        if (nullptr == LUA) lua_impl_init();
        assert(LUA);

        if (1 != pass) {
                SkipToEol(lp);          // skip till EOL (colon), to avoid parsing file name
                return;
        }
        std::unique_ptr<char[]> fnaam(GetFileName(lp));
        EDelimiterType dt = GetDelimiterOfLastFileName();
        char* fullpath = GetPath(fnaam.get(), NULL, DT_ANGLE == dt);
        if (!fullpath[0]) {
                Error("[INCLUDELUA] File doesn't exist", fnaam.get(), EARLY);
        } else {
                extraErrorWarningPrefix = lua_err_prefix;
                fileNameFull = ArchiveFilename(fullpath);       // get const pointer into archive
                if (luaL_dofile(LUA, fileNameFull)) {
                        lua_impl_showLoadError(EARLY);
                }
                extraErrorWarningPrefix = nullptr;
        }
        free(fullpath);
}

#endif //USE_LUA

////////////////////////////////////////////////////////////////////////////////////////////
// close LUA engine and release everything related
void lua_impl_close() {
        #ifdef USE_LUA
                // if Lua was used and initialised, release everything
                if (LUA) lua_close(LUA);
        #endif //USE_LUA

        // do nothing when Lua is compile-time disabled
}

//eof lua_sjasm.cpp