diff --git a/README.md b/README.md index aa6e477..eeb2aa1 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,87 @@ The DLL does code preprocessing and other operations that were deemed too slow t The scripts use `object_event_add` + `event_perform_object` to store and call the final code without parsing penalties that would occur with `execute_string`. -## Intended use cases -

+## Syntactic sugar + + + + + + + + + + + + + +
SyntaxResultNotes
+ +```gml +var i = 0 +globalvar g = 1 +``` + + + +```gml +var i; i = 0 +globalvar g; g = 1 +``` -![shrug](./misc/shrug.png)\ -~~What use cases?~~ + + +Single-variable declarations only! + +
+ +```gml +enum E { + A, + B, + C = 5, + D +} +trace(E.D); +``` + + + +```gml +global.__E__A = 0; +global.__E__B = 1; +global.__E__C = 5; +global.__E__D = 6; +trace(global.__E__D); +``` + + + +Init is done on spot - avoid function calls in enum field "values". + +
+ +```gml +missing(1, 2); +``` + + + +```gml +snippet_call("missing", 1, 2); +``` + + + +Allows snippets to call each other without forward declaration. + +
+ +## Intended use cases -

+| ![shrug](./misc/shrug.png) | +|:-:| +| ~~What use cases?~~ | You could use it for dynamic content loading if you are making a game in GM8.1. @@ -57,6 +131,7 @@ If you compile a [GMEdit](https://github.com/YellowAfterlife/GMEdit/) version fr - Wildcard support in listfiles?\ (`folder/*.gml`) +- `script_execute` hook (that does either script_execute or snippet_call depending on whether index is number/string) ## Meta diff --git a/snippets/CharTools.h b/snippets/CharTools.h index 8cea18b..0fe49a6 100644 --- a/snippets/CharTools.h +++ b/snippets/CharTools.h @@ -1,6 +1,9 @@ #pragma once namespace CharTools { + inline bool isLineSpace(char c) { + return c == ' ' || c == '\t'; + } inline bool isSpace(char c) { switch (c) { case ' ': case '\t': case '\r': case '\n': return true; diff --git a/snippets/preBuild.bat b/snippets/preBuild.bat index c53dc20..55c9f34 100644 --- a/snippets/preBuild.bat +++ b/snippets/preBuild.bat @@ -1,4 +1,5 @@ @echo off +goto bye set dllPath=%~1 set solutionDir=%~2 set projectDir=%~3 @@ -16,4 +17,5 @@ if %ERRORLEVEL% EQU 0 ( --gml "%solutionDir%snippets_gml/snippets_discard.gml"^ --gmk "%solutionDir%snippets_gml/snippets_autogen.gml"^ %projectDir%snippets.cpp -) \ No newline at end of file +) +:bye \ No newline at end of file diff --git a/snippets/snippets.cpp b/snippets/snippets.cpp index e23bea0..9dd31b7 100644 --- a/snippets/snippets.cpp +++ b/snippets/snippets.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -30,12 +31,19 @@ static bool is_gml_keyword(std::string& s) { static std::unordered_set gml_functions{}; +struct snippet_enum { + std::unordered_set fields{}; +}; +static std::unordered_map gml_enums{}; + class snippet_preproc_t { public: std::stringstream out{}; + std::stringstream init{}; std::string out_name{}; std::queue names{}; std::queue codes{}; + std::string splitter{}; /// (including the ones that were empty and weren't added to output) int found = 0; int pos = 0; @@ -47,9 +55,29 @@ class snippet_preproc_t { void skipSpaces() { while (pos < len) { auto c = str[pos]; - if (CharTools::isSpace(c)) pos += 1; else break; + if (CharTools::isSpace(c)) { + pos += 1; + continue; + } + if (c == '/') { + if (str[pos + 1] == '/') { + skipLine(); + continue; + } + if (str[pos + 1] == '*') { + if (skipCommentBlock()) break; + continue; + } + } + break; } } + bool skipIfEqu(char c) { + if (str[pos] == c) { + pos += 1; + return true; + } else return false; + } void skipLine() { while (pos < len) { if (str[pos++] == '\n') break; @@ -60,7 +88,13 @@ class snippet_preproc_t { } std::string readIdent(bool afterFirst = true) { auto identStart = pos; - if (afterFirst) identStart -= 1; + if (afterFirst) { + identStart -= 1; + } else { + if (CharTools::isIdentStart(str[pos])) { + pos += 1; + } else return ""; + } while (pos < len) { auto c = str[pos]; if (CharTools::isIdent(c)) pos += 1; else break; @@ -97,7 +131,23 @@ class snippet_preproc_t { } out = std::stringstream(); } - bool skipCommon(char c, const char* splitter) { + bool skipCommentBlock() { + while (pos < len) { + auto c = str[pos]; + if (c == '/' && str[pos - 1] == '*') break; + if (c == '#' && str[pos - 1] == '\n') { + auto beforeWord = pos++; + auto word = readIdent(false); + if (word == splitter) { + pos = beforeWord; + return true; + } + } + pos += 1; + } + return false; + } + bool skipCommon(char c) { switch (c) { case '/': switch (str[pos]) { @@ -105,19 +155,7 @@ class snippet_preproc_t { skipLine(); return true; case '*': - while (pos < len) { - c = str[pos]; - if (c == '/' && str[pos - 1] == '*') break; - if (c == '#' && str[pos - 1] == '\n') { - auto beforeWord = pos++; - auto word = readIdent(false); - if (word == splitter) { - pos = beforeWord; - break; - } - } - pos += 1; - } + skipCommentBlock(); return true; } return false; @@ -129,172 +167,303 @@ class snippet_preproc_t { } return false; } - int run(const char* first_name, const char* code, const char* splitter) { + int run(const char* first_name, const char* code, const char* _splitter) { str = code; pos = 0; start = 0; found = 0; len = strlen(code); out = std::stringstream(); + init = std::stringstream(); out_name = first_name; while (!names.empty()) names.pop(); while (!codes.empty()) codes.pop(); - bool is_script = std::string(splitter) == "define"; + splitter = _splitter; + bool is_script = splitter == "define"; while (pos < len) { auto c = str[pos++]; if (CharTools::isSpace(c)) continue; - if (skipCommon(c, splitter)) continue; + if (skipCommon(c)) continue; - if (CharTools::isIdentStart(c)) { - auto before_pos = pos - 1; - auto word = readIdent(); - auto wlen = word.size(); + if (!CharTools::isIdentStart(c)) continue; - if (is_script && word == "return") { // `return X` -> `for ({}; true; exit) global.__snippet_result = X` - flush(before_pos); - out << "for ({}; true; exit) global.__snippet__result ="; - start = pos; - continue; - } + auto before_pos = pos - 1; + auto word = readIdent(); + auto wlen = word.size(); - if (word == "var" || word == "globalvar") { // `var i = 1` -> `var i; i = 1` - skipSpaces(); - auto before_vname = pos; - auto vname = readIdent(false); - if (is_gml_keyword(vname)) { // `var var name` - pos = before_vname; - continue; - } - auto before_equ = pos; - skipSpaces(); - if (str[pos] == '=' || str[pos] == ':' && str[pos + 1] == '=') { - flush(before_pos); - out << word << " " << vname << "; " << vname; - start = before_equ; - } + if (is_script && word == "return") { // `return X` -> `for ({}; true; exit) global.__snippet_result = X` + flush(before_pos); + out << "for ({}; true; exit) global.__snippet__result ="; + start = pos; + continue; + } + + if (word == "var" || word == "globalvar") { // `var i = 1` -> `var i; i = 1` + skipSpaces(); + auto before_vname = pos; + auto vname = readIdent(false); + if (is_gml_keyword(vname)) { // `var var name` + pos = before_vname; continue; } + auto before_equ = pos; + skipSpaces(); + if (str[pos] == '=' || str[pos] == ':' && str[pos + 1] == '=') { + flush(before_pos); + out << word << " " << vname << "; " << vname; + start = before_equ; + } + continue; + } - // `for (var i = 0;` -> `for ({ var i; i = 0 };` - if (word == "for") { - auto after_for = pos; - #define for_cancel { pos = after_for; continue; } + // `for (var i = 0;` -> `for ({ var i; i = 0 };` + if (word == "for") { + auto after_for = pos; + #define for_cancel { pos = after_for; continue; } + + skipSpaces(); + if (str[pos++] != '(') for_cancel; + auto after_par = pos; + + skipSpaces(); + if (str[pos] != 'v') for_cancel; + auto before_maybe_var = pos; + auto maybe_var = readIdent(false); + if (maybe_var != "var") for_cancel; + + skipSpaces(); + auto var_name = readIdent(false); + if (var_name == "") for_cancel; + auto after_var_name = pos; + + skipSpaces(); + if (str[pos] == ':' && str[pos + 1] == '=') { + pos += 2; + } else if (str[pos] == '=') { + pos += 1; + } else for_cancel; + auto after_set = pos; - skipSpaces(); - if (str[pos++] != '(') for_cancel; - auto after_par = pos; + while (pos < len) { + c = str[pos++]; + if (skipCommon(c)) continue; + if (c == ';') { pos--; break; } + } + if (pos >= len) for_cancel; + + flush(before_maybe_var); + out << "{"; + start = before_maybe_var; + flush(after_var_name); + out << ";" << var_name; + start = after_var_name; + flush(pos); + out << "}"; + start = pos; + + #undef for_cancel + continue; + } + + if (word == "enum") { + auto after_kw = pos; + snippet_enum* eref = nullptr; + #define skip_cont { if (eref) delete eref; pos = after_kw; continue; } + + skipSpaces(); + auto ename = readIdent(false); + if (ename == "") skip_cont; + eref = new snippet_enum(); + skipSpaces(); + if (!skipIfEqu('{')) skip_cont; + + std::string elast = ""; + while (pos < len) { // read enum contents skipSpaces(); - if (str[pos] != 'v') for_cancel; - auto before_maybe_var = pos; - auto maybe_var = readIdent(false); - if (maybe_var != "var") for_cancel; + c = str[pos++]; + + if (!CharTools::isIdentStart(c)) { + init << "show_error(\"Unexpected character '" << c << "' in enum " << ename << " declaration\", true);\r\n"; + break; + } + + auto efield = readIdent(); skipSpaces(); - auto var_name = readIdent(false); - if (var_name == "") for_cancel; - auto after_var_name = pos; + std::string val{}; + if (skipIfEqu('=')) { + auto depth = 0; + auto loop = true; + auto valStart = pos; + while (pos < len && loop) { // read enum ctr value + c = str[pos++]; + + if (CharTools::isSpace(c)) continue; + if (skipCommon(c)) continue; + switch (c) { + case '(': case '[': case '{': depth += 1; break; + case ')': case ']': case '}': + depth -= 1; + if (c == '}' && depth < 0) { + loop = false; + pos -= 1; + } + break; + case ',': + if (depth <= 0) { + loop = false; + pos -= 1; + } + break; + } + } + val = slice(valStart, pos); + } else if (elast != "") { + val = elast + " + 1"; + } else val = "0"; + + elast = "global.__" + ename + "__" + efield; + init << elast << " = " << val << ";\r\n"; + if (!set_contains(eref->fields, efield)) { + eref->fields.insert(efield); + } else { + init << "show_error(\"Duplicate field " << efield << " in enum " << ename << " declaration\", true);\r\n"; + } + skipSpaces(); - if (str[pos] == ':' && str[pos + 1] == '=') { - pos += 2; - } else if (str[pos] == '=') { - pos += 1; - } else for_cancel; - auto after_set = pos; - - while (pos < len) { - c = str[pos++]; - if (skipCommon(c, splitter)) continue; - if (c == ';') { pos--; break; } + c = str[pos++]; + if (c == '}') break; + if (c != ',') { + init << "show_error(\"Unexpected character '" << c + << "' in enum " << ename + << " declaration after " + << efield << "\", true);\r\n"; + break; } - if (pos >= len) for_cancel; - - flush(before_maybe_var); - out << "{"; - start = before_maybe_var; - flush(after_var_name); - out << ";" << var_name; - start = after_var_name; - flush(pos); - out << "}"; - start = pos; + } // enum fields - #undef for_cancel + if (!set_contains(gml_enums, ename)) { + gml_enums[ename] = eref; + } else { + init << "show_error(\"Enum " << ename << " has already been declared.\", true);\r\n"; } + #undef skip_cont + flush(before_pos); + start = pos; + continue; + } - if (is_script && word == "argument_count") { - flush(before_pos); - out << "global.__snippet__argument_count"; - start = pos; - continue; + if (is_script && word == "argument_count") { + flush(before_pos); + out << "global.__snippet__argument_count"; + start = pos; + continue; + } + + // `argument[#]` / `argument#` -> `global.__snippet__argument[#]` + const auto argn = sizeof("argument") - 1; + if (is_script && wlen >= argn && wlen <= argn + 2 && strncmp(word.c_str(), "argument", argn) == 0) do { + int argi; + if (wlen == argn) { + argi = -1; + } else { + argi = word[argn] - '0'; + if (argi < 0 || argi > 9) break; + if (wlen > argn + 1) { + argi = word[argn + 1] - '0'; + if (argi < 0 || argi > 5) break; + argi += 10; + } + } + flush(before_pos); + out << "global.__snippet__argument"; + if (argi >= 0) { + out << "[" << std::to_string(argi) << "]"; + } + start = pos; + continue; + } while (false); + + // #define + if (word == splitter && before_pos > 0 && str[before_pos - 1] == '#' + && (before_pos == 1 || str[before_pos - 2] == '\n') + ) { + skipSpaces(); + auto name_start = pos; + while (pos < len) { // name spans till eol/space/`(` + c = str[pos]; + if (c == '(') break; + if (CharTools::isSpace(c)) break; + pos += 1; } + auto name = slice(name_start, pos); + flushPart(before_pos - 1, false); + out_name = name; + skipLine(); + start = pos; + continue; + } - // `argument[#]` / `argument#` -> `global.__snippet__argument[#]` - const auto argn = sizeof("argument") - 1; - if (is_script && wlen >= argn && wlen <= argn + 2 && strncmp(word.c_str(), "argument", argn) == 0) do { - int argi; - if (wlen == argn) { - argi = -1; - } else { - argi = word[argn] - '0'; - if (argi < 0 || argi > 9) break; - if (wlen > argn + 1) { - argi = word[argn + 1] - '0'; - if (argi < 0 || argi > 5) break; - argi += 10; - } + // `func(` -> `snippet_call("func",` + auto is_keyword = is_gml_keyword(word); + if (!is_keyword && word != "snippet_call" && !set_contains(gml_functions, word)) { + auto peek = pos; + bool isCall = false; + while (peek < len) { + auto c1 = str[peek++]; + if (!CharTools::isSpace(c1)) { + isCall = c1 == '('; + break; } + } + if (isCall) { flush(before_pos); - out << "global.__snippet__argument"; - if (argi >= 0) { - out << "[" << std::to_string(argi) << "]"; - } + pos = peek; + // it's okay! Trailing commas are allowed in argument lists in GML + out << "snippet_call(\"" << word << "\","; start = pos; - continue; - } while (false); + } + } - if (word == splitter && before_pos > 0 && str[before_pos - 1] == '#' - && (before_pos == 1 || str[before_pos - 2] == '\n') - ) { // #define - skipSpaces(); - auto name_start = pos; - while (pos < len) { // name spans till eol/space/`(` - c = str[pos]; - if (c == '(') break; - if (CharTools::isSpace(c)) break; - pos += 1; - } - auto name = slice(name_start, pos); - flushPart(before_pos - 1, false); - out_name = name; - skipLine(); - start = pos; - continue; + // enum.field -> global.__enum__field + auto enum_pair = gml_enums.find(word); + if (!is_keyword && enum_pair != gml_enums.end()) { + auto p = before_pos; + bool ok = true; + while (--p >= 0) { + c = str[p]; + if (CharTools::isSpace(c)) continue; + if (c == '.') ok = false; // not `something.Enum.Field` + break; + } + + p = pos; + while (p < len) { + c = str[p++]; + if (CharTools::isSpace(c)) continue; + if (c != '.') ok = false; // should be `Enum.Field` + break; } - if (!is_gml_keyword(word) && word != "snippet_call" && !set_contains(gml_functions, word)) { // `func(` -> - auto peek = pos; - bool isCall = false; - while (peek < len) { - auto c1 = str[peek++]; - if (!CharTools::isSpace(c1)) { - isCall = c1 == '('; - break; - } - } - if (isCall) { + if (ok) { + pos = p; + skipSpaces(); + auto efield = readIdent(false); + auto eref = enum_pair->second; + if (efield != "" && set_contains(eref->fields, efield)) { flush(before_pos); - pos = peek; - // it's okay! Trailing commas are allowed in argument lists in GML - out << "snippet_call(\"" << word << "\","; + out << "global.__" << word << "__" << efield; start = pos; } } } + + // regular word! } flushPart(pos, true); return names.size(); @@ -305,6 +474,12 @@ dllx double snippet_preproc_run(const char* name, const char* code, const char* return snippet_preproc.run(name, code, splitter); } /// #gmki +dllx const char* snippet_preproc_get_init() { + static std::string s{}; + s = snippet_preproc.init.str(); + return s.c_str(); +} +/// #gmki dllx const char* snippet_preproc_pop_name() { static std::string s{}; s = snippet_preproc.names.front(); diff --git a/snippets_gml/snippets.gml b/snippets_gml/snippets.gml index 826572d..6b0713e 100644 --- a/snippets_gml/snippets.gml +++ b/snippets_gml/snippets.gml @@ -26,11 +26,11 @@ global.f_sniptools_string_split_start = external_define(_path, "sniptools_string global.f_sniptools_string_split_next = external_define(_path, "sniptools_string_split_next", dll_cdecl, ty_string, 0); global.f_sniptools_show_debug_message = external_define(_path, "sniptools_show_debug_message", dll_cdecl, ty_real, 1, ty_string); global.f_snippet_preproc_run = external_define(_path, "snippet_preproc_run", dll_cdecl, ty_real, 3, ty_string, ty_string, ty_string); +global.f_snippet_preproc_get_init = external_define(_path, "snippet_preproc_get_init", dll_cdecl, ty_string, 0); global.f_snippet_preproc_pop_name = external_define(_path, "snippet_preproc_pop_name", dll_cdecl, ty_string, 0); global.f_snippet_preproc_pop_code = external_define(_path, "snippet_preproc_pop_code", dll_cdecl, ty_string, 0); global.f_snippet_preproc_concat_names = external_define(_path, "snippet_preproc_concat_names", dll_cdecl, ty_string, 0); - #define snippet_init /// (?path_prefix) var _dir, _dir_auto; diff --git a/snippets_gml/tester/obj_control.object.gml b/snippets_gml/tester/obj_control.object.gml index c5bff44..7727f27 100644 --- a/snippets_gml/tester/obj_control.object.gml +++ b/snippets_gml/tester/obj_control.object.gml @@ -1,5 +1,12 @@ #event create start_time = current_time; +enum Test { + A, + B, + C = 100, + D +} +trace("D is " + string(Test.D)); #event keypress:vk_escape if (keyboard_check(vk_shift)) { diff --git a/snippets_gml/tester/snippet_tester.gm81 b/snippets_gml/tester/snippet_tester.gm81 index 6cf3c95..44a9d82 100644 Binary files a/snippets_gml/tester/snippet_tester.gm81 and b/snippets_gml/tester/snippet_tester.gm81 differ diff --git a/snippets_gml/tester/snippets.dll b/snippets_gml/tester/snippets.dll index decc83d..78afedd 100644 Binary files a/snippets_gml/tester/snippets.dll and b/snippets_gml/tester/snippets.dll differ