/Source/script.cpp
C++ | 9651 lines | 7330 code | 411 blank | 1910 comment | 1500 complexity | 61daa4a3e240093c68914366f52d2ccc MD5 | raw file
Possible License(s): AGPL-1.0, BSD-3-Clause
- /*
- AutoHotkey
- Copyright 2003-2009 Chris Mallett (support@autohotkey.com)
- This program is free software; you can redistribute it and/or
- modify it under the terms of the GNU General Public License
- as published by the Free Software Foundation; either version 2
- of the License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- */
- #include "stdafx.h" // pre-compiled headers
- #include "script.h"
- #include "globaldata.h" // for a lot of things
- #include "util.h" // for strlcpy() etc.
- #include "mt19937ar-cok.h" // for random number generator
- #include "window.h" // for a lot of things
- #include "application.h" // for MsgSleep()
- // Globals that are for only this module:
- #define MAX_COMMENT_FLAG_LENGTH 15
- static char g_CommentFlag[MAX_COMMENT_FLAG_LENGTH + 1] = ";"; // Adjust the below for any changes.
- static size_t g_CommentFlagLength = 1; // pre-calculated for performance
- // General note about the methods in here:
- // Want to be able to support multiple simultaneous points of execution
- // because more than one subroutine can be executing simultaneously
- // (well, more precisely, there can be more than one script subroutine
- // that's in a "currently running" state, even though all such subroutines,
- // except for the most recent one, are suspended. So keep this in mind when
- // using things such as static data members or static local variables.
- Script::Script()
- : mFirstLine(NULL), mLastLine(NULL), mCurrLine(NULL), mPlaceholderLabel(NULL), mLineCount(0)
- , mThisHotkeyName(""), mPriorHotkeyName(""), mThisHotkeyStartTime(0), mPriorHotkeyStartTime(0)
- , mEndChar(0), mThisHotkeyModifiersLR(0)
- , mNextClipboardViewer(NULL), mOnClipboardChangeIsRunning(false), mOnClipboardChangeLabel(NULL)
- , mOnExitLabel(NULL), mExitReason(EXIT_NONE)
- , mFirstLabel(NULL), mLastLabel(NULL)
- , mFirstFunc(NULL), mLastFunc(NULL)
- , mFirstTimer(NULL), mLastTimer(NULL), mTimerEnabledCount(0), mTimerCount(0)
- , mFirstMenu(NULL), mLastMenu(NULL), mMenuCount(0)
- , mVar(NULL), mVarCount(0), mVarCountMax(0), mLazyVar(NULL), mLazyVarCount(0)
- , mCurrentFuncOpenBlockCount(0), mNextLineIsFunctionBody(false)
- , mFuncExceptionVar(NULL), mFuncExceptionVarCount(0)
- , mCurrFileIndex(0), mCombinedLineNumber(0), mNoHotkeyLabels(true), mMenuUseErrorLevel(false)
- , mFileSpec(""), mFileDir(""), mFileName(""), mOurEXE(""), mOurEXEDir(""), mMainWindowTitle("")
- , mIsReadyToExecute(false), mAutoExecSectionIsRunning(false)
- , mIsRestart(false), mIsAutoIt2(false), mErrorStdOut(false)
- #ifdef AUTOHOTKEYSC
- , mCompiledHasCustomIcon(false)
- #else
- , mIncludeLibraryFunctionsThenExit(NULL)
- #endif
- , mLinesExecutedThisCycle(0), mUninterruptedLineCountMax(1000), mUninterruptibleTime(15)
- , mRunAsUser(NULL), mRunAsPass(NULL), mRunAsDomain(NULL)
- , mCustomIcon(NULL) // Normally NULL unless there's a custom tray icon loaded dynamically.
- , mCustomIconFile(NULL), mIconFrozen(false), mTrayIconTip(NULL) // Allocated on first use.
- , mCustomIconNumber(0)
- {
- // v1.0.25: mLastScriptRest and mLastPeekTime are now initialized right before the auto-exec
- // section of the script is launched, which avoids an initial Sleep(10) in ExecUntil
- // that would otherwise occur.
- *mThisMenuItemName = *mThisMenuName = '\0';
- ZeroMemory(&mNIC, sizeof(mNIC)); // Constructor initializes this, to be safe.
- mNIC.hWnd = NULL; // Set this as an indicator that it tray icon is not installed.
- // Lastly (after the above have been initialized), anything that can fail:
- if ( !(mTrayMenu = AddMenu("Tray")) ) // realistically never happens
- {
- ScriptError("No tray mem");
- ExitApp(EXIT_CRITICAL);
- }
- else
- mTrayMenu->mIncludeStandardItems = true;
- #ifdef _DEBUG
- if (ID_FILE_EXIT < ID_MAIN_FIRST) // Not a very thorough check.
- ScriptError("DEBUG: ID_FILE_EXIT is too large (conflicts with IDs reserved via ID_USER_FIRST).");
- if (MAX_CONTROLS_PER_GUI > ID_USER_FIRST - 3)
- ScriptError("DEBUG: MAX_CONTROLS_PER_GUI is too large (conflicts with IDs reserved via ID_USER_FIRST).");
- int LargestMaxParams, i, j;
- ActionTypeType *np;
- // Find the Largest value of MaxParams used by any command and make sure it
- // isn't something larger than expected by the parsing routines:
- for (LargestMaxParams = i = 0; i < g_ActionCount; ++i)
- {
- if (g_act[i].MaxParams > LargestMaxParams)
- LargestMaxParams = g_act[i].MaxParams;
- // This next part has been tested and it does work, but only if one of the arrays
- // contains exactly MAX_NUMERIC_PARAMS number of elements and isn't zero terminated.
- // Relies on short-circuit boolean order:
- for (np = g_act[i].NumericParams, j = 0; j < MAX_NUMERIC_PARAMS && *np; ++j, ++np);
- if (j >= MAX_NUMERIC_PARAMS)
- {
- ScriptError("DEBUG: At least one command has a NumericParams array that isn't zero-terminated."
- " This would result in reading beyond the bounds of the array.");
- return;
- }
- }
- if (LargestMaxParams > MAX_ARGS)
- ScriptError("DEBUG: At least one command supports more arguments than allowed.");
- if (sizeof(ActionTypeType) == 1 && g_ActionCount > 256)
- ScriptError("DEBUG: Since there are now more than 256 Action Types, the ActionTypeType"
- " typedef must be changed.");
- #endif
- }
- Script::~Script() // Destructor.
- {
- // MSDN: "Before terminating, an application must call the UnhookWindowsHookEx function to free
- // system resources associated with the hook."
- AddRemoveHooks(0); // Remove all hooks.
- if (mNIC.hWnd) // Tray icon is installed.
- Shell_NotifyIcon(NIM_DELETE, &mNIC); // Remove it.
- // Destroy any Progress/SplashImage windows that haven't already been destroyed. This is necessary
- // because sometimes these windows aren't owned by the main window:
- int i;
- for (i = 0; i < MAX_PROGRESS_WINDOWS; ++i)
- {
- if (g_Progress[i].hwnd && IsWindow(g_Progress[i].hwnd))
- DestroyWindow(g_Progress[i].hwnd);
- if (g_Progress[i].hfont1) // Destroy font only after destroying the window that uses it.
- DeleteObject(g_Progress[i].hfont1);
- if (g_Progress[i].hfont2) // Destroy font only after destroying the window that uses it.
- DeleteObject(g_Progress[i].hfont2);
- if (g_Progress[i].hbrush)
- DeleteObject(g_Progress[i].hbrush);
- }
- for (i = 0; i < MAX_SPLASHIMAGE_WINDOWS; ++i)
- {
- if (g_SplashImage[i].pic)
- g_SplashImage[i].pic->Release();
- if (g_SplashImage[i].hwnd && IsWindow(g_SplashImage[i].hwnd))
- DestroyWindow(g_SplashImage[i].hwnd);
- if (g_SplashImage[i].hfont1) // Destroy font only after destroying the window that uses it.
- DeleteObject(g_SplashImage[i].hfont1);
- if (g_SplashImage[i].hfont2) // Destroy font only after destroying the window that uses it.
- DeleteObject(g_SplashImage[i].hfont2);
- if (g_SplashImage[i].hbrush)
- DeleteObject(g_SplashImage[i].hbrush);
- }
- // It is safer/easier to destroy the GUI windows prior to the menus (especially the menu bars).
- // This is because one GUI window might get destroyed and take with it a menu bar that is still
- // in use by an existing GUI window. GuiType::Destroy() adheres to this philosophy by detaching
- // its menu bar prior to destroying its window:
- for (i = 0; i < MAX_GUI_WINDOWS; ++i)
- GuiType::Destroy(i); // Static method to avoid problems with object destroying itself.
- for (i = 0; i < GuiType::sFontCount; ++i) // Now that GUI windows are gone, delete all GUI fonts.
- if (GuiType::sFont[i].hfont)
- DeleteObject(GuiType::sFont[i].hfont);
- // The above might attempt to delete an HFONT from GetStockObject(DEFAULT_GUI_FONT), etc.
- // But that should be harmless:
- // MSDN: "It is not necessary (but it is not harmful) to delete stock objects by calling DeleteObject."
- // Above: Probably best to have removed icon from tray and destroyed any Gui/Splash windows that were
- // using it prior to getting rid of the script's custom icon below:
- if (mCustomIcon)
- DestroyIcon(mCustomIcon);
- // Since they're not associated with a window, we must free the resources for all popup menus.
- // Update: Even if a menu is being used as a GUI window's menu bar, see note above for why menu
- // destruction is done AFTER the GUI windows are destroyed:
- UserMenu *menu_to_delete;
- for (UserMenu *m = mFirstMenu; m;)
- {
- menu_to_delete = m;
- m = m->mNextMenu;
- ScriptDeleteMenu(menu_to_delete);
- // Above call should not return FAIL, since the only way FAIL can realistically happen is
- // when a GUI window is still using the menu as its menu bar. But all GUI windows are gone now.
- }
- // Since tooltip windows are unowned, they should be destroyed to avoid resource leak:
- for (i = 0; i < MAX_TOOLTIPS; ++i)
- if (g_hWndToolTip[i] && IsWindow(g_hWndToolTip[i]))
- DestroyWindow(g_hWndToolTip[i]);
- if (g_hFontSplash) // The splash window itself should auto-destroyed, since it's owned by main.
- DeleteObject(g_hFontSplash);
- if (mOnClipboardChangeLabel) // Remove from viewer chain.
- ChangeClipboardChain(g_hWnd, mNextClipboardViewer);
- // Close any open sound item to prevent hang-on-exit in certain operating systems or conditions.
- // If there's any chance that a sound was played and not closed out, or that it is still playing,
- // this check is done. Otherwise, the check is avoided since it might be a high overhead call,
- // especially if the sound subsystem part of the OS is currently swapped out or something:
- if (g_SoundWasPlayed)
- {
- char buf[MAX_PATH * 2];
- mciSendString("status " SOUNDPLAY_ALIAS " mode", buf, sizeof(buf), NULL);
- if (*buf) // "playing" or "stopped"
- mciSendString("close " SOUNDPLAY_ALIAS, NULL, 0, NULL);
- }
- #ifdef ENABLE_KEY_HISTORY_FILE
- KeyHistoryToFile(); // Close the KeyHistory file if it's open.
- #endif
- DeleteCriticalSection(&g_CriticalRegExCache); // g_CriticalRegExCache is used elsewhere for thread-safety.
- }
- ResultType Script::Init(global_struct &g, char *aScriptFilename, bool aIsRestart)
- // Returns OK or FAIL.
- // Caller has provided an empty string for aScriptFilename if this is a compiled script.
- // Otherwise, aScriptFilename can be NULL if caller hasn't determined the filename of the script yet.
- {
- mIsRestart = aIsRestart;
- char buf[2048]; // Just to make sure we have plenty of room to do things with.
- #ifdef AUTOHOTKEYSC
- // Fix for v1.0.29: Override the caller's use of __argv[0] by using GetModuleFileName(),
- // so that when the script is started from the command line but the user didn't type the
- // extension, the extension will be included. This necessary because otherwise
- // #SingleInstance wouldn't be able to detect duplicate versions in every case.
- // It also provides more consistency.
- GetModuleFileName(NULL, buf, sizeof(buf));
- #else
- if (!aScriptFilename) // v1.0.46.08: Change in policy: store the default script in the My Documents directory rather than in Program Files. It's more correct and solves issues that occur due to Vista's file-protection scheme.
- {
- // Since no script-file was specified on the command line, use the default name.
- // For backward compatibility, FIRST check if there's an AutoHotkey.ini file in the current
- // directory. If there is, that needs to be used to retain compatibility.
- aScriptFilename = NAME_P ".ini";
- if (GetFileAttributes(aScriptFilename) == 0xFFFFFFFF) // File doesn't exist, so fall back to new method.
- {
- aScriptFilename = buf;
- VarSizeType filespec_length = BIV_MyDocuments(aScriptFilename, ""); // e.g. C:\Documents and Settings\Home\My Documents
- if (filespec_length > sizeof(buf)-16) // Need room for 16 characters ('\\' + "AutoHotkey.ahk" + terminator).
- return FAIL; // Very rare, so for simplicity just abort.
- strcpy(aScriptFilename + filespec_length, "\\AutoHotkey.ahk"); // Append the filename: .ahk vs. .ini seems slightly better in terms of clarity and usefulness (e.g. the ability to double click the default script to launch it).
- // Now everything is set up right because even if aScriptFilename is a nonexistent file, the
- // user will be prompted to create it by a stage further below.
- }
- //else since the legacy .ini file exists, everything is now set up right. (The file might be a directory, but that isn't checked due to rarity.)
- }
- // In case the script is a relative filespec (relative to current working dir):
- char *unused;
- if (!GetFullPathName(aScriptFilename, sizeof(buf), buf, &unused)) // This is also relied upon by mIncludeLibraryFunctionsThenExit. Succeeds even on nonexistent files.
- return FAIL; // Due to rarity, no error msg, just abort.
- #endif
- // Using the correct case not only makes it look better in title bar & tray tool tip,
- // it also helps with the detection of "this script already running" since otherwise
- // it might not find the dupe if the same script name is launched with different
- // lowercase/uppercase letters:
- ConvertFilespecToCorrectCase(buf); // This might change the length, e.g. due to expansion of 8.3 filename.
- char *filename_marker;
- if ( !(filename_marker = strrchr(buf, '\\')) )
- filename_marker = buf;
- else
- ++filename_marker;
- if ( !(mFileSpec = SimpleHeap::Malloc(buf)) ) // The full spec is stored for convenience, and it's relied upon by mIncludeLibraryFunctionsThenExit.
- return FAIL; // It already displayed the error for us.
- filename_marker[-1] = '\0'; // Terminate buf in this position to divide the string.
- size_t filename_length = strlen(filename_marker);
- if ( mIsAutoIt2 = (filename_length >= 4 && !stricmp(filename_marker + filename_length - 4, EXT_AUTOIT2)) )
- {
- // Set the old/AutoIt2 defaults for maximum safety and compatibilility.
- // Standalone EXEs (compiled scripts) are always considered to be non-AutoIt2 (otherwise,
- // the user should probably be using the AutoIt2 compiler).
- g_AllowSameLineComments = false;
- g_EscapeChar = '\\';
- g.TitleFindFast = true; // In case the normal default is false.
- g.DetectHiddenText = false;
- // Make the mouse fast like AutoIt2, but not quite insta-move. 2 is expected to be more
- // reliable than 1 since the AutoIt author said that values less than 2 might cause the
- // drag to fail (perhaps just for specific apps, such as games):
- g.DefaultMouseSpeed = 2;
- g.KeyDelay = 20;
- g.WinDelay = 500;
- g.LinesPerCycle = 1;
- g.IntervalBeforeRest = -1; // i.e. this method is disabled by default for AutoIt2 scripts.
- // Reduce max params so that any non escaped delimiters the user may be using literally
- // in "window text" will still be considered literal, rather than as delimiters for
- // args that are not supported by AutoIt2, such as exclude-title, exclude-text, MsgBox
- // timeout, etc. Note: Don't need to change IfWinExist and such because those already
- // have special handling to recognize whether exclude-title is really a valid command
- // instead (e.g. IfWinExist, title, text, Gosub, something).
- // NOTE: DO NOT ADD the IfWin command series to this section, since there is special handling
- // for parsing those commands to figure out whether they're being used in the old AutoIt2
- // style or the new Exclude Title/Text mode.
- // v1.0.40.02: The following is no longer done because a different mechanism is required now
- // that the ARGn macros do not check whether mArgc is too small and substitute an empty string
- // (instead, there is a loop in ExpandArgs that puts an empty string in each sArgDeref entry
- // for which the script omitted a parameter [and that loop relies on MaxParams being absolutely
- // accurate rather than conditional upon whether the script is of type ".aut"]).
- //g_act[ACT_FILESELECTFILE].MaxParams -= 2;
- //g_act[ACT_FILEREMOVEDIR].MaxParams -= 1;
- //g_act[ACT_MSGBOX].MaxParams -= 1;
- //g_act[ACT_INIREAD].MaxParams -= 1;
- //g_act[ACT_STRINGREPLACE].MaxParams -= 1;
- //g_act[ACT_STRINGGETPOS].MaxParams -= 2;
- //g_act[ACT_WINCLOSE].MaxParams -= 3; // -3 for these two, -2 for the others.
- //g_act[ACT_WINKILL].MaxParams -= 3;
- //g_act[ACT_WINACTIVATE].MaxParams -= 2;
- //g_act[ACT_WINMINIMIZE].MaxParams -= 2;
- //g_act[ACT_WINMAXIMIZE].MaxParams -= 2;
- //g_act[ACT_WINRESTORE].MaxParams -= 2;
- //g_act[ACT_WINHIDE].MaxParams -= 2;
- //g_act[ACT_WINSHOW].MaxParams -= 2;
- //g_act[ACT_WINSETTITLE].MaxParams -= 2;
- //g_act[ACT_WINGETTITLE].MaxParams -= 2;
- }
- if ( !(mFileDir = SimpleHeap::Malloc(buf)) )
- return FAIL; // It already displayed the error for us.
- if ( !(mFileName = SimpleHeap::Malloc(filename_marker)) )
- return FAIL; // It already displayed the error for us.
- #ifdef AUTOHOTKEYSC
- // Omit AutoHotkey from the window title, like AutoIt3 does for its compiled scripts.
- // One reason for this is to reduce backlash if evil-doers create viruses and such
- // with the program:
- snprintf(buf, sizeof(buf), "%s\\%s", mFileDir, mFileName);
- #else
- snprintf(buf, sizeof(buf), "%s\\%s - %s", mFileDir, mFileName, NAME_PV);
- #endif
- if ( !(mMainWindowTitle = SimpleHeap::Malloc(buf)) )
- return FAIL; // It already displayed the error for us.
- // It may be better to get the module name this way rather than reading it from the registry
- // (though it might be more proper to parse it out of the command line args or something),
- // in case the user has moved it to a folder other than the install folder, hasn't installed it,
- // or has renamed the EXE file itself. Also, enclose the full filespec of the module in double
- // quotes since that's how callers usually want it because ActionExec() currently needs it that way:
- *buf = '"';
- if (GetModuleFileName(NULL, buf + 1, sizeof(buf) - 2)) // -2 to leave room for the enclosing double quotes.
- {
- size_t buf_length = strlen(buf);
- buf[buf_length++] = '"';
- buf[buf_length] = '\0';
- if ( !(mOurEXE = SimpleHeap::Malloc(buf)) )
- return FAIL; // It already displayed the error for us.
- else
- {
- char *last_backslash = strrchr(buf, '\\');
- if (!last_backslash) // probably can't happen due to the nature of GetModuleFileName().
- mOurEXEDir = "";
- last_backslash[1] = '\0'; // i.e. keep the trailing backslash for convenience.
- if ( !(mOurEXEDir = SimpleHeap::Malloc(buf + 1)) ) // +1 to omit the leading double-quote.
- return FAIL; // It already displayed the error for us.
- }
- }
- return OK;
- }
-
- ResultType Script::CreateWindows()
- // Returns OK or FAIL.
- {
- if (!mMainWindowTitle || !*mMainWindowTitle) return FAIL; // Init() must be called before this function.
- // Register a window class for the main window:
- WNDCLASSEX wc = {0};
- wc.cbSize = sizeof(wc);
- wc.lpszClassName = WINDOW_CLASS_MAIN;
- wc.hInstance = g_hInstance;
- wc.lpfnWndProc = MainWindowProc;
- // The following are left at the default of NULL/0 set higher above:
- //wc.style = 0; // CS_HREDRAW | CS_VREDRAW
- //wc.cbClsExtra = 0;
- //wc.cbWndExtra = 0;
- wc.hIcon = wc.hIconSm = (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_MAIN), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
- wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
- wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1); // Needed for ProgressBar. Old: (HBRUSH)GetStockObject(WHITE_BRUSH);
- wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU_MAIN); // NULL; // "MainMenu";
- if (!RegisterClassEx(&wc))
- {
- MsgBox("RegClass"); // Short/generic msg since so rare.
- return FAIL;
- }
- // Register a second class for the splash window. The only difference is that
- // it doesn't have the menu bar:
- wc.lpszClassName = WINDOW_CLASS_SPLASH;
- wc.lpszMenuName = NULL; // Override the non-NULL value set higher above.
- if (!RegisterClassEx(&wc))
- {
- MsgBox("RegClass"); // Short/generic msg since so rare.
- return FAIL;
- }
- char class_name[64];
- HWND fore_win = GetForegroundWindow();
- bool do_minimize = !fore_win || (GetClassName(fore_win, class_name, sizeof(class_name))
- && !stricmp(class_name, "Shell_TrayWnd")); // Shell_TrayWnd is the taskbar's class on Win98/XP and probably the others too.
- // Note: the title below must be constructed the same was as is done by our
- // WinMain() (so that we can detect whether this script is already running)
- // which is why it's standardized in g_script.mMainWindowTitle.
- // Create the main window. Prevent momentary disruption of Start Menu, which
- // some users understandably don't like, by omitting the taskbar button temporarily.
- // This is done because testing shows that minimizing the window further below, even
- // though the window is hidden, would otherwise briefly show the taskbar button (or
- // at least redraw the taskbar). Sometimes this isn't noticeable, but other times
- // (such as when the system is under heavy load) a user reported that it is quite
- // noticeable. WS_EX_TOOLWINDOW is used instead of WS_EX_NOACTIVATE because
- // WS_EX_NOACTIVATE is available only on 2000/XP.
- if ( !(g_hWnd = CreateWindowEx(do_minimize ? WS_EX_TOOLWINDOW : 0
- , WINDOW_CLASS_MAIN
- , mMainWindowTitle
- , WS_OVERLAPPEDWINDOW // Style. Alt: WS_POPUP or maybe 0.
- , CW_USEDEFAULT // xpos
- , CW_USEDEFAULT // ypos
- , CW_USEDEFAULT // width
- , CW_USEDEFAULT // height
- , NULL // parent window
- , NULL // Identifies a menu, or specifies a child-window identifier depending on the window style
- , g_hInstance // passed into WinMain
- , NULL)) ) // lpParam
- {
- MsgBox("CreateWindow"); // Short msg since so rare.
- return FAIL;
- }
- #ifdef AUTOHOTKEYSC
- HMENU menu = GetMenu(g_hWnd);
- // Disable the Edit menu item, since it does nothing for a compiled script:
- EnableMenuItem(menu, ID_FILE_EDITSCRIPT, MF_DISABLED | MF_GRAYED);
- EnableOrDisableViewMenuItems(menu, MF_DISABLED | MF_GRAYED); // Fix for v1.0.47.06: No point in checking g_AllowMainWindow because the script hasn't starting running yet, so it will always be false.
- // But leave the ID_VIEW_REFRESH menu item enabled because if the script contains a
- // command such as ListLines in it, Refresh can be validly used.
- #endif
- if ( !(g_hWndEdit = CreateWindow("edit", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER
- | ES_LEFT | ES_MULTILINE | ES_READONLY | WS_VSCROLL // | WS_HSCROLL (saves space)
- , 0, 0, 0, 0, g_hWnd, (HMENU)1, g_hInstance, NULL)) )
- {
- MsgBox("CreateWindow"); // Short msg since so rare.
- return FAIL;
- }
- // FONTS: The font used by default, at least on XP, is GetStockObject(SYSTEM_FONT).
- // It seems preferable to smaller fonts such DEFAULT_GUI_FONT(DEFAULT_GUI_FONT).
- // For more info on pre-loaded fonts (not too many choices), see MSDN's GetStockObject().
- //SendMessage(g_hWndEdit, WM_SETFONT, (WPARAM)GetStockObject(SYSTEM_FONT), 0);
- // v1.0.30.05: Specifying a limit of zero opens the control to its maximum text capacity,
- // which removes the 32K size restriction. Testing shows that this does not increase the actual
- // amount of memory used for controls containing small amounts of text. All it does is allow
- // the control to allocate more memory as needed. By specifying zero, a max
- // of 64K becomes available on Windows 9x, and perhaps as much as 4 GB on NT/2k/XP.
- SendMessage(g_hWndEdit, EM_LIMITTEXT, 0, 0);
- // Some of the MSDN docs mention that an app's very first call to ShowWindow() makes that
- // function operate in a special mode. Therefore, it seems best to get that first call out
- // of the way to avoid the possibility that the first-call behavior will cause problems with
- // our normal use of ShowWindow() below and other places. Also, decided to ignore nCmdShow,
- // to avoid any momentary visual effects on startup.
- // Update: It's done a second time because the main window might now be visible if the process
- // that launched ours specified that. It seems best to override the requested state because
- // some calling processes might specify "maximize" or "shownormal" as generic launch method.
- // The script can display it's own main window with ListLines, etc.
- // MSDN: "the nCmdShow value is ignored in the first call to ShowWindow if the program that
- // launched the application specifies startup information in the structure. In this case,
- // ShowWindow uses the information specified in the STARTUPINFO structure to show the window.
- // On subsequent calls, the application must call ShowWindow with nCmdShow set to SW_SHOWDEFAULT
- // to use the startup information provided by the program that launched the application."
- ShowWindow(g_hWnd, SW_HIDE);
- ShowWindow(g_hWnd, SW_HIDE);
- // Now that the first call to ShowWindow() is out of the way, minimize the main window so that
- // if the script is launched from the Start Menu (and perhaps other places such as the
- // Quick-launch toolbar), the window that was active before the Start Menu was displayed will
- // become active again. But as of v1.0.25.09, this minimize is done more selectively to prevent
- // the launch of a script from knocking the user out of a full-screen game or other application
- // that would be disrupted by an SW_MINIMIZE:
- if (do_minimize)
- {
- ShowWindow(g_hWnd, SW_MINIMIZE);
- SetWindowLong(g_hWnd, GWL_EXSTYLE, 0); // Give the main window back its taskbar button.
- }
- // Note: When the window is not minimized, task manager reports that a simple script (such as
- // one consisting only of the single line "#Persistent") uses 2600 KB of memory vs. ~452 KB if
- // it were immediately minimized. That is probably just due to the vagaries of how the OS
- // manages windows and memory and probably doesn't actually impact system performance to the
- // degree indicated. In other words, it's hard to imagine that the failure to do
- // ShowWidnow(g_hWnd, SW_MINIMIZE) unconditionally upon startup (which causes the side effects
- // discussed further above) significantly increases the actual memory load on the system.
- g_hAccelTable = LoadAccelerators(g_hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
- if (g_NoTrayIcon)
- mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
- else
- // Even if the below fails, don't return FAIL in case the user is using a different shell
- // or something. In other words, it is expected to fail under certain circumstances and
- // we want to tolerate that:
- CreateTrayIcon();
- if (mOnClipboardChangeLabel)
- mNextClipboardViewer = SetClipboardViewer(g_hWnd);
- return OK;
- }
- void Script::EnableOrDisableViewMenuItems(HMENU aMenu, UINT aFlags)
- {
- EnableMenuItem(aMenu, ID_VIEW_KEYHISTORY, aFlags);
- EnableMenuItem(aMenu, ID_VIEW_LINES, aFlags);
- EnableMenuItem(aMenu, ID_VIEW_VARIABLES, aFlags);
- EnableMenuItem(aMenu, ID_VIEW_HOTKEYS, aFlags);
- }
- void Script::CreateTrayIcon()
- // It is the caller's responsibility to ensure that the previous icon is first freed/destroyed
- // before calling us to install a new one. However, that is probably not needed if the Explorer
- // crashed, since the memory used by the tray icon was probably destroyed along with it.
- {
- ZeroMemory(&mNIC, sizeof(mNIC)); // To be safe.
- // Using NOTIFYICONDATA_V2_SIZE vs. sizeof(NOTIFYICONDATA) improves compatibility with Win9x maybe.
- // MSDN: "Using [NOTIFYICONDATA_V2_SIZE] for cbSize will allow your application to use NOTIFYICONDATA
- // with earlier Shell32.dll versions, although without the version 6.0 enhancements."
- // Update: Using V2 gives an compile error so trying V1. Update: Trying sizeof(NOTIFYICONDATA)
- // for compatibility with VC++ 6.x. This is also what AutoIt3 uses:
- mNIC.cbSize = sizeof(NOTIFYICONDATA); // NOTIFYICONDATA_V1_SIZE
- mNIC.hWnd = g_hWnd;
- mNIC.uID = AHK_NOTIFYICON; // This is also used for the ID, see TRANSLATE_AHK_MSG for details.
- mNIC.uFlags = NIF_MESSAGE | NIF_TIP | NIF_ICON;
- mNIC.uCallbackMessage = AHK_NOTIFYICON;
- #ifdef AUTOHOTKEYSC
- // i.e. don't override the user's custom icon:
- mNIC.hIcon = mCustomIcon ? mCustomIcon : (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(mCompiledHasCustomIcon ? IDI_MAIN : g_IconTray), IMAGE_ICON, 0, 0, LR_SHARED);
- #else
- mNIC.hIcon = mCustomIcon ? mCustomIcon : (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(g_IconTray), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
- #endif
- UPDATE_TIP_FIELD
- // If we were called due to an Explorer crash, I don't think it's necessary to call
- // Shell_NotifyIcon() to remove the old tray icon because it was likely destroyed
- // along with Explorer. So just add it unconditionally:
- if (!Shell_NotifyIcon(NIM_ADD, &mNIC))
- mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
- }
- void Script::UpdateTrayIcon(bool aForceUpdate)
- {
- if (!mNIC.hWnd) // tray icon is not installed
- return;
- static bool icon_shows_paused = false;
- static bool icon_shows_suspended = false;
- if (!aForceUpdate && (mIconFrozen || (g->IsPaused == icon_shows_paused && g_IsSuspended == icon_shows_suspended)))
- return; // it's already in the right state
- int icon;
- if (g->IsPaused && g_IsSuspended)
- icon = IDI_PAUSE_SUSPEND;
- else if (g->IsPaused)
- icon = IDI_PAUSE;
- else if (g_IsSuspended)
- icon = g_IconTraySuspend;
- else
- #ifdef AUTOHOTKEYSC
- icon = mCompiledHasCustomIcon ? IDI_MAIN : g_IconTray; // i.e. don't override the user's custom icon.
- #else
- icon = g_IconTray;
- #endif
- // Use the custom tray icon if the icon is normal (non-paused & non-suspended):
- mNIC.hIcon = (mCustomIcon && (mIconFrozen || (!g->IsPaused && !g_IsSuspended))) ? mCustomIcon
- : (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(icon), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED for simplicity and performance more than to conserve memory in this case.
- if (Shell_NotifyIcon(NIM_MODIFY, &mNIC))
- {
- icon_shows_paused = g->IsPaused;
- icon_shows_suspended = g_IsSuspended;
- }
- // else do nothing, just leave it in the same state.
- }
- ResultType Script::AutoExecSection()
- // Returns FAIL if can't run due to critical error. Otherwise returns OK.
- {
- // Now that g_MaxThreadsTotal has been permanently set by the processing of script directives like
- // #MaxThreads, an appropriately sized array can be allocated:
- if ( !(g_array = (global_struct *)malloc((g_MaxThreadsTotal+TOTAL_ADDITIONAL_THREADS) * sizeof(global_struct))) )
- return FAIL; // Due to rarity, just abort. It wouldn't be safe to run ExitApp() due to possibility of an OnExit routine.
- CopyMemory(g_array, g, sizeof(global_struct)); // Copy the temporary/startup "g" into array[0] to preserve historical behaviors that may rely on the idle thread starting with that "g".
- g = g_array; // Must be done after above.
- // v1.0.48: Due to switching from SET_UNINTERRUPTIBLE_TIMER to IsInterruptible():
- // In spite of the comments in IsInterruptible(), periodically have a timer call IsInterruptible() due to
- // the following scenario:
- // - Interrupt timeout is 60 seconds (or 60 milliseconds for that matter).
- // - For some reason IsInterrupt() isn't called for 24+ hours even though there is a current/active thread.
- // - RefreshInterruptibility() fires at 23 hours and marks the thread interruptible.
- // - Sometime after that, one of the following happens:
- // Computer is suspended/hibernated and stays that way for 50+ days.
- // IsInterrupt() is never called (except by RefreshInterruptibility()) for 50+ days.
- // (above is currently unlikely because MSG_FILTER_MAX calls IsInterruptible())
- // In either case, RefreshInterruptibility() has prevented the uninterruptibility duration from being
- // wrongly extended by up to 100% of g_script.mUninterruptibleTime. This isn't a big deal if
- // g_script.mUninterruptibleTime is low (like it almost always is); but if it's fairly large, say an hour,
- // this can prevent an unwanted extension of up to 1 hour.
- // Although any call frequency less than 49.7 days should work, currently calling once per 23 hours
- // in case any older operating systems have a SetTimer() limit of less than 0x7FFFFFFF (and also to make
- // it less likely that a long suspend/hibernate would cause the above issue). The following was
- // actually tested on Windows XP and a message does indeed arrive 23 hours after the script starts.
- SetTimer(g_hWnd, TIMER_ID_REFRESH_INTERRUPTIBILITY, 23*60*60*1000, RefreshInterruptibility); // 3rd param must not exceed 0x7FFFFFFF (2147483647; 24.8 days).
- ResultType ExecUntil_result;
- if (!mFirstLine) // In case it's ever possible to be empty.
- ExecUntil_result = OK;
- // And continue on to do normal exit routine so that the right ExitCode is returned by the program.
- else
- {
- // Choose a timeout that's a reasonable compromise between the following competing priorities:
- // 1) That we want hotkeys to be responsive as soon as possible after the program launches
- // in case the user launches by pressing ENTER on a script, for example, and then immediately
- // tries to use a hotkey. In addition, we want any timed subroutines to start running ASAP
- // because in rare cases the user might rely upon that happening.
- // 2) To support the case when the auto-execute section never finishes (such as when it contains
- // an infinite loop to do background processing), yet we still want to allow the script
- // to put custom defaults into effect globally (for things such as KeyDelay).
- // Obviously, the above approach has its flaws; there are ways to construct a script that would
- // result in unexpected behavior. However, the combination of this approach with the fact that
- // the global defaults are updated *again* when/if the auto-execute section finally completes
- // raises the expectation of proper behavior to a very high level. In any case, I'm not sure there
- // is any better approach that wouldn't break existing scripts or require a redesign of some kind.
- // If this method proves unreliable due to disk activity slowing the program down to a crawl during
- // the critical milliseconds after launch, one thing that might fix that is to have ExecUntil()
- // be forced to run a minimum of, say, 100 lines (if there are that many) before allowing the
- // timer expiration to have its effect. But that's getting complicated and I'd rather not do it
- // unless someone actually reports that such a thing ever happens. Still, to reduce the chance
- // of such a thing ever happening, it seems best to boost the timeout from 50 up to 100:
- SET_AUTOEXEC_TIMER(100);
- mAutoExecSectionIsRunning = true;
- // v1.0.25: This is now done here, closer to the actual execution of the first line in the script,
- // to avoid an unnecessary Sleep(10) that would otherwise occur in ExecUntil:
- mLastScriptRest = mLastPeekTime = GetTickCount();
- ++g_nThreads;
- ExecUntil_result = mFirstLine->ExecUntil(UNTIL_RETURN); // Might never return (e.g. infinite loop or ExitApp).
- --g_nThreads;
- // Our caller will take care of setting g_default properly.
- KILL_AUTOEXEC_TIMER // See also: AutoExecSectionTimeout().
- mAutoExecSectionIsRunning = false;
- }
- // REMEMBER: The ExecUntil() call above will never return if the AutoExec section never finishes
- // (e.g. infinite loop) or it uses Exit/ExitApp.
- // The below is done even if AutoExecSectionTimeout() already set the values once.
- // This is because when the AutoExecute section finally does finish, by definition it's
- // supposed to store the global settings that are currently in effect as the default values.
- // In other words, the only purpose of AutoExecSectionTimeout() is to handle cases where
- // the AutoExecute section takes a long time to complete, or never completes (perhaps because
- // it is being used by the script as a "backround thread" of sorts):
- // Save the values of KeyDelay, WinDelay etc. in case they were changed by the auto-execute part
- // of the script. These new defaults will be put into effect whenever a new hotkey subroutine
- // is launched. Each launched subroutine may then change the values for its own purposes without
- // affecting the settings for other subroutines:
- global_clear_state(*g); // Start with a "clean slate" in both g_default and g (in case things like InitNewThread() check some of the values in g prior to launching a new thread).
- // Always want g_default.AllowInterruption==true so that InitNewThread() doesn't have to
- // set it except when Critical or "Thread Interrupt" require it. If the auto-execute section ended
- // without anyone needing to call IsInterruptible() on it, AllowInterruption could be false
- // even when Critical is off.
- // Even if the still-running AutoExec section has turned on Critical, the assignment below is still okay
- // because InitNewThread() adjusts AllowInterruption based on the value of ThreadIsCritical.
- // See similar code in AutoExecSectionTimeout().
- g->AllowThreadToBeInterrupted = true; // Mostly for the g_default line below. See comments above.
- CopyMemory(&g_default, g, sizeof(global_struct)); // g->IsPaused has been set to false higher above in case it's ever possible that it's true as a result of AutoExecSection().
- // After this point, the values in g_default should never be changed.
- global_maximize_interruptibility(*g); // See below.
- // Now that any changes made by the AutoExec section have been saved to g_default (including
- // the commands Critical and Thread), ensure that the very first g-item is always interruptible.
- // This avoids having to treat the first g-item as special in various places.
- // It seems best to set ErrorLevel to NONE after the auto-execute part of the script is done.
- // However, it isn't set to NONE right before launching each new thread (e.g. hotkey subroutine)
- // because it's more flexible that way (i.e. the user may want one hotkey subroutine to use the value
- // of ErrorLevel set by another). This reset was also done by LoadFromFile(), but it is done again
- // here in case the auto-execute section changed it:
- g_ErrorLevel->Assign(ERRORLEVEL_NONE);
- // BEFORE DOING THE BELOW, "g" and "g_default" should be set up properly in case there's an OnExit
- // routine (even non-persistent scripts can have one).
- // If no hotkeys are in effect, the user hasn't requested a hook to be activated, and the script
- // doesn't contain the #Persistent directive we're done unless there is an OnExit subroutine and it
- // doesn't do "ExitApp":
- if (!IS_PERSISTENT) // Resolve macro again in case any of its components changed since the last time.
- g_script.ExitApp(ExecUntil_result == FAIL ? EXIT_ERROR : EXIT_EXIT);
- return OK;
- }
- ResultType Script::Edit()
- {
- #ifdef AUTOHOTKEYSC
- return OK; // Do nothing.
- #else
- // This is here in case a compiled script ever uses the Edit command. Since the "Edit This
- // Script" menu item is not available for compiled scripts, it can't be called from there.
- TitleMatchModes old_mode = g->TitleMatchMode;
- g->TitleMatchMode = FIND_ANYWHERE;
- HWND hwnd = WinExist(*g, mFileName, "", mMainWindowTitle, ""); // Exclude our own main window.
- g->TitleMatchMode = old_mode;
- if (hwnd)
- {
- char class_name[32];
- GetClassName(hwnd, class_name, sizeof(class_name));
- if (!strcmp(class_name, "#32770") || !strnicmp(class_name, "AutoHotkey", 10)) // MessageBox(), InputBox(), FileSelectFile(), or GUI/script-owned window.
- hwnd = NULL; // Exclude it from consideration.
- }
- if (hwnd) // File appears to already be open for editing, so use the current window.
- SetForegroundWindowEx(hwnd);
- else
- {
- char buf[MAX_PATH * 2];
- // Enclose in double quotes anything that might contain spaces since the CreateProcess()
- // method, which is attempted first, is more likely to succeed. This is because it uses
- // the command line method of creating the process, with everything all lumped together:
- snprintf(buf, sizeof(buf), "\"%s\"", mFileSpec);
- if (!ActionExec("edit", buf, mFileDir, false)) // Since this didn't work, try notepad.
- {
- // v1.0.40.06: Try to open .ini files first with their associated editor rather than trying the
- // "edit" verb on them:
- char *file_ext;
- if ( !(file_ext = strrchr(mFileName, '.')) || stricmp(file_ext, ".ini")
- || !ActionExec("open", buf, mFileDir, false) ) // Relies on short-circuit boolean order.
- {
- // Even though notepad properly handles filenames with spaces in them under WinXP,
- // even without double quotes around them, it seems safer and more correct to always
- // enclose the filename in double quotes for maximum compatibility with all OSes:
- if (!ActionExec("notepad.exe", buf, mFileDir, false))
- MsgBox("Could not open script."); // Short message since so rare.
- }
- }
- }
- return OK;
- #endif
- }
- ResultType Script::Reload(bool aDisplayErrors)
- {
- // The new instance we're about to start will tell our process to stop, or it will display
- // a syntax error or some other error, in which case our process will still be running:
- #ifdef AUTOHOTKEYSC
- // This is here in case a compiled script ever uses the Reload command. Since the "Reload This
- // Script" menu item is not available for compiled scripts, it can't be called from there.
- return g_script.ActionExec(mOurEXE, "/restart", g_WorkingDirOrig, aDisplayErrors);
- #else
- char arg_string[MAX_PATH + 512];
- snprintf(arg_string, sizeof(arg_string), "/restart \"%s\"", mFileSpec);
- return g_script.ActionExec(mOurEXE, arg_string, g_WorkingDirOrig, aDisplayErrors);
- #endif
- }
- ResultType Script::ExitApp(ExitReasons aExitReason, char *aBuf, int aExitCode)
- // Normal exit (if aBuf is NULL), or a way to exit immediately on error (which is mostly
- // for times when it would be unsafe to call MsgBox() due to the possibility that it would
- // make the situation even worse).
- {
- mExitReason = aExitReason;
- bool terminate_afterward = aBuf && !*aBuf;
- if (aBuf && *aBuf)
- {
- char buf[1024];
- // No more than size-1 chars will be written and string will be terminated:
- snprintf(buf, sizeof(buf), "Critical Error: %s\n\n" WILL_EXIT, aBuf);
- // To avoid chance of more errors, don't use MsgBox():
- MessageBox(g_hWnd, buf, g_script.mFileSpec, MB_OK | MB_SETFOREGROUND | MB_APPLMODAL);
- TerminateApp(CRITICAL_ERROR); // Only after the above.
- }
- // Otherwise, it's not a critical error. Note that currently, mOnExitLabel can only be
- // non-NULL if the script is in a runnable state (since registering an OnExit label requires
- // that a script command has executed to do it). If this ever changes, the !mIsReadyToExecute
- // condition should be added to the below if statement:
- static bool sExitLabelIsRunning = false;
- if (!mOnExitLabel || sExitLabelIsRunning) // || !mIsReadyToExecute
- // In the case of sExitLabelIsRunning == true:
- // There is another instance of this function beneath us on the stack. Since we have
- // been called, this is a true exit condition and we exit immediately.
- // MUST NOT create a new thread when sExitLabelIsRunning because g_array allows only one
- // extra thread for ExitApp() (which allows it to run even when MAX_THREADS_EMERGENCY has
- // been reached). See TOTAL_ADDITIONAL_THREADS.
- TerminateApp(aExitCode);
- // Otherwise, the script contains the special RunOnExit label that we will run here instead
- // of exiting. And since it does, we know that the script is in a ready-to-execute state
- // because that is the only way an OnExit label could have been defined in the first place.
- // Usually, the RunOnExit subroutine will contain an Exit or ExitApp statement
- // which results in a recursive call to this function, but this is not required (e.g. the
- // Exit subroutine could display an "Are you sure?" prompt, and if the user chooses "No",
- // the Exit sequence can be aborted by simply not calling ExitApp and letting the thread
- // we create below end normally).
- // Next, save the current state of the globals so that they can be restored just prior
- // to returning to our caller:
- char ErrorLevel_saved[ERRORLEVEL_SAVED_SIZE];
- strlcpy(ErrorLevel_saved, g_ErrorLevel->Contents(), sizeof(ErrorLevel_saved)); // Save caller's errorlevel.
- InitNewThread(0, true, true, ACT_INVALID); // Uninterruptibility is handled below. Since this special thread should always run, no checking of g_MaxThreadsTotal is done before calling this.
- // Turn on uninterruptibility to forbid any hotkeys, timers, or user defined menu items
- // to interrupt. This is mainly done for peace-of-mind (since possible interactions due to
- // interruptions have not been studied) and the fact that this most users would not want this
- // subroutine to be interruptible (it usually runs quickly anyway). Another reason to make
- // it non-interruptible is that some OnExit subroutines might destruct things used by the
- // script's hotkeys/timers/menu items, and activating these items during the deconstruction
- // would not be safe. Finally, if a logoff or shutdown is occurring, it seems best to prevent
- // timed subroutines from running -- which might take too much time and prevent the exit from
- // occurring in a timely fashion. An option can be added via the FutureUse param to make it
- // interruptible if there is ever a demand for that.
- // UPDATE: g_AllowInterruption is now used instead of g->AllowThreadToBeInterrupted for two reasons:
- // 1) It avoids the need to do "int mUninterruptedLineCountMax_prev = g_script.mUninterruptedLineCountMax;"
- // (Disable this item so that ExecUntil() won't automatically make our new thread uninterruptible
- // after it has executed a certain number of lines).
- // 2) Mostly obsolete: If the thread we're interrupting is uninterruptible, the uinterruptible timer
- // might be currently pending. When it fires, it would make the OnExit subroutine interruptible
- // rather than the underlying subroutine. The above fixes the first part of that problem.
- // The 2nd part is fixed by reinstating the timer when the uninterruptible thread is resumed.
- // This special handling is only necessary here -- not in other places where new threads are
- // created -- because OnExit is the only type of thread that can interrupt an uninterruptible
- // thread.
- BOOL g_AllowInterruption_prev = g_AllowInterruption; // Save current setting.
- g_AllowInterruption = FALSE; // Mark the thread just created above as permanently uninterruptible (i.e. until it finishes and is destroyed).
- sExitLabelIsRunning = true;
- if (mOnExitLabel->Execute() == FAIL)
- // If the subroutine encounters a failure condition such as a runtime error, exit immediately.
- // Otherwise, there will be no way to exit the script if the subroutine fails on each attempt.
- TerminateApp(aExitCode);
- sExitLabelIsRunning = false; // In case the user wanted the thread to end normally (see above).
- if (terminate_afterward)
- TerminateApp(aExitCode);
- // Otherwise:
- ResumeUnderlyingThread(ErrorLevel_saved);
- g_AllowInterruption = g_AllowInterruption_prev; // Restore original setting.
- return OK; // for caller convenience.
- }
- void Script::TerminateApp(int aExitCode)
- // Note that g_script's destructor takes care of most other cleanup work, such as destroying
- // tray icons, menus, and unowned windows such as ToolTip.
- {
- // We call DestroyWindow() because MainWindowProc() has left that up to us.
- // DestroyWindow() will cause MainWindowProc() to immediately receive and process the
- // WM_DESTROY msg, which should in turn result in any child windows being destroyed
- // and other cleanup being done:
- if (IsWindow(g_hWnd)) // Adds peace of mind in case WM_DESTROY was already received in some unusual way.
- {
- g_DestroyWindowCalled = true;
- DestroyWindow(g_hWnd);
- }
- Hotkey::AllDestructAndExit(aExitCode);
- }
- #ifdef AUTOHOTKEYSC
- LineNumberType Script::LoadFromFile()
- #else
- LineNumberType Script::LoadFromFile(bool aScriptWasNotspecified)
- #endif
- // Returns the number of non-comment lines that were loaded, or LOADING_FAILED on error.
- {
- mNoHotkeyLabels = true; // Indicate that there are no hotkey labels, since we're (re)loading the entire file.
- mIsReadyToExecute = mAutoExecSectionIsRunning = false;
- if (!mFileSpec || !*mFileSpec) return LOADING_FAILED;
- #ifndef AUTOHOTKEYSC // When not in stand-alone mode, read an external script file.
- DWORD attr = GetFileAttributes(mFileSpec);
- if (attr == MAXDWORD) // File does not exist or lacking the authorization to get its attributes.
- {
- char buf[MAX_PATH + 256];
- if (aScriptWasNotspecified) // v1.0.46.09: Give a more descriptive prompt to help users get started.
- {
- snprintf(buf, sizeof(buf),
- "To help you get started, would you like to create a sample script in the My Documents folder?\n"
- "\n"
- "Press YES to create and display the sample script.\n"
- "Press NO to exit.\n");
- }
- else // Mostly for backward compatibility, also prompt to create if an explicitly specified script doesn't exist.
- snprintf(buf, sizeof(buf), "The script file \"%s\" does not exist. Create it now?", mFileSpec);
- int response = MsgBox(buf, MB_YESNO);
- if (response != IDYES)
- return 0;
- FILE *fp2 = fopen(mFileSpec, "a");
- if (!fp2)
- {
- MsgBox("Could not create file, perhaps because the current directory is read-only"
- " or has insufficient permissions.");
- return LOADING_FAILED;
- }
- fputs(
- "; IMPORTANT INFO ABOUT GETTING STARTED: Lines that start with a\n"
- "; semicolon, such as this one, are comments. They are not executed.\n"
- "\n"
- "; This script has a special filename and path because it is automatically\n"
- "; launched when you run the program directly. Also, any text file whose\n"
- "; name ends in .ahk is associated with the program, which means that it\n"
- "; can be launched simply by double-clicking it. You can have as many .ahk\n"
- "; files as you want, located in any folder. You can also run more than\n"
- "; one ahk file simultaneously and each will get its own tray icon.\n"
- "\n"
- "; SAMPLE HOTKEYS: Below are two sample hotkeys. The first is Win+Z and it\n"
- "; launches a web site in the default browser. The second is Control+Alt+N\n"
- "; and it launches a new Notepad window (or activates an existing one). To\n"
- "; try out these hotkeys, run AutoHotkey again, which will load this file.\n"
- "\n"
- "#z::Run www.autohotkey.com\n"
- "\n"
- "^!n::\n"
- "IfWinExist Untitled - Notepad\n"
- "\tWinActivate\n"
- "else\n"
- "\tRun Notepad\n"
- "return\n"
- "\n"
- "\n"
- "; Note: From now on whenever you run AutoHotkey directly, this script\n"
- "; will be loaded. So feel free to customize it to suit your needs.\n"
- "\n"
- "; Please read the QUICK-START TUTORIAL near the top of the help file.\n"
- "; It explains how to perform common automation tasks such as sending\n"
- "; keystrokes and mouse clicks. It also explains more about hotkeys.\n"
- "\n"
- , fp2);
- fclose(fp2);
- // One or both of the below would probably fail -- at least on Win95 -- if mFileSpec ever
- // has spaces in it (since it's passed as the entire param string). So enclose the filename
- // in double quotes. I don't believe the directory needs to be in double quotes since it's
- // a separate field within the CreateProcess() and ShellExecute() structures:
- snprintf(buf, sizeof(buf), "\"%s\"", mFileSpec);
- if (!ActionExec("edit", buf, mFileDir, false))
- if (!ActionExec("Notepad.exe", buf, mFileDir, false))
- {
- MsgBox("Can't open script."); // Short msg since so rare.
- return LOADING_FAILED;
- }
- // future: have it wait for the process to close, then try to open the script again:
- return 0;
- }
- #endif
- // v1.0.42: Placeholder to use in place of a NULL label to simplify code in some places.
- // This must be created before loading the script because it's relied upon when creating
- // hotkeys to provide an alternative to having a NULL label. It will be given a non-NULL
- // mJumpToLine further down.
- if ( !(mPlaceholderLabel = new Label("")) ) // Not added to linked list since it's never looked up.
- return LOADING_FAILED;
- // Load the main script file. This will also load any files it includes with #Include.
- if ( LoadIncludedFile(mFileSpec, false, false) != OK
- || !AddLine(ACT_EXIT) // Fix for v1.0.47.04: Add an Exit because otherwise, a script that ends in an IF-statement will crash in PreparseBlocks() because PreparseBlocks() expects every IF-statements mNextLine to be non-NULL (helps loading performance too).
- || !PreparseBlocks(mFirstLine) ) // Must preparse the blocks before preparsing the If/Else's further below because If/Else may rely on blocks.
- return LOADING_FAILED; // Error was already displayed by the above calls.
- // ABOVE: In v1.0.47, the above may have auto-included additional files from the userlib/stdlib.
- // That's why the above is done prior to adding the EXIT lines and other things below.
- #ifndef AUTOHOTKEYSC
- if (mIncludeLibraryFunctionsThenExit)
- {
- fclose(mIncludeLibraryFunctionsThenExit);
- return 0; // Tell our caller to do a normal exit.
- }
- #endif
- // v1.0.35.11: Restore original working directory so that changes made to it by the above (via
- // "#Include C:\Scripts" or "#Include %A_ScriptDir%" or even stdlib/userlib) do not affect the
- // script's runtime working directory. This preserves the flexibility of having a startup-determined
- // working directory for the script's runtime (i.e. it seems best that the mere presence of
- // "#Include NewDir" should not entirely eliminate this flexibility).
- SetCurrentDirectory(g_WorkingDirOrig); // g_WorkingDirOrig previously set by WinMain().
- // Rather than do this, which seems kinda nasty if ever someday support same-line
- // else actions such as "else return", just add two EXITs to the end of every script.
- // That way, if the first EXIT added accidentally "corrects" an actionless ELSE
- // or IF, the second one will serve as the anchoring end-point (mRelatedLine) for that
- // IF or ELSE. In other words, since we never want mRelatedLine to be NULL, this should
- // make absolutely sure of that:
- //if (mLastLine->mActionType == ACT_ELSE ||
- // ACT_IS_IF(mLastLine->mActionType)
- // ...
- // Second ACT_EXIT: even if the last line of the script is already "exit", always add
- // another one in case the script ends in a label. That way, every label will have
- // a non-NULL target, which simplifies other aspects of script execution.
- // Making sure that all scripts end with an EXIT ensures that if the script
- // file ends with ELSEless IF or an ELSE, that IF's or ELSE's mRelatedLine
- // will be non-NULL, which further simplifies script execution.
- // Not done since it's number doesn't much matter: ++mCombinedLineNumber;
- ++mCombinedLineNumber; // So that the EXITs will both show up in ListLines as the line # after the last physical one in the script.
- if (!(AddLine(ACT_EXIT) && AddLine(ACT_EXIT))) // Second exit guaranties non-NULL mRelatedLine(s).
- return LOADING_FAILED;
- mPlaceholderLabel->mJumpToLine = mLastLine; // To follow the rule "all labels should have a non-NULL line before the script starts running".
- if (!PreparseIfElse(mFirstLine))
- return LOADING_FAILED; // Error was already displayed by the above calls.
- // Use FindOrAdd, not Add, because the user may already have added it simply by
- // referring to it in the script:
- if ( !(g_ErrorLevel = FindOrAddVar("ErrorLevel")) )
- return LOADING_FAILED; // Error. Above already displayed it for us.
- // Initialize the var state to zero right before running anything in the script:
- g_ErrorLevel->Assign(ERRORLEVEL_NONE);
- // Initialize the random number generator:
- // Note: On 32-bit hardware, the generator module uses only 2506 bytes of static
- // data, so it doesn't seem worthwhile to put it in a class (so that the mem is
- // only allocated on first use of the generator). For v1.0.24, _ftime() is not
- // used since it could be as large as 0.5 KB of non-compressed code. A simple call to
- // GetSystemTimeAsFileTime() seems just as good or better, since it produces
- // a FILETIME, which is "the number of 100-nanosecond intervals since January 1, 1601."
- // Use the low-order DWORD since the high-order one rarely changes. If my calculations are correct,
- // the low-order 32-bits traverses its full 32-bit range every 7.2 minutes, which seems to make
- // using it as a seed superior to GetTickCount for most purposes.
- RESEED_RANDOM_GENERATOR;
- return mLineCount; // The count of runnable lines that were loaded, which might be zero.
- }
- bool IsFunction(char *aBuf, bool *aPendingFunctionHasBrace = NULL)
- // Helper function for LoadIncludedFile().
- // Caller passes in an aBuf containing a candidate line such as "function(x, y)"
- // Caller has ensured that aBuf is rtrim'd.
- // Caller should pass NULL for aPendingFunctionHasBrace to indicate that function definitions (open-brace
- // on same line as function) are not allowed. When non-NULL *and* aBuf is a function call/def,
- // *aPendingFunctionHasBrace is set to true if a brace is present at the end, or false otherwise.
- // In addition, any open-brace is removed from aBuf in this mode.
- {
- char *action_end = StrChrAny(aBuf, EXPR_ALL_SYMBOLS EXPR_ILLEGAL_CHARS);
- // Can't be a function definition or call without an open-parenthesis as first char found by the above.
- // In addition, if action_end isn't NULL, that confirms that the string in aBuf prior to action_end contains
- // no spaces, tabs, colons, or equal-signs. As a result, it can't be:
- // 1) a hotstring, since they always start with at least one colon that would be caught immediately as
- // first-expr-char-is-not-open-parenthesis by the above.
- // 2) Any kind of math or assignment, such as var:=(x+y) or var+=(x+y).
- // The only things it could be other than a function call or function definition are:
- // Normal label that ends in single colon but contains an open-parenthesis prior to the colon, e.g. Label(x):
- // Single-line hotkey such as KeyName::MsgBox. But since '(' isn't valid inside KeyName, this isn't a concern.
- // In addition, note that it isn't necessary to check for colons that lie outside of quoted strings because
- // we're only interested in the first "word" of aBuf: If this is indeed a function call or definition, what
- // lies to the left of its first open-parenthesis can't contain any colons anyway because the above would
- // have caught it as first-expr-char-is-not-open-parenthesis. In other words, there's no way for a function's
- // opening parenthesis to occur after a legtimate/quoted colon or double-colon in its parameters.
- // v1.0.40.04: Added condition "action_end != aBuf" to allow a hotkey or remap or hotkey such as
- // such as "(::" to work even if it ends in a close-parenthesis such as "(::)" or "(::MsgBox )"
- if ( !(action_end && *action_end == '(' && action_end != aBuf
- && (action_end - aBuf != 2 || strnicmp(aBuf, "IF", 2))
- && (action_end - aBuf != 5 || strnicmp(aBuf, "WHILE", 5))) // v1.0.48.04: Recognize While() as loop rather than a function because many programmers are in the habit of writing while() and if().
- || action_end[1] == ':' ) // v1.0.44.07: This prevents "$(::fn_call()" from being seen as a function-call vs. hotkey-with-call. For simplicity and due to rarity, omit_leading_whitespace() isn't called; i.e. assumes that the colon immediate follows the '('.
- return false;
- char *aBuf_last_char = action_end + strlen(action_end) - 1; // Above has already ensured that action_end is "(...".
- if (aPendingFunctionHasBrace) // Caller specified that an optional open-brace may be present at the end of aBuf.
- {
- if (*aPendingFunctionHasBrace = (*aBuf_last_char == '{')) // Caller has ensured that aBuf is rtrim'd.
- {
- *aBuf_last_char = '\0'; // For the caller, remove it from further consideration.
- aBuf_last_char = aBuf + rtrim(aBuf, aBuf_last_char - aBuf) - 1; // Omit trailing whitespace too.
- }
- }
- return *aBuf_last_char == ')'; // This last check avoids detecting a label such as "Label(x):" as a function.
- // Also, it seems best never to allow if(...) to be a function call, even if it's blank inside such as if().
- // In addition, it seems best not to allow if(...) to ever be a function definition since such a function
- // could never be called as ACT_EXPRESSION since it would be seen as an IF-stmt instead.
- }
- ResultType Script::LoadIncludedFile(char *aFileSpec, bool aAllowDuplicateInclude, bool aIgnoreLoadFailure)
- // Returns OK or FAIL.
- // Below: Use double-colon as delimiter to set these apart from normal labels.
- // The main reason for this is that otherwise the user would have to worry
- // about a normal label being unintentionally valid as a hotkey, e.g.
- // "Shift:" might be a legitimate label that the user forgot is also
- // a valid hotkey:
- #define HOTKEY_FLAG "::"
- #define HOTKEY_FLAG_LENGTH 2
- {
- if (!aFileSpec || !*aFileSpec) return FAIL;
- #ifndef AUTOHOTKEYSC
- if (Line::sSourceFileCount >= Line::sMaxSourceFiles)
- {
- if (Line::sSourceFileCount >= ABSOLUTE_MAX_SOURCE_FILES)
- return ScriptError("Too many includes."); // Short msg since so rare.
- int new_max;
- if (Line::sMaxSourceFiles)
- {
- new_max = 2*Line::sMaxSourceFiles;
- if (new_max > ABSOLUTE_MAX_SOURCE_FILES)
- new_max = ABSOLUTE_MAX_SOURCE_FILES;
- }
- else
- new_max = 100;
- // For simplicity and due to rarity of every needing to, expand by reallocating the array.
- // Use a temp var. because realloc() returns NULL on failure but leaves original block allocated.
- char **realloc_temp = (char **)realloc(Line::sSourceFile, new_max*sizeof(char *)); // If passed NULL, realloc() will do a malloc().
- if (!realloc_temp)
- return ScriptError(ERR_OUTOFMEM); // Short msg since so rare.
- Line::sSourceFile = realloc_temp;
- Line::sMaxSourceFiles = new_max;
- }
- char full_path[MAX_PATH];
- #endif
- // Keep this var on the stack due to recursion, which allows newly created lines to be given the
- // correct file number even when some #include's have been encountered in the middle of the script:
- int source_file_index = Line::sSourceFileCount;
- if (!source_file_index)
- // Since this is the first source file, it must be the main script file. Just point it to the
- // location of the filespec already dynamically allocated:
- Line::sSourceFile[source_file_index] = mFileSpec;
- #ifndef AUTOHOTKEYSC // The "else" part below should never execute for compiled scripts since they never include anything (other than the main/combined script).
- else
- {
- // Get the full path in case aFileSpec has a relative path. This is done so that duplicates
- // can be reliably detected (we only want to avoid including a given file more than once):
- char *filename_marker;
- GetFullPathName(aFileSpec, sizeof(full_path), full_path, &filename_marker);
- // Check if this file was already included. If so, it's not an error because we want
- // to support automatic "include once" behavior. So just ignore repeats:
- if (!aAllowDuplicateInclude)
- for (int f = 0; f < source_file_index; ++f) // Here, source_file_index==Line::sSourceFileCount
- if (!lstrcmpi(Line::sSourceFile[f], full_path)) // Case insensitive like the file system (testing shows that "Ä" == "ä" in the NTFS, which is hopefully how lstrcmpi works regardless of locale).
- return OK;
- // The file is added to the list further below, after the file has been opened, in case the
- // opening fails and aIgnoreLoadFailure==true.
- }
- #endif
- UCHAR *script_buf = NULL; // Init for the case when the buffer isn't used (non-standalone mode).
- ULONG nDataSize = 0;
- // <buf> should be no larger than LINE_SIZE because some later functions rely upon that:
- char msg_text[MAX_PATH + 256], buf1[LINE_SIZE], buf2[LINE_SIZE], suffix[16], pending_function[LINE_SIZE] = "";
- char *buf = buf1, *next_buf = buf2; // Oscillate between bufs to improve performance (avoids memcpy from buf2 to buf1).
- size_t buf_length, next_buf_length, suffix_length;
- bool pending_function_has_brace;
- #ifndef AUTOHOTKEYSC
- // Future: might be best to put a stat() or GetFileAttributes() in here for better handling.
- FILE *fp = fopen(aFileSpec, "r");
- if (!fp)
- {
- if (aIgnoreLoadFailure)
- return OK;
- snprintf(msg_text, sizeof(msg_text), "%s file \"%s\" cannot be opened."
- , Line::sSourceFileCount > 0 ? "#Include" : "Script", aFileSpec);
- MsgBox(msg_text);
- return FAIL;
- }
- // v1.0.40.11: Otherwise, check if the first three bytes of the file are the UTF-8 BOM marker (and if
- // so omit them from further consideration). Apps such as Notepad, WordPad, and Word all insert this
- // marker if the file is saved in UTF-8 format. This omits such markers from both the main script and
- // any files it includes via #Include.
- // NOTE: To save code size, any UTF-8 BOM bytes at the beginning of a compiled script have already been
- // stripped out by the script compiler. Thus, there is no need to check for them in the AUTOHOTKEYSC
- // section further below.
- if (fgets(buf, 4, fp)) // Success (the fourth character is the terminator).
- {
- if (strcmp(buf, "ďťż")) // UTF-8 BOM marker is NOT present.
- rewind(fp); // Go back to the beginning so that the first three bytes aren't omitted during loading.
- // The code size of rewind() has been checked and it seems very tiny.
- }
- //else file read error or EOF, let a later section handle it.
- // This is done only after the file has been successfully opened in case aIgnoreLoadFailure==true:
- if (source_file_index > 0)
- Line::sSourceFile[source_file_index] = SimpleHeap::Malloc(full_path);
- //else the first file was already taken care of by another means.
- #else // Stand-alone mode (there are no include files in this mode since all of them were merged into the main script at the time of compiling).
- HS_EXEArc_Read oRead;
- // AutoIt3: Open the archive in this compiled exe.
- // Jon gave me some details about why a password isn't needed: "The code in those libararies will
- // only allow files to be extracted from the exe is is bound to (i.e the script that it was
- // compiled with). There are various checks and CRCs to make sure that it can't be used to read
- // the files from any other exe that is passed."
- if (oRead.Open(aFileSpec, "") != HS_EXEARC_E_OK)
- {
- MsgBox(ERR_EXE_CORRUPTED, 0, aFileSpec); // Usually caused by virus corruption.
- return FAIL;
- }
- // AutoIt3: Read the script (the func allocates the memory for the buffer :) )
- if (oRead.FileExtractToMem(">AUTOHOTKEY SCRIPT<", &script_buf, &nDataSize) == HS_EXEARC_E_OK)
- mCompiledHasCustomIcon = false;
- else if (oRead.FileExtractToMem(">AHK WITH ICON<", &script_buf, &nDataSize) == HS_EXEARC_E_OK)
- mCompiledHasCustomIcon = true;
- else
- {
- oRead.Close(); // Close the archive
- MsgBox("Could not extract script from EXE.", 0, aFileSpec);
- return FAIL;
- }
- UCHAR *script_buf_marker = script_buf; // "marker" will track where we are in the mem. file as we read from it.
- // Must cast to int to avoid loss of negative values:
- #define SCRIPT_BUF_SPACE_REMAINING ((int)(nDataSize - (script_buf_marker - script_buf)))
- int script_buf_space_remaining, max_chars_to_read; // script_buf_space_remaining must be an int to detect negatives.
- // AutoIt3: We have the data in RAW BINARY FORM, the script is a text file, so
- // this means that instead of a newline character, there may also be carridge
- // returns 0x0d 0x0a (\r\n)
- HS_EXEArc_Read *fp = &oRead; // To help consolidate the code below.
- #endif
- ++Line::sSourceFileCount;
- // File is now open, read lines from it.
- char *hotkey_flag, *cp, *cp1, *action_end, *hotstring_start, *hotstring_options;
- Hotkey *hk;
- LineNumberType pending_function_line_number, saved_line_number;
- HookActionType hook_action;
- bool is_label, suffix_has_tilde, in_comment_section, hotstring_options_all_valid;
- // For the remap mechanism, e.g. a::b
- int remap_stage;
- vk_type remap_source_vk, remap_dest_vk = 0; // Only dest is initialized to enforce the fact that it is the flag/signal to indicate whether remapping is in progress.
- char remap_source[32], remap_dest[32], remap_dest_modifiers[8]; // Must fit the longest key name (currently Browser_Favorites [17]), but buffer overflow is checked just in case.
- char *extra_event;
- bool remap_source_is_mouse, remap_dest_is_mouse, remap_keybd_to_mouse;
- // For the line continuation mechanism:
- bool do_ltrim, do_rtrim, literal_escapes, literal_derefs, literal_delimiters
- , has_continuation_section, is_continuation_line;
- #define CONTINUATION_SECTION_WITHOUT_COMMENTS 1 // MUST BE 1 because it's the default set by anything that's boolean-true.
- #define CONTINUATION_SECTION_WITH_COMMENTS 2 // Zero means "not in a continuation section".
- int in_continuation_section;
- char *next_option, *option_end, orig_char, one_char_string[2], two_char_string[3]; // Line continuation mechanism's option parsing.
- one_char_string[1] = '\0'; // Pre-terminate these to simplify code later below.
- two_char_string[2] = '\0'; //
- int continuation_line_count;
- #define MAX_FUNC_VAR_EXCEPTIONS 2000
- Var *func_exception_var[MAX_FUNC_VAR_EXCEPTIONS];
- // Init both for main file and any included files loaded by this function:
- mCurrFileIndex = source_file_index; // source_file_index is kept on the stack due to recursion (from #include).
- #ifdef AUTOHOTKEYSC
- // -1 (MAX_UINT in this case) to compensate for the fact that there is a comment containing
- // the version number added to the top of each compiled script:
- LineNumberType phys_line_number = -1;
- // For compiled scripts, limit the number of characters to read to however many remain in the memory
- // file or the size of the buffer, whichever is less.
- script_buf_space_remaining = SCRIPT_BUF_SPACE_REMAINING; // Resolve macro only once, for performance.
- max_chars_to_read = (LINE_SIZE - 1 < script_buf_space_remaining) ? LINE_SIZE - 1
- : script_buf_space_remaining;
- buf_length = GetLine(buf, max_chars_to_read, 0, script_buf_marker);
- #else
- LineNumberType phys_line_number = 0;
- buf_length = GetLine(buf, LINE_SIZE - 1, 0, fp);
- #endif
- if (in_comment_section = !strncmp(buf, "/*", 2))
- {
- // Fixed for v1.0.35.08. Must reset buffer to allow a script's first line to be "/*".
- *buf = '\0';
- buf_length = 0;
- }
- while (buf_length != -1) // Compare directly to -1 since length is unsigned.
- {
- // For each whole line (a line with continuation section is counted as only a single line
- // for the purpose of this outer loop).
- // Keep track of this line's *physical* line number within its file for A_LineNumber and
- // error reporting purposes. This must be done only in the outer loop so that it tracks
- // the topmost line of any set of lines merged due to continuation section/line(s)..
- mCombinedLineNumber = phys_line_number + 1;
- // This must be reset for each iteration because a prior iteration may have changed it, even
- // indirectly by calling something that changed it:
- mCurrLine = NULL; // To signify that we're in transition, trying to load a new one.
- // v1.0.44.13: An additional call to IsDirective() is now made up here so that #CommentFlag affects
- // the line beneath it the same way as other lines (#EscapeChar et. al. didn't have this bug).
- // It's best not to process ALL directives up here because then they would no longer support a
- // continuation section beneath them (and possibly other drawbacks because it was never thoroughly
- // tested).
- if (!strnicmp(buf, "#CommentFlag", 12)) // Have IsDirective() process this now (it will also process it again later, which is harmless).
- if (IsDirective(buf) == FAIL) // IsDirective() already displayed the error.
- return CloseAndReturnFail(fp, script_buf);
- // Read in the next line (if that next line is the start of a continuation secttion, append
- // it to the line currently being processed:
- for (has_continuation_section = false, in_continuation_section = 0;;)
- {
- // This increment relies on the fact that this loop always has at least one iteration:
- ++phys_line_number; // Tracks phys. line number in *this* file (independent of any recursion caused by #Include).
- #ifdef AUTOHOTKEYSC
- // See similar section above for comments about the following:
- script_buf_space_remaining = SCRIPT_BUF_SPACE_REMAINING; // Resolve macro only once, for performance.
- max_chars_to_read = (LINE_SIZE - 1 < script_buf_space_remaining) ? LINE_SIZE - 1
- : script_buf_space_remaining;
- next_buf_length = GetLine(next_buf, max_chars_to_read, in_continuation_section, script_buf_marker);
- #else
- next_buf_length = GetLine(next_buf, LINE_SIZE - 1, in_continuation_section, fp);
- #endif
- if (next_buf_length && next_buf_length != -1) // Prevents infinite loop when file ends with an unclosed "/*" section. Compare directly to -1 since length is unsigned.
- {
- if (in_comment_section) // Look for the uncomment-flag.
- {
- if (!strncmp(next_buf, "*/", 2))
- {
- in_comment_section = false;
- next_buf_length -= 2; // Adjust for removal of /* from the beginning of the string.
- memmove(next_buf, next_buf + 2, next_buf_length + 1); // +1 to include the string terminator.
- next_buf_length = ltrim(next_buf, next_buf_length); // Get rid of any whitespace that was between the comment-end and remaining text.
- if (!*next_buf) // The rest of the line is empty, so it was just a naked comment-end.
- continue;
- }
- else
- continue;
- }
- else if (!in_continuation_section && !strncmp(next_buf, "/*", 2))
- {
- in_comment_section = true;
- continue; // It's now commented out, so the rest of this line is ignored.
- }
- }
- if (in_comment_section) // Above has incremented and read the next line, which is everything needed while inside /* .. */
- {
- if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
- break; // By design, it's not an error. This allows "/*" to be used to comment out the bottommost portion of the script without needing a matching "*/".
- // Otherwise, continue reading lines so that they can be merged with the line above them
- // if they qualify as continuation lines.
- continue;
- }
- if (!in_continuation_section) // This is either the first iteration or the line after the end of a previous continuation section.
- {
- // v1.0.38.06: The following has been fixed to exclude "(:" and "(::". These should be
- // labels/hotkeys, not the start of a contination section. In addition, a line that starts
- // with '(' but that ends with ':' should be treated as a label because labels such as
- // "(label):" are far more common than something obscure like a continuation section whose
- // join character is colon, namely "(Join:".
- if ( !(in_continuation_section = (next_buf_length != -1 && *next_buf == '(' // Compare directly to -1 since length is unsigned.
- && next_buf[1] != ':' && next_buf[next_buf_length - 1] != ':')) ) // Relies on short-circuit boolean order.
- {
- if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
- break;
- if (!next_buf_length)
- // It is permitted to have blank lines and comment lines in between the line above
- // and any continuation section/line that might come after the end of the
- // comment/blank lines:
- continue;
- // SINCE ABOVE DIDN'T BREAK/CONTINUE, NEXT_BUF IS NON-BLANK.
- if (next_buf[next_buf_length - 1] == ':' && *next_buf != ',')
- // With the exception of lines starting with a comma, the last character of any
- // legitimate continuation line can't be a colon because expressions can't end
- // in a colon. The only exception is the ternary operator's colon, but that is
- // very rare because it requires the line after it also be a continuation line
- // or section, which is unusual to say the least -- so much so that it might be
- // too obscure to even document as a known limitation. Anyway, by excluding lines
- // that end with a colon from consideration ambiguity with normal labels
- // and non-single-line hotkeys and hotstrings is eliminated.
- break;
- is_continuation_line = false; // Set default.
- switch(toupper(*next_buf)) // Above has ensured *next_buf != '\0' (toupper might have problems with '\0').
- {
- case 'A': // "AND".
- // See comments in the default section further below.
- if (!strnicmp(next_buf, "and", 3) && IS_SPACE_OR_TAB_OR_NBSP(next_buf[3])) // Relies on short-circuit boolean order.
- {
- cp = omit_leading_whitespace(next_buf + 3);
- // v1.0.38.06: The following was fixed to use EXPR_CORE vs. EXPR_OPERAND_TERMINATORS
- // to properly detect a continuation line whose first char after AND/OR is "!~*&-+()":
- if (!strchr(EXPR_CORE, *cp))
- // This check recognizes the following examples as NON-continuation lines by checking
- // that AND/OR aren't followed immediately by something that's obviously an operator:
- // and := x, and = 2 (but not and += 2 since the an operand can have a unary plus/minus).
- // This is done for backward compatibility. Also, it's documented that
- // AND/OR/NOT aren't supported as variable names inside expressions.
- is_continuation_line = true; // Override the default set earlier.
- }
- break;
- case 'O': // "OR".
- // See comments in the default section further below.
- if (toupper(next_buf[1]) == 'R' && IS_SPACE_OR_TAB_OR_NBSP(next_buf[2])) // Relies on short-circuit boolean order.
- {
- cp = omit_leading_whitespace(next_buf + 2);
- // v1.0.38.06: The following was fixed to use EXPR_CORE vs. EXPR_OPERAND_TERMINATORS
- // to properly detect a continuation line whose first char after AND/OR is "!~*&-+()":
- if (!strchr(EXPR_CORE, *cp)) // See comment in the "AND" case above.
- is_continuation_line = true; // Override the default set earlier.
- }
- break;
- default:
- // Desired line continuation operators:
- // Pretty much everything, namely:
- // +, -, *, /, //, **, <<, >>, &, |, ^, <, >, <=, >=, =, ==, <>, !=, :=, +=, -=, /=, *=, ?, :
- // And also the following remaining unaries (i.e. those that aren't also binaries): !, ~
- // The first line below checks for ::, ++, and --. Those can't be continuation lines because:
- // "::" isn't a valid operator (this also helps performance if there are many hotstrings).
- // ++ and -- are ambiguous with an isolated line containing ++Var or --Var (and besides,
- // wanting to use ++ to continue an expression seems extremely rare, though if there's ever
- // demand for it, might be able to look at what lies to the right of the operator's operand
- // -- though that would produce inconsisent continuation behavior since ++Var itself still
- // could never be a continuation line due to ambiguity).
- //
- // The logic here isn't smart enough to differentiate between a leading ! or - that's
- // meant as a continuation character and one that isn't. Even if it were, it would
- // still be ambiguous in some cases because the author's intent isn't known; for example,
- // the leading minus sign on the second line below is ambiguous, so will probably remain
- // a continuation character in both v1 and v2:
- // x := y
- // -z ? a:=1 : func()
- if ((*next_buf == ':' || *next_buf == '+' || *next_buf == '-') && next_buf[1] == *next_buf // See above.
- || (*next_buf == '.' || *next_buf == '?') && !IS_SPACE_OR_TAB_OR_NBSP(next_buf[1]) // The "." and "?" operators require a space or tab after them to be legitimate. For ".", this is done in case period is ever a legal character in var names, such as struct support. For "?", it's done for backward compatibility since variable names can contain question marks (though "?" by itself is not considered a variable in v1.0.46).
- && next_buf[1] != '=' // But allow ".=" (and "?=" too for code simplicity), since ".=" is the concat-assign operator.
- || !strchr(CONTINUATION_LINE_SYMBOLS, *next_buf)) // Line doesn't start with a continuation char.
- break; // Leave is_continuation_line set to its default of false.
- // Some of the above checks must be done before the next ones.
- if ( !(hotkey_flag = strstr(next_buf, HOTKEY_FLAG)) ) // Without any "::", it can't be a hotkey or hotstring.
- {
- is_continuation_line = true; // Override the default set earlier.
- break;
- }
- if (*next_buf == ':') // First char is ':', so it's more likely a hotstring than a hotkey.
- {
- // Remember that hotstrings can contain what *appear* to be quoted literal strings,
- // so detecting whether a "::" is in a quoted/literal string in this case would
- // be more complicated. That's one reason this other method is used.
- for (hotstring_options_all_valid = true, cp = next_buf + 1; *cp && *cp != ':'; ++cp)
- if (!IS_HOTSTRING_OPTION(*cp)) // Not a perfect test, but eliminates most of what little remaining ambiguity exists between ':' as a continuation character vs. ':' as the start of a hotstring. It especially eliminates the ":=" operator.
- {
- hotstring_options_all_valid = false;
- break;
- }
- if (hotstring_options_all_valid && *cp == ':') // It's almost certainly a hotstring.
- break; // So don't treat it as a continuation line.
- //else it's not a hotstring but it might still be a hotkey such as ": & x::".
- // So continue checking below.
- }
- // Since above didn't "break", this line isn't a hotstring but it is probably a hotkey
- // because above already discovered that it contains "::" somewhere. So try to find out
- // if there's anything that disqualifies this from being a hotkey, such as some
- // expression line that contains a quoted/literal "::" (or a line starting with
- // a comma that contains an unquoted-but-literal "::" such as for FileAppend).
- if (*next_buf == ',')
- {
- cp = omit_leading_whitespace(next_buf + 1);
- // The above has set cp to the position of the non-whitespace item to the right of
- // this comma. Normal (single-colon) labels can't contain commas, so only hotkey
- // labels are sources of ambiguity. In addition, normal labels and hotstrings have
- // already been checked for, higher above.
- if ( strncmp(cp, HOTKEY_FLAG, HOTKEY_FLAG_LENGTH) // It's not a hotkey such as ",::action".
- && strncmp(cp - 1, COMPOSITE_DELIMITER, COMPOSITE_DELIMITER_LENGTH) ) // ...and it's not a hotkey such as ", & y::action".
- is_continuation_line = true; // Override the default set earlier.
- }
- else // First symbol in line isn't a comma but some other operator symbol.
- {
- // Check if the "::" found earlier appears to be inside a quoted/literal string.
- // This check is NOT done for a line beginning with a comma since such lines
- // can contain an unquoted-but-literal "::". In addition, this check is done this
- // way to detect hotkeys such as the following:
- // +keyname:: (and other hotkey modifier symbols such as ! and ^)
- // +keyname1 & keyname2::
- // +^:: (i.e. a modifier symbol followed by something that is a hotkey modifer and/or a hotkey suffix and/or an expression operator).
- // <:: and &:: (i.e. hotkeys that are also expression-continuation symbols)
- // By contrast, expressions that qualify as continuation lines can look like:
- // . "xxx::yyy"
- // + x . "xxx::yyy"
- // In addition, hotkeys like the following should continue to be supported regardless
- // of how things are done here:
- // ^"::
- // . & "::
- // Finally, keep in mind that an expression-continuation line can start with two
- // consecutive unary operators like !! or !*. It can also start with a double-symbol
- // operator such as <=, <>, !=, &&, ||, //, **.
- for (cp = next_buf; cp < hotkey_flag && *cp != '"'; ++cp);
- if (cp == hotkey_flag) // No '"' found to left of "::", so this "::" appears to be a real hotkey flag rather than part of a literal string.
- break; // Treat this line as a normal line vs. continuation line.
- for (cp = hotkey_flag + HOTKEY_FLAG_LENGTH; *cp && *cp != '"'; ++cp);
- if (*cp)
- {
- // Closing quote was found so "::" is probably inside a literal string of an
- // expression (further checking seems unnecessary given the fairly extreme
- // rarity of using '"' as a key in a hotkey definition).
- is_continuation_line = true; // Override the default set earlier.
- }
- //else no closing '"' found, so this "::" probably belongs to something like +":: or
- // . & "::. Treat this line as a normal line vs. continuation line.
- }
- } // switch(toupper(*next_buf))
- if (is_continuation_line)
- {
- if (buf_length + next_buf_length >= LINE_SIZE - 1) // -1 to account for the extra space added below.
- {
- ScriptError(ERR_CONTINUATION_SECTION_TOO_LONG, next_buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- if (*next_buf != ',') // Insert space before expression operators so that built/combined expression works correctly (some operators like 'and', 'or', '.', and '?' currently require spaces on either side) and also for readability of ListLines.
- buf[buf_length++] = ' ';
- memcpy(buf + buf_length, next_buf, next_buf_length + 1); // Append this line to prev. and include the zero terminator.
- buf_length += next_buf_length;
- continue; // Check for yet more continuation lines after this one.
- }
- // Since above didn't continue, there is no continuation line or section. In addition,
- // since this line isn't blank, no further searching is needed.
- break;
- } // if (!in_continuation_section)
- // OTHERWISE in_continuation_section != 0, so the above has found the first line of a new
- // continuation section.
- // "has_continuation_section" indicates whether the line we're about to construct is partially
- // composed of continuation lines beneath it. It's separate from continuation_line_count
- // in case there is another continuation section immediately after/adjacent to the first one,
- // but the second one doesn't have any lines in it:
- has_continuation_section = true;
- continuation_line_count = 0; // Reset for this new section.
- // Otherwise, parse options. First set the defaults, which can be individually overridden
- // by any options actually present. RTrim defaults to ON for two reasons:
- // 1) Whitespace often winds up at the end of a lines in a text editor by accident. In addition,
- // whitespace at the end of any consolidated/merged line will be rtrim'd anyway, since that's
- // how command parsing works.
- // 2) Copy & paste from the forum and perhaps other web sites leaves a space at the end of each
- // line. Although this behavior is probably site/browser-specific, it's a consideration.
- do_ltrim = g_ContinuationLTrim; // Start off at global default.
- do_rtrim = true; // Seems best to rtrim even if this line is a hotstring, since it is very rare that trailing spaces and tabs would ever be desirable.
- // For hotstrings (which could be detected via *buf==':'), it seems best not to default the
- // escape character (`) to be literal because the ability to have `t `r and `n inside the
- // hotstring continuation section seems more useful/common than the ability to use the
- // accent character by itself literally (which seems quite rare in most languages).
- literal_escapes = false;
- literal_derefs = false;
- literal_delimiters = true; // This is the default even for hotstrings because although using (*buf != ':') would improve loading performance, it's not a 100% reliable way to detect hotstrings.
- // The default is linefeed because:
- // 1) It's the best choice for hotstrings, for which the line continuation mechanism is well suited.
- // 2) It's good for FileAppend.
- // 3) Minor: Saves memory in large sections by being only one character instead of two.
- suffix[0] = '\n';
- suffix[1] = '\0';
- suffix_length = 1;
- for (next_option = omit_leading_whitespace(next_buf + 1); *next_option; next_option = omit_leading_whitespace(option_end))
- {
- // Find the end of this option item:
- if ( !(option_end = StrChrAny(next_option, " \t")) ) // Space or tab.
- option_end = next_option + strlen(next_option); // Set to position of zero terminator instead.
- // Temporarily terminate to help eliminate ambiguity for words contained inside other words,
- // such as hypothetical "Checked" inside of "CheckedGray":
- orig_char = *option_end;
- *option_end = '\0';
- if (!strnicmp(next_option, "Join", 4))
- {
- next_option += 4;
- strlcpy(suffix, next_option, sizeof(suffix)); // The word "Join" by itself will product an empty string, as documented.
- // Passing true for the last parameter supports `s as the special escape character,
- // which allows space to be used by itself and also at the beginning or end of a string
- // containing other chars.
- ConvertEscapeSequences(suffix, g_EscapeChar, true);
- suffix_length = strlen(suffix);
- }
- else if (!strnicmp(next_option, "LTrim", 5))
- do_ltrim = (next_option[5] != '0'); // i.e. Only an explicit zero will turn it off.
- else if (!strnicmp(next_option, "RTrim", 5))
- do_rtrim = (next_option[5] != '0');
- else
- {
- // Fix for v1.0.36.01: Missing "else" above, because otherwise, the option Join`r`n
- // would be processed above but also be processed again below, this time seeing the
- // accent and thinking it's the signal to treat accents literally for the entire
- // continuation section rather than as escape characters.
- // Within this terminated option substring, allow the characters to be adjacent to
- // improve usability:
- for (; *next_option; ++next_option)
- {
- switch (*next_option)
- {
- case '`': // Although not using g_EscapeChar (reduces code size/complexity), #EscapeChar is still supported by continuation sections; it's just that enabling the option uses '`' rather than the custom escape-char (one reason is that that custom escape-char might be ambiguous with future/past options if it's somehing weird like an alphabetic character).
- literal_escapes = true;
- break;
- case '%': // Same comment as above.
- literal_derefs = true;
- break;
- case ',': // Same comment as above.
- literal_delimiters = false;
- break;
- case 'C': // v1.0.45.03: For simplicity, anything that begins with "C" is enough to
- case 'c': // identify it as the option to allow comments in the section.
- in_continuation_section = CONTINUATION_SECTION_WITH_COMMENTS; // Override the default, which is boolean true (i.e. 1).
- break;
- }
- }
- }
- // If the item was not handled by the above, ignore it because it is unknown.
- *option_end = orig_char; // Undo the temporary termination.
- } // for() each item in option list
- continue; // Now that the open-parenthesis of this continuation section has been processed, proceed to the next line.
- } // if (!in_continuation_section)
- // Since above didn't "continue", we're in the continuation section and thus next_buf contains
- // either a line to be appended onto buf or the closing parenthesis of this continuation section.
- if (next_buf_length == -1) // Compare directly to -1 since length is unsigned.
- {
- ScriptError(ERR_MISSING_CLOSE_PAREN, buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- if (next_buf_length == -2) // v1.0.45.03: Special flag that means "this is a commented-out line to be
- continue; // entirely omitted from the continuation section." Compare directly to -2 since length is unsigned.
- if (*next_buf == ')')
- {
- in_continuation_section = 0; // Facilitates back-to-back continuation sections and proper incrementing of phys_line_number.
- next_buf_length = rtrim(next_buf); // Done because GetLine() wouldn't have done it due to have told it we're in a continuation section.
- // Anything that lies to the right of the close-parenthesis gets appended verbatim, with
- // no trimming (for flexibility) and no options-driven translation:
- cp = next_buf + 1; // Use temp var cp to avoid altering next_buf (for maintainability).
- --next_buf_length; // This is now the length of cp, not next_buf.
- }
- else
- {
- cp = next_buf;
- // The following are done in this block only because anything that comes after the closing
- // parenthesis (i.e. the block above) is exempt from translations and custom trimming.
- // This means that commas are always delimiters and percent signs are always deref symbols
- // in the previous block.
- if (do_rtrim)
- next_buf_length = rtrim(next_buf, next_buf_length);
- if (do_ltrim)
- next_buf_length = ltrim(next_buf, next_buf_length);
- // Escape each comma and percent sign in the body of the continuation section so that
- // the later parsing stages will see them as literals. Although, it's not always
- // necessary to do this (e.g. commas in the last parameter of a command don't need to
- // be escaped, nor do percent signs in hotstrings' auto-replace text), the settings
- // are applied unconditionally because:
- // 1) Determining when its safe to omit the translation would add a lot of code size and complexity.
- // 2) The translation doesn't affect the functionality of the script since escaped literals
- // are always de-escaped at a later stage, at least for everything that's likely to matter
- // or that's reasonable to put into a continuation section (e.g. a hotstring's replacement text).
- // UPDATE for v1.0.44.11: #EscapeChar, #DerefChar, #Delimiter are now supported by continuation
- // sections because there were some requests for that in forum.
- int replacement_count = 0;
- if (literal_escapes) // literal_escapes must be done FIRST because otherwise it would also replace any accents added for literal_delimiters or literal_derefs.
- {
- one_char_string[0] = g_EscapeChar; // These strings were terminated earlier, so no need to
- two_char_string[0] = g_EscapeChar; // do it here. In addition, these strings must be set by
- two_char_string[1] = g_EscapeChar; // each iteration because the #EscapeChar (and similar directives) can occur multiple times, anywhere in the script.
- replacement_count += StrReplace(next_buf, one_char_string, two_char_string, SCS_SENSITIVE, UINT_MAX, LINE_SIZE);
- }
- if (literal_derefs)
- {
- one_char_string[0] = g_DerefChar;
- two_char_string[0] = g_EscapeChar;
- two_char_string[1] = g_DerefChar;
- replacement_count += StrReplace(next_buf, one_char_string, two_char_string, SCS_SENSITIVE, UINT_MAX, LINE_SIZE);
- }
- if (literal_delimiters)
- {
- one_char_string[0] = g_delimiter;
- two_char_string[0] = g_EscapeChar;
- two_char_string[1] = g_delimiter;
- replacement_count += StrReplace(next_buf, one_char_string, two_char_string, SCS_SENSITIVE, UINT_MAX, LINE_SIZE);
- }
- if (replacement_count) // Update the length if any actual replacements were done.
- next_buf_length = strlen(next_buf);
- } // Handling of a normal line within a continuation section.
- // Must check the combined length only after anything that might have expanded the string above.
- if (buf_length + next_buf_length + suffix_length >= LINE_SIZE)
- {
- ScriptError(ERR_CONTINUATION_SECTION_TOO_LONG, cp);
- return CloseAndReturnFail(fp, script_buf);
- }
- ++continuation_line_count;
- // Append this continuation line onto the primary line.
- // The suffix for the previous line gets written immediately prior writing this next line,
- // which allows the suffix to be omitted for the final line. But if this is the first line,
- // No suffix is written because there is no previous line in the continuation section.
- // In addition, cp!=next_buf, this is the special line whose text occurs to the right of the
- // continuation section's closing parenthesis. In this case too, the previous line doesn't
- // get a suffix.
- if (continuation_line_count > 1 && suffix_length && cp == next_buf)
- {
- memcpy(buf + buf_length, suffix, suffix_length + 1); // Append and include the zero terminator.
- buf_length += suffix_length; // Must be done only after the old value of buf_length was used above.
- }
- if (next_buf_length)
- {
- memcpy(buf + buf_length, cp, next_buf_length + 1); // Append this line to prev. and include the zero terminator.
- buf_length += next_buf_length; // Must be done only after the old value of buf_length was used above.
- }
- } // for() each sub-line (continued line) that composes this line.
- // buf_length can't be -1 (though next_buf_length can) because outer loop's condition prevents it:
- if (!buf_length) // Done only after the line number increments above so that the physical line number is properly tracked.
- goto continue_main_loop; // In lieu of "continue", for performance.
- // Since neither of the above executed, or they did but didn't "continue",
- // buf now contains a non-commented line, either by itself or built from
- // any continuation sections/lines that might have been present. Also note that
- // by design, phys_line_number will be greater than mCombinedLineNumber whenever
- // a continuation section/lines were used to build this combined line.
- // If there's a previous line waiting to be processed, its fate can now be determined based on the
- // nature of *this* line:
- if (*pending_function)
- {
- // Somewhat messy to decrement then increment later, but it's probably easier than the
- // alternatives due to the use of "continue" in some places above. NOTE: phys_line_number
- // would not need to be decremented+incremented even if the below resulted in a recursive
- // call to us (though it doesn't currently) because line_number's only purpose is to
- // remember where this layer left off when the recursion collapses back to us.
- // Fix for v1.0.31.05: It's not enough just to decrement mCombinedLineNumber because there
- // might be some blank lines or commented-out lines between this function call/definition
- // and the line that follows it, each of which will have previously incremented mCombinedLineNumber.
- saved_line_number = mCombinedLineNumber;
- mCombinedLineNumber = pending_function_line_number; // Done so that any syntax errors that occur during the calls below will report the correct line number.
- // Open brace means this is a function definition. NOTE: buf was already ltrimmed by GetLine().
- // Could use *g_act[ACT_BLOCK_BEGIN].Name instead of '{', but it seems too elaborate to be worth it.
- if (*buf == '{' || pending_function_has_brace) // v1.0.41: Support one-true-brace, e.g. fn(...) {
- {
- // Note that two consecutive function definitions aren't possible:
- // fn1()
- // fn2()
- // {
- // ...
- // }
- // In the above, the first would automatically be deemed a function call by means of
- // the check higher above (by virtue of the fact that the line after it isn't an open-brace).
- if (g->CurrentFunc)
- {
- // Though it might be allowed in the future -- perhaps to have nested functions have
- // access to their parent functions' local variables, or perhaps just to improve
- // script readability and maintainability -- it's currently not allowed because of
- // the practice of maintaining the func_exception_var list on our stack:
- ScriptError("Functions cannot contain functions.", pending_function);
- return CloseAndReturnFail(fp, script_buf);
- }
- if (!DefineFunc(pending_function, func_exception_var))
- return CloseAndReturnFail(fp, script_buf);
- if (pending_function_has_brace) // v1.0.41: Support one-true-brace for function def, e.g. fn() {
- if (!AddLine(ACT_BLOCK_BEGIN))
- return CloseAndReturnFail(fp, script_buf);
- }
- else // It's a function call on a line by itself, such as fn(x). It can't be if(..) because another section checked that.
- {
- if (!ParseAndAddLine(pending_function, ACT_EXPRESSION))
- return CloseAndReturnFail(fp, script_buf);
- mCurrLine = NULL; // Prevents showing misleading vicinity lines if the line after a function call is a syntax error.
- }
- mCombinedLineNumber = saved_line_number;
- *pending_function = '\0'; // Reset now that it's been fully handled, as an indicator for subsequent iterations.
- // Now fall through to the below so that *this* line (the one after it) will be processed.
- // Note that this line might be a pre-processor directive, label, etc. that won't actually
- // become a runtime line per se.
- } // if (*pending_function)
- // By doing the following section prior to checking for hotkey and hotstring labels, double colons do
- // not need to be escaped inside naked function calls and function definitions such as the following:
- // fn("::") ; Function call.
- // fn(Str="::") ; Function definition with default value for its parameter.
- if (IsFunction(buf, &pending_function_has_brace)) // If true, it's either a function definition or a function call (to be distinguished later).
- {
- // Defer this line until the next line comes in, which helps determine whether this line is
- // a function call vs. definition:
- strcpy(pending_function, buf);
- pending_function_line_number = mCombinedLineNumber;
- goto continue_main_loop; // In lieu of "continue", for performance.
- }
- // The following "examine_line" label skips the following parts above:
- // 1) IsFunction() because that's only for a function call or definition alone on a line
- // e.g. not "if fn()" or x := fn(). Those who goto this label don't need that processing.
- // 2) The "if (*pending_function)" block: Doesn't seem applicable for the callers of this label.
- // 3) The inner loop that handles continuation sections: Not needed by the callers of this label.
- // 4) Things like the following should be skipped because callers of this label don't want the
- // physical line number changed (which would throw off the count of lines that lie beneath a remap):
- // mCombinedLineNumber = phys_line_number + 1;
- // ++phys_line_number;
- // 5) "mCurrLine = NULL": Probably not necessary since it's only for error reporting. Worst thing
- // that could happen is that syntax errors would be thrown off, which testing shows isn't the case.
- examine_line:
- // "::" alone isn't a hotstring, it's a label whose name is colon.
- // Below relies on the fact that no valid hotkey can start with a colon, since
- // ": & somekey" is not valid (since colon is a shifted key) and colon itself
- // should instead be defined as "+;::". It also relies on short-circuit boolean:
- hotstring_start = NULL;
- hotstring_options = NULL; // Set default as "no options were specified for this hotstring".
- hotkey_flag = NULL;
- if (buf[0] == ':' && buf[1])
- {
- if (buf[1] != ':')
- {
- hotstring_options = buf + 1; // Point it to the hotstring's option letters.
- // The following relies on the fact that options should never contain a literal colon.
- // ALSO, the following doesn't use IS_HOTSTRING_OPTION() for backward compatibility,
- // performance, and because it seems seldom if ever necessary at this late a stage.
- if ( !(hotstring_start = strchr(hotstring_options, ':')) )
- hotstring_start = NULL; // Indicate that this isn't a hotstring after all.
- else
- ++hotstring_start; // Points to the hotstring itself.
- }
- else // Double-colon, so it's a hotstring if there's more after this (but this means no options are present).
- if (buf[2])
- hotstring_start = buf + 2; // And leave hotstring_options at its default of NULL to indicate no options.
- //else it's just a naked "::", which is considered to be an ordinary label whose name is colon.
- }
- if (hotstring_start)
- {
- // Find the hotstring's final double-colon by considering escape sequences from left to right.
- // This is necessary for to handles cases such as the following:
- // ::abc```::::Replacement String
- // The above hotstring translates literally into "abc`::".
- char *escaped_double_colon = NULL;
- for (cp = hotstring_start; ; ++cp) // Increment to skip over the symbol just found by the inner for().
- {
- for (; *cp && *cp != g_EscapeChar && *cp != ':'; ++cp); // Find the next escape char or colon.
- if (!*cp) // end of string.
- break;
- cp1 = cp + 1;
- if (*cp == ':')
- {
- if (*cp1 == ':') // Found a non-escaped double-colon, so this is the right one.
- {
- hotkey_flag = cp++; // Increment to have loop skip over both colons.
- // and the continue with the loop so that escape sequences in the replacement
- // text (if there is replacement text) are also translated.
- }
- // else just a single colon, or the second colon of an escaped pair (`::), so continue.
- continue;
- }
- switch (*cp1)
- {
- // Only lowercase is recognized for these:
- case 'a': *cp1 = '\a'; break; // alert (bell) character
- case 'b': *cp1 = '\b'; break; // backspace
- case 'f': *cp1 = '\f'; break; // formfeed
- case 'n': *cp1 = '\n'; break; // newline
- case 'r': *cp1 = '\r'; break; // carriage return
- case 't': *cp1 = '\t'; break; // horizontal tab
- case 'v': *cp1 = '\v'; break; // vertical tab
- // Otherwise, if it's not one of the above, the escape-char is considered to
- // mark the next character as literal, regardless of what it is. Examples:
- // `` -> `
- // `:: -> :: (effectively)
- // `; -> ;
- // `c -> c (i.e. unknown escape sequences resolve to the char after the `)
- }
- // Below has a final +1 to include the terminator:
- MoveMemory(cp, cp1, strlen(cp1) + 1);
- // Since single colons normally do not need to be escaped, this increments one extra
- // for double-colons to skip over the entire pair so that its second colon
- // is not seen as part of the hotstring's final double-colon. Example:
- // ::ahc```::::Replacement String
- if (*cp == ':' && *cp1 == ':')
- ++cp;
- } // for()
- if (!hotkey_flag)
- hotstring_start = NULL; // Indicate that this isn't a hotstring after all.
- }
- if (!hotstring_start) // Not a hotstring (hotstring_start is checked *again* in case above block changed it; otherwise hotkeys like ": & x" aren't recognized).
- {
- // Note that there may be an action following the HOTKEY_FLAG (on the same line).
- if (hotkey_flag = strstr(buf, HOTKEY_FLAG)) // Find the first one from the left, in case there's more than 1.
- {
- if (hotkey_flag == buf && hotkey_flag[2] == ':') // v1.0.46: Support ":::" to mean "colon is a hotkey".
- ++hotkey_flag;
- // v1.0.40: It appears to be a hotkey, but validate it as such before committing to processing
- // it as a hotkey. If it fails validation as a hotkey, treat it as a command that just happens
- // to contain a double-colon somewhere. This avoids the need to escape double colons in scripts.
- // Note: Hotstrings can't suffer from this type of ambiguity because a leading colon or pair of
- // colons makes them easier to detect.
- cp = omit_trailing_whitespace(buf, hotkey_flag); // For maintainability.
- orig_char = *cp;
- *cp = '\0'; // Temporarily terminate.
- if (!Hotkey::TextInterpret(omit_leading_whitespace(buf), NULL, false)) // Passing NULL calls it in validate-only mode.
- hotkey_flag = NULL; // It's not a valid hotkey, so indicate that it's a command (i.e. one that contains a literal double-colon, which avoids the need to escape the double-colon).
- *cp = orig_char; // Undo the temp. termination above.
- }
- }
- // Treat a naked "::" as a normal label whose label name is colon:
- if (is_label = (hotkey_flag && hotkey_flag > buf)) // It's a hotkey/hotstring label.
- {
- if (g->CurrentFunc)
- {
- // Even if it weren't for the reasons below, the first hotkey/hotstring label in a script
- // will end the auto-execute section with a "return". Therefore, if this restriction here
- // is ever removed, be sure that that extra return doesn't get put inside the function.
- //
- // The reason for not allowing hotkeys and hotstrings inside a function's body is that
- // when the subroutine is launched, the hotstring/hotkey would be using the function's
- // local variables. But that is not appropriate and it's likely to cause problems even
- // if it were. It doesn't seem useful in any case. By contrast, normal labels can
- // safely exist inside a function body and since the body is a block, other validation
- // ensures that a Gosub or Goto can't jump to it from outside the function.
- ScriptError("Hotkeys/hotstrings are not allowed inside functions.", buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- if (mLastLine && mLastLine->mActionType == ACT_IFWINACTIVE)
- {
- mCurrLine = mLastLine; // To show vicinity lines.
- ScriptError("IfWin should be #IfWin.", buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- *hotkey_flag = '\0'; // Terminate so that buf is now the label itself.
- hotkey_flag += HOTKEY_FLAG_LENGTH; // Now hotkey_flag is the hotkey's action, if any.
- if (!hotstring_start)
- {
- ltrim(hotkey_flag); // Has already been rtrimmed by GetLine().
- rtrim(buf); // Trim the new substring inside of buf (due to temp termination). It has already been ltrimmed.
- cp = hotkey_flag; // Set default, conditionally overridden below (v1.0.44.07).
- // v1.0.40: Check if this is a remap rather than hotkey:
- if ( *hotkey_flag // This hotkey's action is on the same line as its label.
- && (remap_source_vk = TextToVK(cp1 = Hotkey::TextToModifiers(buf, NULL)))
- && (remap_dest_vk = hotkey_flag[1] ? TextToVK(cp = Hotkey::TextToModifiers(hotkey_flag, NULL)) : 0xFF) ) // And the action appears to be a remap destination rather than a command.
- // For above:
- // Fix for v1.0.44.07: Set remap_dest_vk to 0xFF if hotkey_flag's length is only 1 because:
- // 1) It allows a destination key that doesn't exist in the keyboard layout (such as 6::đ in
- // English).
- // 2) It improves performance a little by not calling TextToVK except when the destination key
- // might be a mouse button or some longer key name whose actual/correct VK value is relied
- // upon by other places below.
- // Fix for v1.0.40.01: Since remap_dest_vk is also used as the flag to indicate whether
- // this line qualifies as a remap, must do it last in the statement above. Otherwise,
- // the statement might short-circuit and leave remap_dest_vk as non-zero even though
- // the line shouldn't be a remap. For example, I think a hotkey such as "x & y::return"
- // would trigger such a bug.
- {
- // These will be ignored in other stages if it turns out not to be a remap later below:
- remap_source_is_mouse = IsMouseVK(remap_source_vk);
- remap_dest_is_mouse = IsMouseVK(remap_dest_vk);
- remap_keybd_to_mouse = !remap_source_is_mouse && remap_dest_is_mouse;
- snprintf(remap_source, sizeof(remap_source), "%s%s"
- , strlen(cp1) == 1 && IsCharUpper(*cp1) ? "+" : "" // Allow A::b to be different than a::b.
- , buf); // Include any modifiers too, e.g. ^b::c.
- strlcpy(remap_dest, cp, sizeof(remap_dest)); // But exclude modifiers here; they're wanted separately.
- strlcpy(remap_dest_modifiers, hotkey_flag, sizeof(remap_dest_modifiers));
- if (cp - hotkey_flag < sizeof(remap_dest_modifiers)) // Avoid reading beyond the end.
- remap_dest_modifiers[cp - hotkey_flag] = '\0'; // Terminate at the proper end of the modifier string.
- remap_stage = 0; // Init for use in the next stage.
- // In the unlikely event that the dest key has the same name as a command, disqualify it
- // from being a remap (as documented). v1.0.40.05: If the destination key has any modifiers,
- // it is unambiguously a key name rather than a command, so the switch() isn't necessary.
- if (*remap_dest_modifiers)
- goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
- switch (remap_dest_vk)
- {
- case VK_CONTROL: // Checked in case it was specified as "Control" rather than "Ctrl".
- case VK_SLEEP:
- if (StrChrAny(hotkey_flag, " \t,")) // Not using g_delimiter (reduces code size/complexity).
- break; // Any space, tab, or enter means this is a command rather than a remap destination.
- goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
- // "Return" and "Pause" as destination keys are always considered commands instead.
- // This is documented and is done to preserve backward compatibility.
- case VK_RETURN:
- // v1.0.40.05: Although "Return" can't be a destination, "Enter" can be. Must compare
- // to "Return" not "Enter" so that things like "vk0d" (the VK of "Enter") can also be a
- // destination key:
- if (!stricmp(remap_dest, "Return"))
- break;
- goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
- case VK_PAUSE: // Used for both "Pause" and "Break"
- break;
- default: // All other VKs are valid destinations and thus the remap is valid.
- goto continue_main_loop; // It will see that remap_dest_vk is non-zero and act accordingly.
- }
- // Since above didn't goto, indicate that this is not a remap after all:
- remap_dest_vk = 0;
- }
- }
- // else don't trim hotstrings since literal spaces in both substrings are significant.
- // If this is the first hotkey label encountered, Add a return before
- // adding the label, so that the auto-exectute section is terminated.
- // Only do this if the label is a hotkey because, for example,
- // the user may want to fully execute a normal script that contains
- // no hotkeys but does contain normal labels to which the execution
- // should fall through, if specified, rather than returning.
- // But this might result in weirdness? Example:
- //testlabel:
- // Sleep, 1
- // return
- // ^a::
- // return
- // It would put the hard return in between, which is wrong. But in the case above,
- // the first sub shouldn't have a return unless there's a part up top that ends in Exit.
- // So if Exit is encountered before the first hotkey, don't add the return?
- // Even though wrong, the return is harmless because it's never executed? Except when
- // falling through from above into a hotkey (which probably isn't very valid anyway)?
- // Update: Below must check if there are any true hotkey labels, not just regular labels.
- // Otherwise, a normal (non-hotkey) label in the autoexecute section would count and
- // thus the RETURN would never be added here, even though it should be:
-
- // Notes about the below macro:
- // Fix for v1.0.34: Don't point labels to this particular RETURN so that labels
- // can point to the very first hotkey or hotstring in a script. For example:
- // Goto Test
- // Test:
- // ^!z::ToolTip Without the fix`, this is never displayed by "Goto Test".
- // UCHAR_MAX signals it not to point any pending labels to this RETURN.
- // mCurrLine = NULL -> signifies that we're in transition, trying to load a new one.
- #define CHECK_mNoHotkeyLabels \
- if (mNoHotkeyLabels)\
- {\
- mNoHotkeyLabels = false;\
- if (!AddLine(ACT_RETURN, NULL, UCHAR_MAX))\
- return CloseAndReturnFail(fp, script_buf);\
- mCurrLine = NULL;\
- }
- CHECK_mNoHotkeyLabels
- // For hotstrings, the below makes the label include leading colon(s) and the full option
- // string (if any) so that the uniqueness of labels is preserved. For example, we want
- // the following two hotstring labels to be unique rather than considered duplicates:
- // ::abc::
- // :c:abc::
- if (!AddLabel(buf, true)) // Always add a label before adding the first line of its section.
- return CloseAndReturnFail(fp, script_buf);
- hook_action = 0; // Set default.
- if (*hotkey_flag) // This hotkey's action is on the same line as its label.
- {
- if (!hotstring_start)
- // Don't add the alt-tabs as a line, since it has no meaning as a script command.
- // But do put in the Return regardless, in case this label is ever jumped to
- // via Goto/Gosub:
- if ( !(hook_action = Hotkey::ConvertAltTab(hotkey_flag, false)) )
- if (!ParseAndAddLine(hotkey_flag, IsFunction(hotkey_flag) ? ACT_EXPRESSION : ACT_INVALID)) // It can't be a function definition vs. call since it's a single-line hotkey.
- return CloseAndReturnFail(fp, script_buf);
- // Also add a Return that's implicit for a single-line hotkey. This is also
- // done for auto-replace hotstrings in case gosub/goto is ever used to jump
- // to their labels:
- if (!AddLine(ACT_RETURN))
- return CloseAndReturnFail(fp, script_buf);
- }
- if (hotstring_start)
- {
- if (!*hotstring_start)
- {
- // The following error message won't indicate the correct line number because
- // the hotstring (as a label) does not actually exist as a line. But it seems
- // best to report it this way in case the hotstring is inside a #Include file,
- // so that the correct file name and approximate line number are shown:
- ScriptError("This hotstring is missing its abbreviation.", buf); // Display buf vs. hotkey_flag in case the line is simply "::::".
- return CloseAndReturnFail(fp, script_buf);
- }
- // In the case of hotstrings, hotstring_start is the beginning of the hotstring itself,
- // i.e. the character after the second colon. hotstring_options is NULL if no options,
- // otherwise it's the first character in the options list (option string is not terminated,
- // but instead ends in a colon). hotkey_flag is blank if it's not an auto-replace
- // hotstring, otherwise it contains the auto-replace text.
- // v1.0.42: Unlike hotkeys, duplicate hotstrings are not detected. This is because
- // hotstrings are less commonly used and also because it requires more code to find
- // hotstring duplicates (and performs a lot worse if a script has thousands of
- // hotstrings) because of all the hotstring options.
- if (!Hotstring::AddHotstring(mLastLabel, hotstring_options ? hotstring_options : ""
- , hotstring_start, hotkey_flag, has_continuation_section))
- return CloseAndReturnFail(fp, script_buf);
- }
- else // It's a hotkey vs. hotstring.
- {
- if (hk = Hotkey::FindHotkeyByTrueNature(buf, suffix_has_tilde)) // Parent hotkey found. Add a child/variant hotkey for it.
- {
- if (hook_action) // suffix_has_tilde has always been ignored for these types (alt-tab hotkeys).
- {
- // Hotkey::Dynamic() contains logic and comments similar to this, so maintain them together.
- // An attempt to add an alt-tab variant to an existing hotkey. This might have
- // merit if the intention is to make it alt-tab now but to later disable that alt-tab
- // aspect via the Hotkey cmd to let the context-sensitive variants shine through
- // (take effect).
- hk->mHookAction = hook_action;
- }
- else
- {
- // Detect duplicate hotkey variants to help spot bugs in scripts.
- if (hk->FindVariant()) // See if there's already a variant matching the current criteria (suffix_has_tilde does not make variants distinct form each other because it would require firing two hotkey IDs in response to pressing one hotkey, which currently isn't in the design).
- {
- mCurrLine = NULL; // Prevents showing unhelpful vicinity lines.
- ScriptError("Duplicate hotkey.", buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- if (!hk->AddVariant(mLastLabel, suffix_has_tilde))
- {
- ScriptError(ERR_OUTOFMEM, buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- }
- }
- else // No parent hotkey yet, so create it.
- if ( !(hk = Hotkey::AddHotkey(mLastLabel, hook_action, NULL, suffix_has_tilde, false)) )
- return CloseAndReturnFail(fp, script_buf); // It already displayed the error.
- }
- goto continue_main_loop; // In lieu of "continue", for performance.
- } // if (is_label = ...)
- // Otherwise, not a hotkey or hotstring. Check if it's a generic, non-hotkey label:
- if (buf[buf_length - 1] == ':') // Labels must end in a colon (buf was previously rtrimmed).
- {
- if (buf_length == 1) // v1.0.41.01: Properly handle the fact that this line consists of only a colon.
- {
- ScriptError(ERR_UNRECOGNIZED_ACTION, buf);
- return CloseAndReturnFail(fp, script_buf);
- }
- // Labels (except hotkeys) must contain no whitespace, delimiters, or escape-chars.
- // This is to avoid problems where a legitimate action-line ends in a colon,
- // such as "WinActivate SomeTitle" and "#Include c:".
- // We allow hotkeys to violate this since they may contain commas, and since a normal
- // script line (i.e. just a plain command) is unlikely to ever end in a double-colon:
- for (cp = buf, is_label = true; *cp; ++cp)
- if (IS_SPACE_OR_TAB(*cp) || *cp == g_delimiter || *cp == g_EscapeChar)
- {
- is_label = false;
- break;
- }
- if (is_label) // It's a generic, non-hotkey/non-hotstring label.
- {
- // v1.0.44.04: Fixed this check by moving it after the above loop.
- // Above has ensured buf_length>1, so it's safe to check for double-colon:
- // v1.0.44.03: Don't allow anything that ends in "::" (other than a line consisting only
- // of "::") to be a normal label. Assume it's a command instead (if it actually isn't, a
- // later stage will report it as "invalid hotkey"). This change avoids the situation in
- // which a hotkey like ^!ä:: is seen as invalid because the current keyboard layout doesn't
- // have a "ä" key. Without this change, if such a hotkey appears at the top of the script,
- // its subroutine would execute immediately as a normal label, which would be especially
- // bad if the hotkey were something like the "Shutdown" command.
- if (buf[buf_length - 2] == ':' && buf_length > 2) // i.e. allow "::" as a normal label, but consider anything else with double-colon to be a failed-hotkey label that terminates the auto-exec section.
- {
- CHECK_mNoHotkeyLabels // Terminate the auto-execute section since this is a failed hotkey vs. a mere normal label.
- snprintf(msg_text, sizeof(msg_text), "Note: The hotkey %s will not be active because it does not exist in the current keyboard layout.", buf);
- MsgBox(msg_text);
- }
- buf[--buf_length] = '\0'; // Remove the trailing colon.
- rtrim(buf, buf_length); // Has already been ltrimmed.
- if (!AddLabel(buf, false))
- return CloseAndReturnFail(fp, script_buf);
- goto continue_main_loop; // In lieu of "continue", for performance.
- }
- }
- // Since above didn't "goto", it's not a label.
- if (*buf == '#')
- {
- saved_line_number = mCombinedLineNumber; // Backup in case IsDirective() processes an include file, which would change mCombinedLineNumber's value.
- switch(IsDirective(buf)) // Note that it may alter the contents of buf, at least in the case of #IfWin.
- {
- case CONDITION_TRUE:
- // Since the directive may have been a #include which called us recursively,
- // restore the class's values for these two, which are maintained separately
- // like this to avoid having to specify them in various calls, especially the
- // hundreds of calls to ScriptError() and LineError():
- mCurrFileIndex = source_file_index;
- mCombinedLineNumber = saved_line_number;
- goto continue_main_loop; // In lieu of "continue", for performance.
- case FAIL: // IsDirective() already displayed the error.
- return CloseAndReturnFail(fp, script_buf);
- //case CONDITION_FALSE: Do nothing; let processing below handle it.
- }
- }
- // Otherwise, treat it as a normal script line.
- // v1.0.41: Support the "} else {" style in one-true-brace (OTB). As a side-effect,
- // any command, not just an else, is probably supported to the right of '}', not just "else".
- // This is undocumented because it would make for less readable scripts, and doesn't seem
- // to have much value.
- if (*buf == '}')
- {
- if (!AddLine(ACT_BLOCK_END))
- return CloseAndReturnFail(fp, script_buf);
- // The following allows the next stage to see "else" or "else {" if it's present:
- if ( !*(buf = omit_leading_whitespace(buf + 1)) )
- goto continue_main_loop; // It's just a naked "}", so no more processing needed for this line.
- buf_length = strlen(buf); // Update for possible use below.
- }
- // First do a little special handling to support actions on the same line as their
- // ELSE, e.g.:
- // else if x = 1
- // This is done here rather than in ParseAndAddLine() because it's fairly
- // complicated to do there (already tried it) mostly due to the fact that
- // literal_map has to be properly passed in a recursive call to itself, as well
- // as properly detecting special commands that don't have keywords such as
- // IF comparisons, ACT_ASSIGN, +=, -=, etc.
- // v1.0.41: '{' was added to the line below to support no spaces inside "}else{".
- if (!(action_end = StrChrAny(buf, "\t ,{"))) // Position of first tab/space/comma/open-brace. For simplicitly, a non-standard g_delimiter is not supported.
- action_end = buf + buf_length; // It's done this way so that ELSE can be fully handled here; i.e. that ELSE does not have to be in the list of commands recognizable by ParseAndAddLine().
- // The following method ensures that words or variables that start with "Else", e.g. ElseAction, are not
- // incorrectly detected as an Else command:
- if (strlicmp(buf, "Else", (UINT)(action_end - buf))) // It's not an ELSE. ("Else" is used vs. g_act[ACT_ELSE].Name for performance).
- {
- // It's not an ELSE. Also, at this stage it can't be ACT_EXPRESSION (such as an isolated function call)
- // because it would have been already handled higher above.
- // v1.0.41.01: Check if there is a command/action on the same line as the '{'. This is apparently
- // a style that some people use, and it also supports "{}" as a shorthand way of writing an empty block.
- if (*buf == '{')
- {
- if (!AddLine(ACT_BLOCK_BEGIN))
- return CloseAndReturnFail(fp, script_buf);
- if ( *(action_end = omit_leading_whitespace(buf + 1)) ) // There is an action to the right of the '{'.
- {
- mCurrLine = NULL; // To signify that we're in transition, trying to load a new one.
- if (!ParseAndAddLine(action_end, IsFunction(action_end) ? ACT_EXPRESSION : ACT_INVALID)) // If it's a function, it must be a call vs. a definition because a function can't be defined on the same line as an open-brace.
- return CloseAndReturnFail(fp, script_buf);
- }
- // Otherwise, there was either no same-line action or the same-line action was successfully added,
- // so do nothing.
- }
- else
- if (!ParseAndAddLine(buf))
- return CloseAndReturnFail(fp, script_buf);
- }
- else // This line is an ELSE, possibly with another command immediately after it (on the same line).
- {
- // Add the ELSE directly rather than calling ParseAndAddLine() because that function
- // would resolve escape sequences throughout the entire length of <buf>, which we
- // don't want because we wouldn't have access to the corresponding literal-map to
- // figure out the proper use of escaped characters:
- if (!AddLine(ACT_ELSE))
- return CloseAndReturnFail(fp, script_buf);
- mCurrLine = NULL; // To signify that we're in transition, trying to load a new one.
- action_end = omit_leading_whitespace(action_end); // Now action_end is the word after the ELSE.
- if (*action_end == g_delimiter) // Allow "else, action"
- action_end = omit_leading_whitespace(action_end + 1);
- if (*action_end && !ParseAndAddLine(action_end, IsFunction(action_end) ? ACT_EXPRESSION : ACT_INVALID)) // If it's a function, it must be a call vs. a definition because a function can't be defined on the same line as an Else.
- return CloseAndReturnFail(fp, script_buf);
- // Otherwise, there was either no same-line action or the same-line action was successfully added,
- // so do nothing.
- }
- continue_main_loop: // This method is used in lieu of "continue" for performance and code size reduction.
- if (remap_dest_vk)
- {
- // For remapping, decided to use a "macro expansion" approach because I think it's considerably
- // smaller in code size and complexity than other approaches would be. I originally wanted to
- // do it with the hook by changing the incoming event prior to passing it back out again (for
- // example, a::b would transform an incoming 'a' keystroke into 'b' directly without having
- // to suppress the original keystroke and simulate a new one). Unfortunately, the low-level
- // hooks apparently do not allow this. Here is the test that confirmed it:
- // if (event.vkCode == 'A')
- // {
- // event.vkCode = 'B';
- // event.scanCode = 0x30; // Or use vk_to_sc(event.vkCode).
- // return CallNextHookEx(g_KeybdHook, aCode, wParam, lParam);
- // }
- switch (++remap_stage)
- {
- case 1: // Stage 1: Add key-down hotkey label, e.g. *LButton::
- buf_length = sprintf(buf, "*%s::", remap_source); // Should be no risk of buffer overflow due to prior validation.
- goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
- case 2: // Stage 2.
- // Copied into a writable buffer for maintainability: AddLine() might rely on this.
- // Also, it seems unnecessary to set press-duration to -1 even though the auto-exec section might
- // have set it to something higher than -1 because:
- // 1) Press-duration doesn't apply to normal remappings since they use down-only and up-only events.
- // 2) Although it does apply to remappings such as a::B and a::^b (due to press-duration being
- // applied after a change to modifier state), those remappings are fairly rare and supporting
- // a non-negative-one press-duration (almost always 0) probably adds a degree of flexibility
- // that may be desirable to keep.
- // 3) SendInput may become the predominant SendMode, so press-duration won't often be in effect anyway.
- // 4) It has been documented that remappings use the auto-execute section's press-duration.
- strcpy(buf, "-1"); // Does NOT need to be "-1, -1" for SetKeyDelay (see above).
- // The primary reason for adding Key/MouseDelay -1 is to minimize the chance that a one of
- // these hotkey threads will get buried under some other thread such as a timer, which
- // would disrupt the remapping if #MaxThreadsPerHotkey is at its default of 1.
- AddLine(remap_dest_is_mouse ? ACT_SETMOUSEDELAY : ACT_SETKEYDELAY, &buf, 1, NULL); // PressDuration doesn't need to be specified because it doesn't affect down-only and up-only events.
- if (remap_keybd_to_mouse)
- {
- // Since source is keybd and dest is mouse, prevent keyboard auto-repeat from auto-repeating
- // the mouse button (since that would be undesirable 90% of the time). This is done
- // by inserting a single extra IF-statement above the Send that produces the down-event:
- buf_length = sprintf(buf, "if not GetKeyState(\"%s\")", remap_dest); // Should be no risk of buffer overflow due to prior validation.
- remap_stage = 9; // Have it hit special stage 9+1 next time for code reduction purposes.
- goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
- }
- // Otherwise, remap_keybd_to_mouse==false, so fall through to next case.
- case 10:
- extra_event = ""; // Set default.
- switch (remap_dest_vk)
- {
- case VK_LMENU:
- case VK_RMENU:
- case VK_MENU:
- switch (remap_source_vk)
- {
- case VK_LCONTROL:
- case VK_CONTROL:
- extra_event = "{LCtrl up}"; // Somewhat surprisingly, this is enough to make "Ctrl::Alt" properly remap both right and left control.
- break;
- case VK_RCONTROL:
- extra_event = "{RCtrl up}";
- break;
- // Below is commented out because its only purpose was to allow a shift key remapped to alt
- // to be able to alt-tab. But that wouldn't work correctly due to the need for the following
- // hotkey, which does more harm than good by impacting the normal Alt key's ability to alt-tab
- // (if the normal Alt key isn't remapped): *Tab::Send {Blind}{Tab}
- //case VK_LSHIFT:
- //case VK_SHIFT:
- // extra_event = "{LShift up}";
- // break;
- //case VK_RSHIFT:
- // extra_event = "{RShift up}";
- // break;
- }
- break;
- }
- mCurrLine = NULL; // v1.0.40.04: Prevents showing misleading vicinity lines for a syntax-error such as %::%
- sprintf(buf, "{Blind}%s%s{%s DownTemp}", extra_event, remap_dest_modifiers, remap_dest); // v1.0.44.05: DownTemp vs. Down. See Send's DownTemp handler for details.
- if (!AddLine(ACT_SEND, &buf, 1, NULL)) // v1.0.40.04: Check for failure due to bad remaps such as %::%.
- return CloseAndReturnFail(fp, script_buf);
- AddLine(ACT_RETURN);
- // Add key-up hotkey label, e.g. *LButton up::
- buf_length = sprintf(buf, "*%s up::", remap_source); // Should be no risk of buffer overflow due to prior validation.
- remap_stage = 2; // Adjust to hit stage 3 next time (in case this is stage 10).
- goto examine_line; // Have the main loop process the contents of "buf" as though it came in from the script.
- case 3: // Stage 3.
- strcpy(buf, "-1");
- AddLine(remap_dest_is_mouse ? ACT_SETMOUSEDELAY : ACT_SETKEYDELAY, &buf, 1, NULL);
- sprintf(buf, "{Blind}{%s Up}", remap_dest); // Unlike the down-event above, remap_dest_modifiers is not included for the up-event; e.g. ^{b up} is inappropriate.
- AddLine(ACT_SEND, &buf, 1, NULL);
- AddLine(ACT_RETURN);
- remap_dest_vk = 0; // Reset to signal that the remapping expansion is now complete.
- break; // Fall through to the next section so that script loading can resume at the next line.
- }
- } // if (remap_dest_vk)
- // Since above didn't "continue", resume loading script line by line:
- buf = next_buf;
- buf_length = next_buf_length;
- next_buf = (buf == buf1) ? buf2 : buf1;
- // The line above alternates buffers (toggles next_buf to be the unused buffer), which helps
- // performance because it avoids memcpy from buf2 to buf1.
- } // for each whole/constructed line.
- if (*pending_function) // Since this is the last non-comment line, the pending function must be a function call, not a function definition.
- {
- // Somewhat messy to decrement then increment later, but it's probably easier than the
- // alternatives due to the use of "continue" in some places above.
- saved_line_number = mCombinedLineNumber;
- mCombinedLineNumber = pending_function_line_number; // Done so that any syntax errors that occur during the calls below will report the correct line number.
- if (!ParseAndAddLine(pending_function, ACT_EXPRESSION)) // Must be function call vs. definition since otherwise the above would have detected the opening brace beneath it and already cleared pending_function.
- return CloseAndReturnFail(fp, script_buf);
- mCombinedLineNumber = saved_line_number;
- }
- #ifdef AUTOHOTKEYSC
- free(script_buf); // AutoIt3: Close the archive and free the file in memory.
- oRead.Close(); //
- #else
- fclose(fp);
- #endif
- return OK;
- }
- // Small inline to make LoadIncludedFile() code cleaner.
- #ifdef AUTOHOTKEYSC
- inline ResultType Script::CloseAndReturnFailFunc(HS_EXEArc_Read *fp, UCHAR *aBuf)
- {
- free(aBuf);
- fp->Close();
- return FAIL;
- }
- #else
- inline ResultType Script::CloseAndReturnFailFunc(FILE *fp)
- {
- fclose(fp);
- return FAIL;
- }
- #endif
- #ifdef AUTOHOTKEYSC
- size_t Script::GetLine(char *aBuf, int aMaxCharsToRead, int aInContinuationSection, UCHAR *&aMemFile) // last param = reference to pointer
- #else
- size_t Script::GetLine(char *aBuf, int aMaxCharsToRead, int aInContinuationSection, FILE *fp)
- #endif
- {
- size_t aBuf_length = 0;
- #ifdef AUTOHOTKEYSC
- if (!aBuf || !aMemFile) return -1;
- if (aMaxCharsToRead < 1) return -1; // We're signaling to caller that the end of the memory file has been reached.
- // Otherwise, continue reading characters from the memory file until either a newline is
- // reached or aMaxCharsToRead have been read:
- // Track "i" separately from aBuf_length because we want to read beyond the bounds of the memory file.
- int i;
- for (i = 0; i < aMaxCharsToRead; ++i)
- {
- if (aMemFile[i] == '\n')
- {
- // The end of this line has been reached. Don't copy this char into the target buffer.
- // In addition, if the previous char was '\r', remove it from the target buffer:
- if (aBuf_length > 0 && aBuf[aBuf_length - 1] == '\r')
- aBuf[--aBuf_length] = '\0';
- ++i; // i.e. so that aMemFile will be adjusted to omit this newline char.
- break;
- }
- else
- aBuf[aBuf_length++] = aMemFile[i];
- }
- // We either read aMaxCharsToRead or reached the end of the line (as indicated by the newline char).
- // In the former case, aMemFile might now be changed to be a position outside the bounds of the
- // memory area, which the caller will reflect back to us during the next call as a 0 value for
- // aMaxCharsToRead, which we then signal to the caller (above) as the end of the file):
- aMemFile += i; // Update this value for use by the caller.
- // Terminate the buffer (the caller has already ensured that there's room for the terminator
- // via its value of aMaxCharsToRead):
- aBuf[aBuf_length] = '\0';
- #else
- if (!aBuf || !fp) return -1;
- if (aMaxCharsToRead < 1) return 0;
- if (feof(fp)) return -1; // Previous call to this function probably already read the last line.
- if (fgets(aBuf, aMaxCharsToRead, fp) == NULL) // end-of-file or error
- {
- *aBuf = '\0'; // Reset since on error, contents added by fgets() are indeterminate.
- return -1;
- }
- aBuf_length = strlen(aBuf);
- if (!aBuf_length)
- return 0;
- if (aBuf[aBuf_length-1] == '\n')
- aBuf[--aBuf_length] = '\0';
- if (aBuf[aBuf_length-1] == '\r') // In case there are any, e.g. a Macintosh or Unix file?
- aBuf[--aBuf_length] = '\0';
- #endif
- if (aInContinuationSection)
- {
- char *cp = omit_leading_whitespace(aBuf);
- if (aInContinuationSection == CONTINUATION_SECTION_WITHOUT_COMMENTS) // By default, continuation sections don't allow comments (lines beginning with a semicolon are treated as literal text).
- {
- // Caller relies on us to detect the end of the continuation section so that trimming
- // will be done on the final line of the section and so that a comment can immediately
- // follow the closing parenthesis (on the same line). Example:
- // (
- // Text
- // ) ; Same line comment.
- if (*cp != ')') // This isn't the last line of the continuation section, so leave the line untrimmed (caller will apply the ltrim setting on its own).
- return aBuf_length; // Earlier sections are responsible for keeping aBufLength up-to-date with any changes to aBuf.
- //else this line starts with ')', so continue on to later section that checks for a same-line comment on its right side.
- }
- else // aInContinuationSection == CONTINUATION_SECTION_WITH_COMMENTS (i.e. comments are allowed in this continuation section).
- {
- // Fix for v1.0.46.09+: The "com" option shouldn't put "ltrim" into effect.
- if (!strncmp(cp, g_CommentFlag, g_CommentFlagLength)) // Case sensitive.
- {
- *aBuf = '\0'; // Since this line is a comment, have the caller ignore it.
- return -2; // Callers tolerate -2 only when in a continuation section. -2 indicates, "don't include this line at all, not even as a blank line to which the JOIN string (default "\n") will apply.
- }
- if (*cp == ')') // This isn't the last line of the continuation section, so leave the line untrimmed (caller will apply the ltrim setting on its own).
- {
- ltrim(aBuf); // Ltrim this line unconditionally so that caller will see that it starts with ')' without having to do extra steps.
- aBuf_length = strlen(aBuf); // ltrim() doesn't always return an accurate length, so do it this way.
- }
- }
- }
- // Since above didn't return, either:
- // 1) We're not in a continuation section at all, so apply ltrim() to support semicolons after tabs or
- // other whitespace. Seems best to rtrim also.
- // 2) CONTINUATION_SECTION_WITHOUT_COMMENTS but this line is the final line of the section. Apply
- // trim() and other logic further below because caller might rely on it.
- // 3) CONTINUATION_SECTION_WITH_COMMENTS (i.e. comments allowed), but this line isn't a comment (though
- // it may start with ')' and thus be the final line of this section). In either case, need to check
- // for same-line comments further below.
- if (aInContinuationSection != CONTINUATION_SECTION_WITH_COMMENTS) // Case #1 & #2 above.
- {
- aBuf_length = trim(aBuf);
- if (!strncmp(aBuf, g_CommentFlag, g_CommentFlagLength)) // Case sensitive.
- {
- // Due to other checks, aInContinuationSection==false whenever the above condition is true.
- *aBuf = '\0';
- return 0;
- }
- }
- //else CONTINUATION_SECTION_WITH_COMMENTS (case #3 above), which due to other checking also means that
- // this line isn't a comment (though it might have a comment on its right side, which is checked below).
- // CONTINUATION_SECTION_WITHOUT_COMMENTS would already have returned higher above if this line isn't
- // the last line of the continuation section.
- if (g_AllowSameLineComments)
- {
- // Handle comment-flags that appear to the right of a valid line. But don't
- // allow these types of comments if the script is considers to be the AutoIt2
- // style, to improve compatibility with old scripts that may use non-escaped
- // comment-flags as literal characters rather than comments:
- char *cp, *prevp;
- for (cp = strstr(aBuf, g_CommentFlag); cp; cp = strstr(cp + g_CommentFlagLength, g_CommentFlag))
- {
- // If no whitespace to its left, it's not a valid comment.
- // We insist on this so that a semi-colon (for example) immediately after
- // a word (as semi-colons are often used) will not be considered a comment.
- prevp = cp - 1;
- if (prevp < aBuf) // should never happen because we already checked above.
- {
- *aBuf = '\0';
- return 0;
- }
- if (IS_SPACE_OR_TAB_OR_NBSP(*prevp)) // consider it to be a valid comment flag
- {
- *prevp = '\0';
- aBuf_length = rtrim_with_nbsp(aBuf, prevp - aBuf); // Since it's our responsibility to return a fully trimmed string.
- break; // Once the first valid comment-flag is found, nothing after it can matter.
- }
- else // No whitespace to the left.
- if (*prevp == g_EscapeChar) // Remove the escape char.
- {
- // The following isn't exactly correct because it prevents an include filename from ever
- // containing the literal string "`;". This is because attempts to escape the accent via
- // "``;" are not supported. This is documented here as a known limitation because fixing
- // it would probably break existing scripts that rely on the fact that accents do not need
- // to be escaped inside #Include. Also, the likelihood of "`;" appearing literally in a
- // legitimate #Include file seems vanishingly small.
- memmove(prevp, prevp + 1, strlen(prevp + 1) + 1); // +1 for the terminator.
- --aBuf_length;
- // Then continue looking for others.
- }
- // else there wasn't any whitespace to its left, so keep looking in case there's
- // another further on in the line.
- } // for()
- } // if (g_AllowSameLineComments)
- return aBuf_length; // The above is responsible for keeping aBufLength up-to-date with any changes to aBuf.
- }
- inline ResultType Script::IsDirective(char *aBuf)
- // aBuf must be a modifiable string since this function modifies it in the case of "#Include %A_ScriptDir%"
- // changes it. It must also be large enough to accept the replacement of %A_ScriptDir% with a larger string.
- // Returns CONDITION_TRUE, CONDITION_FALSE, or FAIL.
- // Note: Don't assume that every line in the script that starts with '#' is a directive
- // because hotkeys can legitimately start with that as well. i.e., the following line should
- // not be unconditionally ignored, just because it starts with '#', since it is a valid hotkey:
- // #y::run, notepad
- {
- char end_flags[] = {' ', '\t', g_delimiter, '\0'}; // '\0' must be last.
- char *directive_end, *parameter_raw;
- if ( !(directive_end = StrChrAny(aBuf, end_flags)) )
- {
- directive_end = aBuf + strlen(aBuf); // Point it to the zero terminator.
- parameter_raw = NULL;
- }
- else
- if (!*(parameter_raw = omit_leading_whitespace(directive_end)))
- parameter_raw = NULL;
- // The raw parameter retains any leading comma for those directives that need that (none currently).
- // But the following omits that comma:
- char *parameter;
- if (!parameter_raw)
- parameter = NULL;
- else // Since parameter_raw is non-NULL, it's also non-blank and non-whitespace due to the above checking.
- if (*parameter_raw != g_delimiter)
- parameter = parameter_raw;
- else // It's a delimiter, so "parameter" will be whatever non-whitespace character follows it, if any.
- if (!*(parameter = omit_leading_whitespace(parameter_raw + 1)))
- parameter = NULL;
- //else leave it set to the value returned by omit_leading_whitespace().
- int value; // Helps detect values that are too large, since some of the target globals are UCHAR.
- // Use strnicmp() so that a match is found as long as aBuf starts with the string in question.
- // e.g. so that "#SingleInstance, on" will still work too, but
- // "#a::run, something, "#SingleInstance" (i.e. a hotkey) will not be falsely detected
- // due to using a more lenient function such as strcasestr().
- // UPDATE: Using strlicmp() now so that overlapping names, such as #MaxThreads and #MaxThreadsPerHotkey,
- // won't get mixed up:
- #define IS_DIRECTIVE_MATCH(directive) (!strlicmp(aBuf, directive, directive_name_length))
- UINT directive_name_length = (UINT)(directive_end - aBuf); // To avoid calculating it every time in the macro above.
- bool is_include_again = false; // Set default in case of short-circuit boolean.
- if (IS_DIRECTIVE_MATCH("#Include") || (is_include_again = IS_DIRECTIVE_MATCH("#IncludeAgain")))
- {
- // Standalone EXEs ignore this directive since the included files were already merged in
- // with the main file when the script was compiled. These should have been removed
- // or commented out by Ahk2Exe, but just in case, it's safest to ignore them:
- #ifdef AUTOHOTKEYSC
- return CONDITION_TRUE;
- #else
- // If the below decision is ever changed, be sure to update ahk2exe with the same change:
- // "parameter" is checked rather than parameter_raw for backward compatibility with earlier versions,
- // in which a leading comma is not considered part of the filename. Although this behavior is incorrect
- // because it prevents files whose names start with a comma from being included without the first
- // delim-comma being there too, it is kept because filesnames that start with a comma seem
- // exceedingly rare. As a workaround, the script can do #Include ,,FilenameWithLeadingComma.ahk
- if (!parameter)
- return ScriptError(ERR_PARAM1_REQUIRED, aBuf);
- // v1.0.32:
- bool ignore_load_failure = (parameter[0] == '*' && toupper(parameter[1]) == 'I'); // Relies on short-circuit boolean order.
- if (ignore_load_failure)
- {
- parameter += 2;
- if (IS_SPACE_OR_TAB(*parameter)) // Skip over at most one space or tab, since others might be a literal part of the filename.
- ++parameter;
- }
- size_t space_remaining = LINE_SIZE - (parameter-aBuf);
- char buf[MAX_PATH];
- StrReplace(parameter, "%A_ScriptDir%", mFileDir, SCS_INSENSITIVE, 1, space_remaining); // v1.0.35.11. Caller has ensured string is writable.
- if (strcasestr(parameter, "%A_AppData%")) // v1.0.45.04: This and the next were requested by Tekl to make it easier to customize scripts on a per-user basis.
- {
- BIV_AppData(buf, "A_AppData");
- StrReplace(parameter, "%A_AppData%", buf, SCS_INSENSITIVE, 1, space_remaining);
- }
- if (strcasestr(parameter, "%A_AppDataCommon%")) // v1.0.45.04.
- {
- BIV_AppData(buf, "A_AppDataCommon");
- StrReplace(parameter, "%A_AppDataCommon%", buf, SCS_INSENSITIVE, 1, space_remaining);
- }
- DWORD attr = GetFileAttributes(parameter);
- if (attr != 0xFFFFFFFF && (attr & FILE_ATTRIBUTE_DIRECTORY)) // File exists and its a directory (possibly A_ScriptDir or A_AppData set above).
- {
- // v1.0.35.11 allow changing of load-time directory to increase flexibility. This feature has
- // been asked for directly or indirectly several times.
- // If a script ever wants to use a string like "%A_ScriptDir%" literally in an include's filename,
- // that would not work. But that seems too rare to worry about.
- // v1.0.45.01: Call SetWorkingDir() vs. SetCurrentDirectory() so that it succeeds even for a root
- // drive like C: that lacks a backslash (see SetWorkingDir() for details).
- SetWorkingDir(parameter);
- return CONDITION_TRUE;
- }
- // Since above didn't return, it's a file (or non-existent file, in which case the below will display
- // the error). This will also display any other errors that occur:
- return LoadIncludedFile(parameter, is_include_again, ignore_load_failure) ? CONDITION_TRUE : FAIL;
- #endif
- }
- if (IS_DIRECTIVE_MATCH("#NoEnv"))
- {
- g_NoEnv = TRUE;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#NoTrayIcon"))
- {
- g_NoTrayIcon = true;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#Persistent"))
- {
- g_persistent = true;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#SingleInstance"))
- {
- g_AllowOnlyOneInstance = SINGLE_INSTANCE_PROMPT; // Set default.
- if (parameter)
- {
- if (!stricmp(parameter, "Force"))
- g_AllowOnlyOneInstance = SINGLE_INSTANCE_REPLACE;
- else if (!stricmp(parameter, "Ignore"))
- g_AllowOnlyOneInstance = SINGLE_INSTANCE_IGNORE;
- else if (!stricmp(parameter, "Off"))
- g_AllowOnlyOneInstance = SINGLE_INSTANCE_OFF;
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#InstallKeybdHook"))
- {
- // It seems best not to report this warning because a user may want to use partial functionality
- // of a script on Win9x:
- //MsgBox("#InstallKeybdHook is not supported on Windows 95/98/Me. This line will be ignored.");
- if (!g_os.IsWin9x())
- Hotkey::RequireHook(HOOK_KEYBD);
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#InstallMouseHook"))
- {
- // It seems best not to report this warning because a user may want to use partial functionality
- // of a script on Win9x:
- //MsgBox("#InstallMouseHook is not supported on Windows 95/98/Me. This line will be ignored.");
- if (!g_os.IsWin9x())
- Hotkey::RequireHook(HOOK_MOUSE);
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#UseHook"))
- {
- g_ForceKeybdHook = !parameter || Line::ConvertOnOff(parameter) != TOGGLED_OFF;
- return CONDITION_TRUE;
- }
- if (!strnicmp(aBuf, "#IfWin", 6))
- {
- bool invert = !strnicmp(aBuf + 6, "Not", 3);
- if (!strnicmp(aBuf + (invert ? 9 : 6), "Active", 6)) // It matches #IfWin[Not]Active.
- g_HotCriterion = invert ? HOT_IF_NOT_ACTIVE : HOT_IF_ACTIVE;
- else if (!strnicmp(aBuf + (invert ? 9 : 6), "Exist", 5))
- g_HotCriterion = invert ? HOT_IF_NOT_EXIST : HOT_IF_EXIST;
- else // It starts with #IfWin but isn't Active or Exist: Don't alter g_HotCriterion.
- return CONDITION_FALSE; // Indicate unknown directive since there are currently no other possibilities.
- if (!parameter) // The omission of the parameter indicates that any existing criteria should be turned off.
- {
- g_HotCriterion = HOT_NO_CRITERION; // Indicate that no criteria are in effect for subsequent hotkeys.
- g_HotWinTitle = ""; // Helps maintainability and some things might rely on it.
- g_HotWinText = ""; //
- return CONDITION_TRUE;
- }
- char *hot_win_title = parameter, *hot_win_text; // Set default for title; text is determined later.
- // Scan for the first non-escaped comma. If there is one, it marks the second paramter: WinText.
- char *cp, *first_non_escaped_comma;
- for (first_non_escaped_comma = NULL, cp = hot_win_title; ; ++cp) // Increment to skip over the symbol just found by the inner for().
- {
- for (; *cp && !(*cp == g_EscapeChar || *cp == g_delimiter || *cp == g_DerefChar); ++cp); // Find the next escape char, comma, or %.
- if (!*cp) // End of string was found.
- break;
- #define ERR_ESCAPED_COMMA_PERCENT "Literal commas and percent signs must be escaped (e.g. `%)"
- if (*cp == g_DerefChar)
- return ScriptError(ERR_ESCAPED_COMMA_PERCENT, aBuf);
- if (*cp == g_delimiter) // non-escaped delimiter was found.
- {
- // Preserve the ability to add future-use parameters such as section of window
- // over which the mouse is hovering, e.g. #IfWinActive, Untitled - Notepad,, TitleBar
- if (first_non_escaped_comma) // A second non-escaped comma was found.
- return ScriptError(ERR_ESCAPED_COMMA_PERCENT, aBuf);
- // Otherwise:
- first_non_escaped_comma = cp;
- continue; // Check if there are any more non-escaped commas.
- }
- // Otherwise, an escape character was found, so skip over the next character (if any).
- if (!*(++cp)) // The string unexpectedly ends in an escape character, so avoid out-of-bounds.
- break;
- // Otherwise, the ++cp above has skipped over the escape-char itself, and the loop's ++cp will now
- // skip over the char-to-be-escaped, which is not the one we want (even if it is a comma).
- }
- if (first_non_escaped_comma) // Above found a non-escaped comma, so there is a second parameter (WinText).
- {
- // Omit whitespace to (seems best to conform to convention/expectations rather than give
- // strange whitespace flexibility that would likely cause unwanted bugs due to inadvertently
- // have two spaces instead of one). The user may use `s and `t to put literal leading/trailing
- // spaces/tabs into these paramters.
- hot_win_text = omit_leading_whitespace(first_non_escaped_comma + 1);
- *first_non_escaped_comma = '\0'; // Terminate at the comma to split off hot_win_title on its own.
- rtrim(hot_win_title, first_non_escaped_comma - hot_win_title); // Omit whitespace (see similar comment above).
- // The following must be done only after trimming and omitting whitespace above, so that
- // `s and `t can be used to insert leading/trailing spaces/tabs. ConvertEscapeSequences()
- // also supports insertion of literal commas via escaped sequences.
- ConvertEscapeSequences(hot_win_text, g_EscapeChar, true);
- }
- else
- hot_win_text = ""; // And leave hot_win_title set to the entire string because there's only one parameter.
- // The following must be done only after trimming and omitting whitespace above (see similar comment above).
- ConvertEscapeSequences(hot_win_title, g_EscapeChar, true);
- // The following also handles the case where both title and text are blank, which could happen
- // due to something weird but legit like: #IfWinActive, ,
- if (!SetGlobalHotTitleText(hot_win_title, hot_win_text))
- return ScriptError(ERR_OUTOFMEM); // So rare that no second param is provided (since its contents may have been temp-terminated or altered above).
- return CONDITION_TRUE;
- } // Above completely handles all directives and non-directives that start with "#IfWin".
- if (IS_DIRECTIVE_MATCH("#Hotstring"))
- {
- if (parameter)
- {
- char *suboption = strcasestr(parameter, "EndChars");
- if (suboption)
- {
- // Since it's not realistic to have only a couple, spaces and literal tabs
- // must be included in between other chars, e.g. `n `t has a space in between.
- // Also, EndChar \t will have a space and a tab since there are two spaces
- // after the word EndChar.
- if ( !(parameter = StrChrAny(suboption, "\t ")) )
- return CONDITION_TRUE;
- strlcpy(g_EndChars, ++parameter, sizeof(g_EndChars));
- ConvertEscapeSequences(g_EndChars, g_EscapeChar, false);
- return CONDITION_TRUE;
- }
- if (!strnicmp(parameter, "NoMouse", 7)) // v1.0.42.03
- {
- g_HSResetUponMouseClick = false;
- return CONDITION_TRUE;
- }
- // Otherwise assume it's a list of options. Note that for compatibility with its
- // other caller, it will stop at end-of-string or ':', whichever comes first.
- Hotstring::ParseOptions(parameter, g_HSPriority, g_HSKeyDelay, g_HSSendMode, g_HSCaseSensitive
- , g_HSConformToCase, g_HSDoBackspace, g_HSOmitEndChar, g_HSSendRaw, g_HSEndCharRequired
- , g_HSDetectWhenInsideWord, g_HSDoReset);
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#HotkeyModifierTimeout"))
- {
- if (parameter)
- g_HotkeyModifierTimeout = ATOI(parameter); // parameter was set to the right position by the above macro
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#HotkeyInterval"))
- {
- if (parameter)
- {
- g_HotkeyThrottleInterval = ATOI(parameter); // parameter was set to the right position by the above macro
- if (g_HotkeyThrottleInterval < 10) // values under 10 wouldn't be useful due to timer granularity.
- g_HotkeyThrottleInterval = 10;
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#MaxHotkeysPerInterval"))
- {
- if (parameter)
- {
- g_MaxHotkeysPerInterval = ATOI(parameter); // parameter was set to the right position by the above macro
- if (g_MaxHotkeysPerInterval < 1) // sanity check
- g_MaxHotkeysPerInterval = 1;
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#MaxThreadsPerHotkey"))
- {
- if (parameter)
- {
- // Use value as a temp holder since it's int vs. UCHAR and can thus detect very large or negative values:
- value = ATOI(parameter); // parameter was set to the right position by the above macro
- if (value > MAX_THREADS_LIMIT) // For now, keep this limited to prevent stack overflow due to too many pseudo-threads.
- value = MAX_THREADS_LIMIT; // UPDATE: To avoid array overflow, this limit must by obeyed except where otherwise documented.
- else if (value < 1)
- value = 1;
- g_MaxThreadsPerHotkey = value; // Note: g_MaxThreadsPerHotkey is UCHAR.
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#MaxThreadsBuffer"))
- {
- g_MaxThreadsBuffer = !parameter || Line::ConvertOnOff(parameter) != TOGGLED_OFF;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#MaxThreads"))
- {
- if (parameter)
- {
- value = ATOI(parameter); // parameter was set to the right position by the above macro
- if (value > MAX_THREADS_LIMIT) // For now, keep this limited to prevent stack overflow due to too many pseudo-threads.
- value = MAX_THREADS_LIMIT; // UPDATE: To avoid array overflow, this limit must by obeyed except where otherwise documented.
- else if (value < 1)
- value = 1;
- g_MaxThreadsTotal = value;
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#ClipboardTimeout"))
- {
- if (parameter)
- g_ClipboardTimeout = ATOI(parameter); // parameter was set to the right position by the above macro
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#LTrim"))
- {
- g_ContinuationLTrim = !parameter || Line::ConvertOnOff(parameter) != TOGGLED_OFF;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#WinActivateForce"))
- {
- g_WinActivateForce = true;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#ErrorStdOut"))
- {
- mErrorStdOut = true;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#AllowSameLineComments")) // i.e. There's no way to turn it off, only on.
- {
- g_AllowSameLineComments = true;
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#MaxMem"))
- {
- if (parameter)
- {
- double valuef = ATOF(parameter); // parameter was set to the right position by the above macro
- if (valuef > 4095) // Don't exceed capacity of VarSizeType, which is currently a DWORD (4 gig).
- valuef = 4095; // Don't use 4096 since that might be a special/reserved value for some functions.
- else if (valuef < 1)
- valuef = 1;
- g_MaxVarCapacity = (VarSizeType)(valuef * 1024 * 1024);
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#KeyHistory"))
- {
- if (parameter)
- {
- g_MaxHistoryKeys = ATOI(parameter); // parameter was set to the right position by the above macro
- if (g_MaxHistoryKeys < 0)
- g_MaxHistoryKeys = 0;
- else if (g_MaxHistoryKeys > 500)
- g_MaxHistoryKeys = 500;
- // Above: There are two reasons for limiting the history file to 500 keystrokes:
- // 1) GetHookStatus() only has a limited size buffer in which to transcribe the keystrokes.
- // 500 events is about what you would expect to fit in a 32 KB buffer (it the unlikely event
- // that the transcribed events create too much text, the text will be truncated, so it's
- // not dangerous anyway).
- // 2) To reduce the impression that AutoHotkey designed for key logging (the key history file
- // is in a very unfriendly format that type of key logging anyway).
- }
- return CONDITION_TRUE;
- }
- // For the below series, it seems okay to allow the comment flag to contain other reserved chars,
- // such as DerefChar, since comments are evaluated, and then taken out of the game at an earlier
- // stage than DerefChar and the other special chars.
- if (IS_DIRECTIVE_MATCH("#CommentFlag"))
- {
- if (parameter)
- {
- if (!*(parameter + 1)) // i.e. the length is 1
- {
- // Don't allow '#' since it's the preprocessor directive symbol being used here.
- // Seems ok to allow "." to be the comment flag, since other constraints mandate
- // that at least one space or tab occur to its left for it to be considered a
- // comment marker.
- if (*parameter == '#' || *parameter == g_DerefChar || *parameter == g_EscapeChar || *parameter == g_delimiter)
- return ScriptError(ERR_PARAM1_INVALID, aBuf);
- // Exclude hotkey definition chars, such as ^ and !, because otherwise
- // the following example wouldn't work:
- // User defines ! as the comment flag.
- // The following hotkey would never be in effect since it's considered to
- // be commented out:
- // !^a::run,notepad
- if (*parameter == '!' || *parameter == '^' || *parameter == '+' || *parameter == '$' || *parameter == '~' || *parameter == '*'
- || *parameter == '<' || *parameter == '>')
- // Note that '#' is already covered by the other stmt. above.
- return ScriptError(ERR_PARAM1_INVALID, aBuf);
- }
- strlcpy(g_CommentFlag, parameter, MAX_COMMENT_FLAG_LENGTH + 1);
- g_CommentFlagLength = strlen(g_CommentFlag); // Keep this in sync with above.
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#EscapeChar"))
- {
- if (parameter)
- {
- // Don't allow '.' since that can be part of literal floating point numbers:
- if ( *parameter == '#' || *parameter == g_DerefChar || *parameter == g_delimiter || *parameter == '.'
- || (g_CommentFlagLength == 1 && *parameter == *g_CommentFlag) )
- return ScriptError(ERR_PARAM1_INVALID, aBuf);
- g_EscapeChar = *parameter;
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#DerefChar"))
- {
- if (parameter)
- {
- if ( *parameter == g_EscapeChar || *parameter == g_delimiter || *parameter == '.'
- || (g_CommentFlagLength == 1 && *parameter == *g_CommentFlag) ) // Fix for v1.0.47.05: Allow deref char to be # as documented.
- return ScriptError(ERR_PARAM1_INVALID, aBuf);
- g_DerefChar = *parameter;
- }
- return CONDITION_TRUE;
- }
- if (IS_DIRECTIVE_MATCH("#Delimiter"))
- {
- // Attempts to change the delimiter to its starting default (comma) are ignored.
- // For example, "#Delimiter ," isn't meaningful if the delimiter already is a comma,
- // which is good because "parameter" has already assumed that the comma is accidental
- // (not a symbol) and omitted it.
- if (parameter)
- {
- if ( *parameter == '#' || *parameter == g_EscapeChar || *parameter == g_DerefChar || *parameter == '.'
- || (g_CommentFlagLength == 1 && *parameter == *g_CommentFlag) )
- return ScriptError(ERR_PARAM1_INVALID, aBuf);
- g_delimiter = *parameter;
- }
- return CONDITION_TRUE;
- }
- // Otherwise, report that this line isn't a directive:
- return CONDITION_FALSE;
- }
- void ScriptTimer::Disable()
- {
- mEnabled = false;
- --g_script.mTimerEnabledCount;
- if (!g_script.mTimerEnabledCount && !g_nLayersNeedingTimer && !Hotkey::sJoyHotkeyCount)
- KILL_MAIN_TIMER
- // Above: If there are now no enabled timed subroutines, kill the main timer since there's no other
- // reason for it to exist if we're here. This is because or direct or indirect caller is
- // currently always ExecUntil(), which doesn't need the timer while its running except to
- // support timed subroutines. UPDATE: The above is faulty; Must also check g_nLayersNeedingTimer
- // because our caller can be one that still needs a timer as proven by this script that
- // hangs otherwise:
- //SetTimer, Test, on
- //Sleep, 1000
- //msgbox, done
- //return
- //Test:
- //SetTimer, Test, off
- //return
- }
- ResultType Script::UpdateOrCreateTimer(Label *aLabel, char *aPeriod, char *aPriority, bool aEnable
- , bool aUpdatePriorityOnly)
- // Caller should specific a blank aPeriod to prevent the timer's period from being changed
- // (i.e. if caller just wants to turn on or off an existing timer). But if it does this
- // for a non-existent timer, that timer will be created with the default period as specfied in
- // the constructor.
- {
- ScriptTimer *timer;
- for (timer = mFirstTimer; timer != NULL; timer = timer->mNextTimer)
- if (timer->mLabel == aLabel) // Match found.
- break;
- bool timer_existed = (timer != NULL);
- if (!timer_existed) // Create it.
- {
- if ( !(timer = new ScriptTimer(aLabel)) )
- return ScriptError(ERR_OUTOFMEM);
- if (!mFirstTimer)
- mFirstTimer = mLastTimer = timer;
- else
- {
- mLastTimer->mNextTimer = timer;
- // This must be done after the above:
- mLastTimer = timer;
- }
- ++mTimerCount;
- }
- // Update its members:
- if (aEnable && !timer->mEnabled) // Must check both or the mTimerEnabledCount below will be wrong.
- {
- // The exception is if the timer already existed but the caller only wanted its priority changed:
- if (!(timer_existed && aUpdatePriorityOnly))
- {
- timer->mEnabled = true;
- ++mTimerEnabledCount;
- SET_MAIN_TIMER // Ensure the API timer is always running when there is at least one enabled timed subroutine.
- }
- //else do nothing, leave it disabled.
- }
- else if (!aEnable && timer->mEnabled) // Must check both or the below count will be wrong.
- timer->Disable();
- if (*aPeriod) // Caller wanted us to update this member.
- {
- __int64 period = ATOI64(aPeriod);
- if (period < 0) // v1.0.46.16: Support negative periods to mean "run only once".
- {
- timer->mRunOnlyOnce = true;
- timer->mPeriod = (DWORD)-period;
- }
- else // Positive number. v1.0.36.33: Changed from int to DWORD, and ATOI to ATOU, to double its capacity:
- {
- timer->mPeriod = (DWORD)period; // Always use this method & check to retain compatibility with existing scripts.
- timer->mRunOnlyOnce = false;
- }
- }
- if (*aPriority) // Caller wants this member to be changed from its current or default value.
- timer->mPriority = ATOI(aPriority); // Read any float in a runtime variable reference as an int.
- if (!(timer_existed && aUpdatePriorityOnly))
- // Caller relies on us updating mTimeLastRun in this case. This is done because it's more
- // flexible, e.g. a user might want to create a timer that is triggered 5 seconds from now.
- // In such a case, we don't want the timer's first triggering to occur immediately.
- // Instead, we want it to occur only when the full 5 seconds have elapsed:
- timer->mTimeLastRun = GetTickCount();
- // Below is obsolete, see above for why:
- // We don't have to kill or set the main timer because the only way this function is called
- // is directly from the execution of a script line inside ExecUntil(), in which case:
- // 1) KILL_MAIN_TIMER is never needed because the timer shouldn't exist while in ExecUntil().
- // 2) SET_MAIN_TIMER is never needed because it will be set automatically the next time ExecUntil()
- // calls MsgSleep().
- return OK;
- }
- Label *Script::FindLabel(char *aLabelName)
- // Returns the first label whose name matches aLabelName, or NULL if not found.
- // v1.0.42: Since duplicates labels are now possible (to support #IfWin variants of a particular
- // hotkey or hotstring), callers must be aware that only the first match is returned.
- // This helps performance by requiring on average only half the labels to be searched before
- // a match is found.
- {
- if (!aLabelName || !*aLabelName) return NULL;
- for (Label *label = mFirstLabel; label != NULL; label = label->mNextLabel)
- if (!stricmp(label->mName, aLabelName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- return label; // Match found.
- return NULL; // No match found.
- }
- ResultType Script::AddLabel(char *aLabelName, bool aAllowDupe)
- // Returns OK or FAIL.
- {
- if (!*aLabelName)
- return FAIL; // For now, silent failure because callers should check this beforehand.
- if (!aAllowDupe && FindLabel(aLabelName)) // Relies on short-circuit boolean order.
- // Don't attempt to dereference "duplicate_label->mJumpToLine because it might not
- // exist yet. Example:
- // label1:
- // label1: <-- This would be a dupe-error but it doesn't yet have an mJumpToLine.
- // return
- return ScriptError("Duplicate label.", aLabelName);
- char *new_name = SimpleHeap::Malloc(aLabelName);
- if (!new_name)
- return FAIL; // It already displayed the error for us.
- Label *the_new_label = new Label(new_name); // Pass it the dynamic memory area we created.
- if (the_new_label == NULL)
- return ScriptError(ERR_OUTOFMEM);
- the_new_label->mPrevLabel = mLastLabel; // Whether NULL or not.
- if (mFirstLabel == NULL)
- mFirstLabel = the_new_label;
- else
- mLastLabel->mNextLabel = the_new_label;
- // This must be done after the above:
- mLastLabel = the_new_label;
- if (!stricmp(new_name, "OnClipboardChange"))
- mOnClipboardChangeLabel = the_new_label;
- return OK;
- }
- ResultType Script::ParseAndAddLine(char *aLineText, ActionTypeType aActionType, ActionTypeType aOldActionType
- , char *aActionName, char *aEndMarker, char *aLiteralMap, size_t aLiteralMapLength)
- // Returns OK or FAIL.
- // aLineText needs to be a string whose contents are modifiable (though the string won't be made any
- // longer than it is now, so it doesn't have to be of size LINE_SIZE). This helps performance by
- // allowing the string to be split into sections without having to make temporary copies.
- {
- #ifdef _DEBUG
- if (!aLineText || !*aLineText)
- return ScriptError("DEBUG: ParseAndAddLine() called incorrectly.");
- #endif
- bool in_quotes;
- int open_parens;
- char action_name[MAX_VAR_NAME_LENGTH + 1], *end_marker;
- if (aActionName) // i.e. this function was called recursively with explicit values for the optional params.
- {
- strcpy(action_name, aActionName);
- end_marker = aEndMarker;
- }
- else if (aActionType == ACT_EXPRESSION)
- {
- *action_name = '\0';
- end_marker = NULL; // Indicate that there is no action to mark the end of.
- }
- else // We weren't called recursively from self, nor is it ACT_EXPRESSION, so set action_name and end_marker the normal way.
- {
- for (;;) // A loop with only one iteration so that "break" can be used instead of a lot of nested if's.
- {
- if (!g->CurrentFunc) // Not inside a function body, so "Global"/"Local"/"Static" get no special treatment.
- break;
- int declare_type;
- char *cp;
- if (!strnicmp(aLineText, "Global", 6)) // Checked first because it's more common than the others.
- {
- cp = aLineText + 6; // The character after the declaration word.
- declare_type = VAR_DECLARE_GLOBAL;
- }
- else if (!strnicmp(aLineText, "Local", 5))
- {
- cp = aLineText + 5; // The character after the declaration word.
- declare_type = VAR_DECLARE_LOCAL;
- }
- else if (!strnicmp(aLineText, "Static", 6)) // Static also implies local (for functions that default to global).
- {
- cp = aLineText + 6; // The character after the declaration word.
- declare_type = VAR_DECLARE_STATIC;
- }
- else // It's not the word "global", "local", or static, so no further checking is done.
- break;
- if (*cp && !IS_SPACE_OR_TAB(*cp)) // There is a character following the word local but it's not a space or tab.
- break; // It doesn't qualify as being the global or local keyword because it's something like global2.
- if (*cp && *(cp = omit_leading_whitespace(cp))) // Probably always a true stmt since caller rtrimmed it, but even if not it's handled correctly.
- {
- // Check whether the first character is an operator by seeing if it alone would be a
- // valid variable name. If it's not valid, this doesn't qualify as the global or local
- // keyword because it's something like this instead:
- // local := xyz
- // local += 3
- char orig_char = cp[1];
- cp[1] = '\0'; // Temporarily terminate.
- ResultType result = Var::ValidateName(cp, false, DISPLAY_NO_ERROR);
- cp[1] = orig_char; // Undo the termination.
- if (!result) // It's probably operator, e.g. local = %var%
- break;
- }
- else // It's the word "global", "local", "static" by itself. But only global or static is valid that way (when it's the first line in the function body).
- {
- // All of the following must be checked to catch back-to-back conflicting declarations such
- // as these:
- // global x
- // global ; Should be an error because global vars are implied/automatic.
- // v1.0.48: Lexikos: Added assume-static mode. For now, this requires "static" to be
- // placed above local or global variable declarations.
- if (declare_type != VAR_DECLARE_LOCAL // i.e. VAR_DECLARE_GLOBAL or VAR_DECLARE_STATIC (can't due be VAR_DECLARE_NONE due to checks higher above).
- && mNextLineIsFunctionBody && g->CurrentFunc->mDefaultVarType == VAR_DECLARE_NONE)
- {
- g->CurrentFunc->mDefaultVarType = declare_type;
- // No further action is required for the word "global" or "static" by itself.
- return OK;
- }
- // Otherwise, it's the word "local" by itself (which isn't allowed since it's the default),
- // or it's the word global or static by itself, but it occurs too far down in the body.
- return ScriptError(ERR_UNRECOGNIZED_ACTION, aLineText); // Vague error since so rare.
- }
- if (mNextLineIsFunctionBody && g->CurrentFunc->mDefaultVarType == VAR_DECLARE_NONE)
- {
- // Both of the above must be checked to catch back-to-back conflicting declarations such
- // as these:
- // local x
- // global y ; Should be an error because global vars are implied/automatic.
- // This line will become first non-directive, non-label line in the function's body.
- // If the first non-directive, non-label line in the function's body contains
- // the "local" keyword, everything inside this function will assume that variables
- // are global unless they are explicitly declared local (this is the opposite of
- // the default). The converse is also true. UPDATE: "static" must also force ASSUME_LOCAL
- // into effect because otherwise statics wouldn't go into the exception list and thus
- // wouldn't be properly looked up when they're referenced throughout the function body.
- // Therefore, if the first line of the function body is "static MyVar", VAR_DECLARE_LOCAL
- // goes into effect permanently, which can be worked around by using the word "global"
- // as the first word of the function instead.
- g->CurrentFunc->mDefaultVarType = declare_type == VAR_DECLARE_LOCAL ? VAR_DECLARE_GLOBAL : VAR_DECLARE_LOCAL;
- }
- else // Since this isn't the first line of the function's body, mDefaultVarType has aleady been set permanently.
- {
- if (declare_type == g->CurrentFunc->mDefaultVarType) // Can't be VAR_DECLARE_NONE at this point.
- {
- // Seems best to flag redundant/unnecessary declarations since they might be an indication
- // to the user that something is being done incorrectly in this function. This errors also
- // remind the user what mode the function is in:
- if (declare_type == VAR_DECLARE_GLOBAL)
- return ScriptError("Global variables must not be declared in this function.", aLineText);
- if (declare_type == VAR_DECLARE_LOCAL)
- return ScriptError("Local variables must not be declared in this function.", aLineText);
- // In assume-static mode, allow declarations in case they contain initializers.
- // Would otherwise lose the ability to "initialize only once upon startup".
- //if (declare_type == VAR_DECLARE_STATIC)
- // return ScriptError("Static variables must not be declared in this function.", aLineText);
- }
- }
- // Since above didn't break or return, a variable is being declared as an exception to the
- // mode specified by mDefaultVarType.
- // v1.0.48: A declaration is an exception to this function's assume-mode when the
- // declaration's general nature as a local-or-global (in which static is considered local)
- // differs from that of the current mode. In other words, a static or local declaration is
- // not an exception unless this function is assume-global. Also, earlier logic has ensured
- // that mDefaultVarType!=VAR_DECLARE_NONE by the time the first variable declaration is reached.
- // Lexikos: Changed the following to support assume-static mode - i.e. when declaring a local,
- // it is only an "exception" if the function is assume-global.
- bool is_exception = ((declare_type == VAR_DECLARE_GLOBAL) != (g->CurrentFunc->mDefaultVarType == VAR_DECLARE_GLOBAL));
- bool open_brace_was_added, belongs_to_if_or_else_or_loop;
- VarSizeType var_name_length;
- char *item;
- for (belongs_to_if_or_else_or_loop = ACT_IS_IF_OR_ELSE_OR_LOOP(mLastLine->mActionType)
- , open_brace_was_added = false, item = cp
- ; *item;) // FOR EACH COMMA-SEPARATED ITEM IN THE DECLARATION LIST.
- {
- char *item_end = StrChrAny(item, ", \t=:"); // Comma, space or tab, equal-sign, colon.
- if (!item_end) // This is probably the last/only variable in the list; e.g. the "x" in "local x"
- item_end = item + strlen(item);
- var_name_length = (VarSizeType)(item_end - item);
- int always_use;
- if (is_exception)
- always_use = g->CurrentFunc->mDefaultVarType == VAR_DECLARE_GLOBAL ? ALWAYS_USE_LOCAL : ALWAYS_USE_GLOBAL;
- else
- always_use = ALWAYS_USE_DEFAULT;
- Var *var;
- bool is_already_exception;
- if ( !(var = FindOrAddVar(item, var_name_length, always_use, &is_already_exception)) )
- return FAIL; // It already displayed the error.
- if (is_already_exception) // It was already in the exception list (previously declared).
- return ScriptError("Duplicate declaration.", item);
- if (var->Type() != VAR_NORMAL || !strlicmp(item, "ErrorLevel", var_name_length)) // Shouldn't be declared either way (global or local).
- return ScriptError("Built-in variables must not be declared.", item);
- for (int i = 0; i < g->CurrentFunc->mParamCount; ++i) // Search by name to find both global and local declarations.
- if (!strlicmp(item, g->CurrentFunc->mParam[i].var->mName, var_name_length))
- return ScriptError("Parameters must not be declared.", item);
- if (is_exception)
- {
- if (mFuncExceptionVarCount >= MAX_FUNC_VAR_EXCEPTIONS)
- return ScriptError("Too many declarations.", item); // Short message since it's so unlikely.
- mFuncExceptionVar[mFuncExceptionVarCount++] = var;
- }
- if (declare_type == VAR_DECLARE_STATIC)
- var->ConvertToStatic();
- else if (declare_type == VAR_DECLARE_LOCAL && g->CurrentFunc->mDefaultVarType == VAR_DECLARE_STATIC) // v1.0.48: Lexikos.
- // For explicitly-declared locals, remove VAR_ATTRIB_STATIC because AddVar() earlier set it
- // as a default due to assume-static mode.
- var->ConvertToNonStatic();
- item_end = omit_leading_whitespace(item_end); // Move up to the next comma, assignment-op, or '\0'.
- bool convert_the_operator;
- switch(*item_end)
- {
- case ',': // No initializer is present for this variable, so move on to the next one.
- item = omit_leading_whitespace(item_end + 1); // Set "item" for use by the next iteration.
- continue; // No further processing needed below.
- case '\0': // No initializer is present for this variable, so move on to the next one.
- item = item_end; // Set "item" for use by the next iteration.
- continue;
- case ':':
- if (item_end[1] != '=') // Colon with no following '='.
- return ScriptError(ERR_UNRECOGNIZED_ACTION, item); // Vague error since so rare.
- item_end += 2; // Point to the character after the ":=".
- convert_the_operator = false;
- break;
- case '=': // Here '=' is clearly an assignment not a comparison, so further below it will be converted to :=
- ++item_end; // Point to the character after the "=".
- convert_the_operator = true;
- break;
- }
- char *right_side_of_operator = item_end; // Save for use by VAR_DECLARE_STATIC below.
- // Since above didn't "continue", this declared variable also has an initializer.
- // Add that initializer as a separate line to be executed at runtime. Separate lines
- // might actually perform better at runtime because most initializers tend to be simple
- // literals or variables that are simplified into non-expressions at runtime. In addition,
- // items without an initializer are omitted, further improving runtime performance.
- // However, the following must be done ONLY after having done the FindOrAddVar()
- // above, since that may have changed this variable to a non-default type (local or global).
- // But what about something like "global x, y=x"? Even that should work as long as x
- // appears in the list prior to initializers that use it.
- // Now, find the comma (or terminator) that marks the end of this sub-statement.
- // The search must exclude commas that are inside quoted/literal strings and those that
- // are inside parentheses (chiefly those of function-calls, but possibly others).
- for (in_quotes = false, open_parens = 0; *item_end; ++item_end) // FIND THE NEXT "REAL" COMMA.
- {
- if (*item_end == ',') // This is outside the switch() further below so that its "break" can get out of the loop.
- {
- if (!in_quotes && open_parens < 1) // A delimiting comma other than one in a sub-statement or function. Shouldn't need to worry about unquoted escaped commas since they don't make sense in a declaration list.
- break;
- // Otherwise, its a quoted/literal comma or one in parentheses (such as function-call).
- continue; // Continue past it to look for the correct comma.
- }
- switch (*item_end)
- {
- case '"': // There are sections similar this one later below; so see them for comments.
- in_quotes = !in_quotes;
- break;
- case '(':
- if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
- ++open_parens;
- break;
- case ')':
- if (!in_quotes)
- {
- if (!open_parens)
- return ScriptError(ERR_MISSING_OPEN_PAREN, item);
- --open_parens;
- }
- break;
- //default: some other character; just have the loop skip over it.
- }
- } // for() to look for the ending comma or terminator of this sub-statement.
- if (open_parens) // At least one '(' is never closed.
- return ScriptError(ERR_MISSING_CLOSE_PAREN, item); // Use "item" because the problem is probably somewhere after that point in the declaration list.
- if (in_quotes)
- return ScriptError(ERR_MISSING_CLOSE_QUOTE, item);
- // Above has now found the final comma of this sub-statement (or the terminator if there is no comma).
- char *terminate_here = omit_trailing_whitespace(item, item_end-1) + 1; // v1.0.47.02: Fix the fact that "x=5 , y=6" would preserve the whitespace at the end of "5". It also fixes wrongly showing a syntax error for things like: static d="xyz" , e = 5
- char orig_char = *terminate_here;
- *terminate_here = '\0'; // Temporarily terminate (it might already be the terminator, but that's harmless).
- if (declare_type == VAR_DECLARE_STATIC) // v1.0.46: Support simple initializers for static variables.
- {
- // The following is similar to the code used to support default values for function parameters.
- // So maybe maintain them together.
- right_side_of_operator = omit_leading_whitespace(right_side_of_operator);
- if (!stricmp(right_side_of_operator, "false"))
- var->Assign("0");
- else if (!stricmp(right_side_of_operator, "true"))
- var->Assign("1");
- else // The only other supported initializers are "string", integers, and floats.
- {
- // Vars could be supported here via FindVar(), but only globals ABOVE this point in
- // the script would be supported (since other globals don't exist yet; in fact, even
- // those that do exist don't have any contents yet, so it would be pointless). So it
- // seems best to wait until full/comprehesive support for expressions is
- // studied/designed for both statics and parameter-default-values.
- if (*right_side_of_operator == '"' && terminate_here[-1] == '"') // Quoted/literal string.
- {
- ++right_side_of_operator; // Omit the opening-quote from further consideration.
- terminate_here[-1] = '\0'; // Remove the close-quote from further consideration.
- ConvertEscapeSequences(right_side_of_operator, g_EscapeChar, false); // Raw escape sequences like `n haven't been converted yet, so do it now.
- // Convert all pairs of quotes into single literal quotes:
- StrReplace(right_side_of_operator, "\"\"", "\"", SCS_SENSITIVE);
- }
- else // It's not a quoted string (nor the empty string); or it has a missing ending quote (rare).
- {
- if (!IsPureNumeric(right_side_of_operator, true, false, true)) // It's not a number, and since we're here it's not a quoted/literal string either.
- return ScriptError("Unsupported static initializer.", right_side_of_operator);
- //else it's an int or float, so just assign the numeric string itself (there
- // doesn't seem to be any need to convert it to float/int first, though that would
- // make things more consistent such as storing .1 as 0.1).
- }
- if (*right_side_of_operator) // It can be "" in cases such as "" being specified literally in the script, in which case nothing needs to be done because all variables start off as "".
- var->Assign(right_side_of_operator);
- }
- }
- else // A non-static initializer, so a line of code must be produced that will be executed at runtime every time the function is called.
- {
- // PERFORMANCE: As of v1.0.48 (with cached binary numbers and pre-postfixed expressions),
- // assignments of literal integers to variables are up to 10% slower when done as a combined
- // (comma-separated) expression rather than each as a separate line. However, this slowness
- // eventually disappears and may even reverse as more and more such expressions are combined
- // into a single expression (e.g. the following is almost the same speed either way:
- // x:=1,y:=22,z:=333,a:=4444,b:=55555). By contrast, assigning a literal string, another
- // variable, or a complex expression is the opposite: they are always faster when done via
- // commas, and they continue to get faster and faster as more expressions are merged into a
- // single comma-separated expression. In light of this, a future version could combine ONLY
- // those declarations that have initializers into a single comma-separately expression rather
- // than making a separate expression for each. However, since it's not always faster to do
- // so (e.g. x:=0,y:=1 is faster as separate statements), and since it is somewhat rare to
- // have a long chain of initializers, and since these performance differences are documented,
- // it might not be worth changing.
- char *line_to_add;
- char new_buf[LINE_SIZE]; // Declared outside the braces below so that it stays in scope long enough. Using so much stack space here and in caller seems unlikely to affect performance, so _alloca seems unlikely to help.
- if (convert_the_operator) // Convert first '=' in item to be ":=".
- {
- // Prevent any chance of overflow by using new_buf (overflow might otherwise occur in cases
- // such as this sub-statement being the very last one in the declaration list, and being
- // at the limit of the buffer's capacity).
- StrReplace(strcpy(new_buf, item), "=", ":=", SCS_SENSITIVE, 1); // Can't overflow because there's only one replacement and we know item's length can't be that close to the capacity limit.
- line_to_add = new_buf;
- }
- else
- line_to_add = item;
- if (belongs_to_if_or_else_or_loop && !open_brace_was_added) // v1.0.46.01: Put braces to allow initializers to work even directly under an IF/ELSE/LOOP. Note that the braces aren't added or needed for static initializers.
- {
- if (!AddLine(ACT_BLOCK_BEGIN))
- return FAIL;
- open_brace_was_added = true;
- }
- // Call Parse() vs. AddLine() because it detects and optimizes simple assignments into
- // non-exprssions for faster runtime execution.
- if (!ParseAndAddLine(line_to_add)) // For simplicity and maintainability, call self rather than trying to set things up properly to stay in self.
- return FAIL; // Above already displayed the error.
- }
- *terminate_here = orig_char; // Undo the temporary termination.
- // Set "item" for use by the next iteration:
- item = (*item_end == ',') // i.e. it's not the terminator and thus not the final item in the list.
- ? omit_leading_whitespace(item_end + 1)
- : item_end; // It's the terminator, so let the loop detect that to finish.
- } // for() each item in the declaration list.
- if (open_brace_was_added)
- if (!AddLine(ACT_BLOCK_END))
- return FAIL;
- return OK;
- } // single-iteration for-loop
- // Since above didn't return, it's not a declaration such as "global MyVar".
- if ( !(end_marker = ParseActionType(action_name, aLineText, true)) )
- return FAIL; // It already displayed the error.
- }
-
- // Above has ensured that end_marker is the address of the last character of the action name,
- // or NULL if there is no action name.
- // Find the arguments (not to be confused with exec_params) of this action, if it has any:
- char *action_args = end_marker ? omit_leading_whitespace(end_marker + 1) : aLineText;
- // Now action_args is either the first delimiter or the first parameter (if the optional first
- // delimiter was omitted).
- bool add_openbrace_afterward = false; // v1.0.41: Set default for use in supporting brace in "if (expr) {" and "Loop {".
- if (*action_args == g_delimiter)
- {
- // Since there's a comma, don't change aActionType because if it's ACT_INVALID, it should stay that way
- // so that "something, += 4" is not a valid assignment or other operator, but should still be checked
- // against the list of commands to see if it's something like "MsgBox, += 4" (in this case, a script may
- // use the comma to avoid ambiguity).
- // Find the start of the next token (or its ending delimiter if the token is blank such as ", ,"):
- for (++action_args; IS_SPACE_OR_TAB(*action_args); ++action_args);
- }
- else if (!aActionType && !aOldActionType) // i.e. the caller hasn't yet determined this line's action type.
- {
- if (!stricmp(action_name, "IF")) // It's an IF-statement.
- {
- /////////////////////////////////////
- // Detect all types of IF-statements.
- /////////////////////////////////////
- char *operation, *next_word;
- if ( *action_args == '(' // i.e. if (expression)
- || *action_args == g_DerefChar && IS_SPACE_OR_TAB(action_args[1]) ) // v1.0.48.04: "if % expr" is always an expressions. This check was added to allow lines like "if % IniWinCount = b" to work rather than being misinterpreted as "if var in", "if var is", and possibly other things. However, "if %var%..." is NOT always an expression because it might be something like: if %A_Index%Array <> unquoted_literal_string
- {
- // To support things like the following, the outermost enclosing parentheses are not removed:
- // if (x < 3) or (x > 6)
- // Also note that although the expression must normally start with an open-parenthesis to be
- // recognized as ACT_IFEXPR, it need not end in a close-paren; e.g. if (x = 1) or !done.
- // If these or any other parentheses are unbalanced, it will caught further below.
- aActionType = ACT_IFEXPR; // Fixed for v1.0.31.01.
- }
- else // Generic or indeterminate IF-statement, so find out what type it is.
- {
- // Skip over the variable name so that the "is" and "is not" operators are properly supported:
- DEFINE_END_FLAGS
- if (operation = StrChrAny(action_args, end_flags))
- operation = omit_leading_whitespace(operation);
- else
- operation = action_args + strlen(action_args); // Point it to the NULL terminator instead.
- // v1.0.42: Fix "If not Installed" not be seen as "If var-named-'not' in MatchList", being
- // careful not to break "If NotInstalled in MatchList". The following are also fixed in
- // a similar way:
- // If not BetweenXXX
- // If not ContainsXXX
- bool first_word_is_not = !strnicmp(action_args, "Not", 3) && strchr(end_flags, action_args[3]);
- switch (*operation)
- {
- case '=': // But don't allow == to be "Equals" since the 2nd '=' might be literal.
- aActionType = ACT_IFEQUAL;
- break;
- case '<':
- switch(operation[1])
- {
- // Note: User can use whitespace to differentiate a literal symbol from
- // part of an operator, e.g. if var1 < = <--- char is literal
- case '=':
- aActionType = ACT_IFLESSOREQUAL;
- operation[1] = ' ';
- break;
- case '>':
- aActionType = ACT_IFNOTEQUAL;
- operation[1] = ' ';
- break;
- default: // i.e. some other character follows '<'
- aActionType = ACT_IFLESS;
- }
- break;
- case '>': // Don't allow >< to be NotEqual since the '<' might be intended as a literal part of an arg.
- if (operation[1] == '=')
- {
- aActionType = ACT_IFGREATEROREQUAL;
- operation[1] = ' '; // Remove it from so that it won't be considered by later parsing.
- }
- else
- aActionType = ACT_IFGREATER;
- break;
- case '!':
- if (operation[1] == '=')
- {
- aActionType = ACT_IFNOTEQUAL;
- operation[1] = ' '; // Remove it from so that it won't be considered by later parsing.
- }
- else
- // To minimize the times where expressions must have an outer set of parentheses,
- // assume all unknown operators are expressions, e.g. "if !var"
- aActionType = ACT_IFEXPR;
- break;
- case 'b': // "Between"
- case 'B':
- // Must fall back to ACT_IFEXPR, otherwise "if not var_name_beginning_with_b" is a syntax error.
- if (first_word_is_not || strnicmp(operation, "between", 7))
- aActionType = ACT_IFEXPR;
- else
- {
- aActionType = ACT_IFBETWEEN;
- // Set things up to be parsed as args further down. A delimiter is inserted later below:
- memset(operation, ' ', 7);
- }
- break;
- case 'c': // "Contains"
- case 'C':
- // Must fall back to ACT_IFEXPR, otherwise "if not var_name_beginning_with_c" is a syntax error.
- if (first_word_is_not || strnicmp(operation, "contains", 8))
- aActionType = ACT_IFEXPR;
- else
- {
- aActionType = ACT_IFCONTAINS;
- // Set things up to be parsed as args further down. A delimiter is inserted later below:
- memset(operation, ' ', 8);
- }
- break;
- case 'i': // "is" or "is not"
- case 'I':
- switch (toupper(operation[1]))
- {
- case 's': // "IS"
- case 'S':
- if (first_word_is_not) // v1.0.45: Had forgotten to fix this one with the others,
- aActionType = ACT_IFEXPR; // so now "if not is_something" and "if not is_something()" work.
- else
- {
- next_word = omit_leading_whitespace(operation + 2);
- if (strnicmp(next_word, "not", 3)) // No need to check for whitespace after the word "not" because things like "if var is notxxx" are never valid.
- aActionType = ACT_IFIS;
- else
- {
- aActionType = ACT_IFISNOT;
- // Remove the word "not" to set things up to be parsed as args further down.
- memset(next_word, ' ', 3);
- }
- operation[1] = ' '; // Remove the 'S' in "IS". 'I' is replaced with ',' later below.
- }
- break;
- case 'n': // "IN"
- case 'N':
- if (first_word_is_not)
- aActionType = ACT_IFEXPR;
- else
- {
- aActionType = ACT_IFIN;
- operation[1] = ' '; // Remove the 'N' in "IN". 'I' is replaced with ',' later below.
- }
- break;
- default:
- // v1.0.35.01 It must fall back to ACT_IFEXPR, otherwise "if not var_name_beginning_with_i"
- // is a syntax error.
- aActionType = ACT_IFEXPR;
- } // switch()
- break;
- case 'n': // It's either "not in", "not between", or "not contains"
- case 'N':
- // Must fall back to ACT_IFEXPR, otherwise "if not var_name_beginning_with_n" is a syntax error.
- if (strnicmp(operation, "not", 3) || !IS_SPACE_OR_TAB(operation[3])) // Fix for v1.0.48: Must also check for whitespace after the word "not" to avoid a syntax error for lines like "if not note".
- aActionType = ACT_IFEXPR;
- else
- {
- // Remove the "NOT" separately in case there is more than one space or tab between
- // it and the following word, e.g. "not between":
- memset(operation, ' ', 3);
- next_word = omit_leading_whitespace(operation + 3);
- if (!strnicmp(next_word, "in", 2))
- {
- aActionType = ACT_IFNOTIN;
- memset(next_word, ' ', 2);
- }
- else if (!strnicmp(next_word, "between", 7))
- {
- aActionType = ACT_IFNOTBETWEEN;
- memset(next_word, ' ', 7);
- }
- else if (!strnicmp(next_word, "contains", 8))
- {
- aActionType = ACT_IFNOTCONTAINS;
- memset(next_word, ' ', 8);
- }
- }
- break;
- default: // To minimize the times where expressions must have an outer set of parentheses, assume all unknown operators are expressions.
- aActionType = ACT_IFEXPR;
- } // switch()
- } // Detection of type of IF-statement.
- if (aActionType == ACT_IFEXPR) // There are various ways above for aActionType to become ACT_IFEXPR.
- {
- // Since this is ACT_IFEXPR, action_args is known not to be an empty string, which is relied on below.
- char *action_args_last_char = action_args + strlen(action_args) - 1; // Shouldn't be a whitespace char since those should already have been removed at an earlier stage.
- if (*action_args_last_char == '{') // This is an if-expression statement with an open-brace on the same line.
- {
- *action_args_last_char = '\0';
- rtrim(action_args, action_args_last_char - action_args); // Remove the '{' and all its whitespace from further consideration.
- add_openbrace_afterward = true;
- }
- }
- else // It's a IF-statement, but a traditional/non-expression one.
- {
- // Set things up to be parsed as args later on.
- *operation = g_delimiter;
- if (aActionType == ACT_IFBETWEEN || aActionType == ACT_IFNOTBETWEEN)
- {
- // I decided against the syntax "if var between 3,8" because the gain in simplicity
- // and the small avoidance of ambiguity didn't seem worth the cost in terms of readability.
- for (next_word = operation;;)
- {
- if ( !(next_word = strcasestr(next_word, "and")) )
- return ScriptError("BETWEEN requires the word AND.", aLineText); // Seems too rare a thing to warrant falling back to ACT_IFEXPR for this.
- if (strchr(" \t", *(next_word - 1)) && strchr(" \t", *(next_word + 3)))
- {
- // Since there's a space or tab on both sides, we know this is the correct "and",
- // i.e. not one contained within one of the parameters. Examples:
- // if var between band and cat ; Don't falsely detect "band"
- // if var betwwen Andy and David ; Don't falsely detect "Andy".
- // Replace the word AND with a delimiter so that it will be parsed correctly later:
- *next_word = g_delimiter;
- *(next_word + 1) = ' ';
- *(next_word + 2) = ' ';
- break;
- }
- else
- next_word += 3; // Skip over this false "and".
- } // for()
- } // ACT_IFBETWEEN
- } // aActionType != ACT_IFEXPR
- }
- else // It isn't an IF-statement, so check for assignments/operators that determine that this line isn't one that starts with a named command.
- {
- //////////////////////////////////////////////////////
- // Detect operators and assignments such as := and +=
- //////////////////////////////////////////////////////
- // This section is done before the section that checks whether action_name is a valid command
- // because it avoids ambiguity in a line such as the following:
- // Input = test ; Would otherwise be confused with the Input command.
- // But there may be times when a line like this is used:
- // MsgBox = ; i.e. the equals is intended to be the first parameter, not an operator.
- // In the above case, the user can provide the optional comma to avoid the ambiguity:
- // MsgBox, =
- char action_args_2nd_char = action_args[1];
- bool convert_pre_inc_or_dec = false; // Set default.
- switch(*action_args)
- {
- case '=': // i.e. var=value (old-style assignment)
- aActionType = ACT_ASSIGN;
- break;
- case ':':
- // v1.0.40: Allow things like "MsgBox :: test" to be valid by insisting that '=' follows ':'.
- if (action_args_2nd_char == '=') // i.e. :=
- aActionType = ACT_ASSIGNEXPR;
- break;
- case '+':
- // Support for ++i (and in the next case, --i). In these cases, action_name must be either
- // "+" or "-", and the first character of action_args must match it.
- if ((convert_pre_inc_or_dec = action_name[0] == '+' && !action_name[1]) // i.e. the pre-increment operator; e.g. ++index.
- || action_args_2nd_char == '=') // i.e. x+=y (by contrast, post-increment is recognized only after we check for a command name to cut down on ambiguity).
- aActionType = ACT_ADD;
- break;
- case '-':
- // Do a complete validation/recognition of the operator to allow a line such as the following,
- // which omits the first optional comma, to still be recognized as a command rather than a
- // variable-with-operator:
- // SetBatchLines -1
- if ((convert_pre_inc_or_dec = action_name[0] == '-' && !action_name[1]) // i.e. the pre-decrement operator; e.g. --index.
- || action_args_2nd_char == '=') // i.e. x-=y (by contrast, post-decrement is recognized only after we check for a command name to cut down on ambiguity).
- aActionType = ACT_SUB;
- break;
- case '*':
- if (action_args_2nd_char == '=') // i.e. *=
- aActionType = ACT_MULT;
- break;
- case '/':
- if (action_args_2nd_char == '=') // i.e. /=
- aActionType = ACT_DIV;
- // ACT_DIV is different than //= and // because ACT_DIV supports floating point inputs by yielding
- // a floating point result (i.e. it doesn't Floor() the result when the inputs are floats).
- else if (action_args_2nd_char == '/' && action_args[2] == '=') // i.e. //=
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- break;
- case '.':
- case '|':
- case '&':
- case '^':
- if (action_args_2nd_char == '=') // i.e. .= and |= and &= and ^=
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- break;
- //case '?': Stand-alone ternary such as true ? fn1() : fn2(). These are rare so are
- // checked later, only after action_name has been checked to see if it's a valid command.
- case '>':
- case '<':
- if (action_args_2nd_char == *action_args && action_args[2] == '=') // i.e. >>= and <<=
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- break;
- //default: Leave aActionType set to ACT_INVALID. This also covers case '\0' in case that's possible.
- } // switch()
- if (aActionType) // An assignment or other type of action was discovered above.
- {
- if (convert_pre_inc_or_dec) // Set up pre-ops like ++index and --index to be parsed properly later.
- {
- // The following converts:
- // ++x -> EnvAdd x,1 (not really "EnvAdd" per se; but ACT_ADD).
- // Set action_args to be the word that occurs after the ++ or --:
- action_args = omit_leading_whitespace(++action_args); // Though there generally isn't any.
- if (StrChrAny(action_args, EXPR_ALL_SYMBOLS ".")) // Support things like ++Var ? f1() : f2() and ++Var /= 5. Don't need strstr(action_args, " ?") because the search already looks for ':'.
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- else
- {
- // Set up aLineText and action_args to be parsed later on as a list of two parameters:
- // The variable name followed by the amount to be added or subtracted (e.g. "ScriptVar, 1").
- // We're not changing the length of aLineText by doing this, so it should be large enough:
- size_t new_length = strlen(action_args);
- // Since action_args is just a pointer into the aLineText buffer (which caller has ensured
- // is modifiable), use memmove() so that overlapping source & dest are properly handled:
- memmove(aLineText, action_args, new_length + 1); // +1 to include the zero terminator.
- // Append the second param, which is just "1" since the ++ and -- only inc/dec by 1:
- aLineText[new_length++] = g_delimiter;
- aLineText[new_length++] = '1';
- aLineText[new_length] = '\0';
- }
- }
- else if (aActionType != ACT_EXPRESSION) // i.e. it's ACT_ASSIGN/ASSIGNEXPR/ADD/SUB/MULT/DIV
- {
- if (aActionType != ACT_ASSIGN) // i.e. it's ACT_ASSIGNEXPR/ADD/SUB/MULT/DIV
- {
- // Find the first non-function comma, which in the case of ACT_ADD/SUB can be
- // either a statement-separator comma (expression) or the time units arg.
- // Reasons for this:
- // 1) ACT_ADD/SUB: Need to distinguish compound statements from date/time math;
- // e.g. "x+=1, y+=2" should be marked as a stand-alone expression, not date math.
- // 2) ACT_ASSIGNEXPR/MULT/DIV (and ACT_ADD/SUB for that matter): Need to make
- // comma-separated sub-expressions into one big ACT_EXPRESSION so that the
- // leftmost sub-expression will get evaluated prior to the others (for consistency
- // and as documented). However, this has some side-effects, such as making
- // the leftmost /= operator into true division rather than ENV_DIV behavior,
- // and treating blanks as errors in math expressions when otherwise ENV_MULT
- // would treat them as zero.
- // ALSO: ACT_ASSIGNEXPR/ADD/SUB/MULT/DIV are made into ACT_EXPRESSION *only* when multi-
- // statement commas are present because the following legacy behaviors must be retained:
- // 1) Math treatment of blanks as zero in ACT_ADD/SUB/etc.
- // 2) EnvDiv's special behavior, which is different than both true divide and floor divide.
- // 3) Possibly add/sub's date/time math.
- // 4) Maybe obsolete: For performance, don't want trivial assignments to become ACT_EXPRESSION.
- char *cp;
- for (in_quotes = false, open_parens = 0, cp = action_args + 2; *cp; ++cp)
- {
- switch (*cp)
- {
- case '"': // This is whole section similar to another one later below, so see it for comments.
- in_quotes = !in_quotes;
- break;
- case '(':
- if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
- ++open_parens;
- break;
- case ')':
- if (!in_quotes)
- --open_parens;
- break;
- }
- if (*cp == g_delimiter && !in_quotes && open_parens < 1) // A delimiting comma other than one in a sub-statement or function. Shouldn't need to worry about unquoted escaped commas since they don't make sense with += and -=.
- {
- if (aActionType == ACT_ADD || aActionType == ACT_SUB)
- {
- cp = omit_leading_whitespace(cp + 1);
- if (StrChrAny(cp, EXPR_ALL_SYMBOLS ".")) // Don't need strstr(cp, " ?") because the search already looks for ':'.
- aActionType = ACT_EXPRESSION; // It's clearly an expression not a word like Days or %VarContainingTheWordDays%.
- //else it's probably date/time math, so leave it as-is.
- }
- else // ACT_ASSIGNEXPR/MULT/DIV, for which any non-function comma qualifies it as multi-statement.
- aActionType = ACT_EXPRESSION;
- break;
- }
- }
- }
- if (aActionType != ACT_EXPRESSION) // The above didn't make it a stand-alone expression.
- {
- // The following converts:
- // x+=2 -> ACT_ADD x, 2.
- // x:=2 -> ACT_ASSIGNEXPR, x, 2
- // etc.
- // But post-inc/dec are recognized only after we check for a command name to cut down on ambiguity
- *action_args = g_delimiter; // Replace the =,+,-,:,*,/ with a delimiter for later parsing.
- if (aActionType != ACT_ASSIGN) // i.e. it's not just a plain equal-sign (which has no 2nd char).
- action_args[1] = ' '; // Remove the "=" from consideration.
- }
- }
- //else it's already an isolated expression, so no changes are desired.
- action_args = aLineText; // Since this is an assignment and/or expression, use the line's full text for later parsing.
- } // if (aActionType)
- } // Handling of assignments and other operators.
- }
- //else aActionType was already determined by the caller.
- // Now the above has ensured that action_args is the first parameter itself, or empty-string if none.
- // If action_args now starts with a delimiter, it means that the first param is blank/empty.
- if (!aActionType && !aOldActionType) // Caller nor logic above has yet determined the action.
- if ( !(aActionType = ConvertActionType(action_name)) ) // Is this line a command?
- aOldActionType = ConvertOldActionType(action_name); // If not, is it an old-command?
- if (!aActionType && !aOldActionType) // Didn't find any action or command in this line.
- {
- // v1.0.41: Support one-true brace style even if there's no space, but make it strict so that
- // things like "Loop{ string" are reported as errors (in case user intended a file-pattern loop).
- if (!stricmp(action_name, "Loop{") && !*action_args)
- {
- aActionType = ACT_LOOP;
- add_openbrace_afterward = true;
- }
- else if (*action_args == '?' && IS_SPACE_OR_TAB(action_args[1]) // '?' currently requires a trailing space or tab because variable names can contain '?' (except '?' by itself). For simplicty, no NBSP check.
- || strchr(EXPR_ALL_SYMBOLS ".", *action_args))
- {
- char *question_mark;
- if ((*action_args == '+' || *action_args == '-') && action_args[1] == *action_args) // Post-inc/dec. See comments further below.
- {
- if (action_args[2]) // i.e. if the ++ and -- isn't the last thing; e.g. x++ ? fn1() : fn2() ... Var++ //= 2
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- else
- {
- // The logic here allows things like IfWinActive-- to be seen as commands even without
- // a space before the -- or ++. For backward compatibility and code simplicity, it seems
- // best to keep that behavior rather than distinguishing between Command-- and Command --.
- // In any case, "Command --" should continue to be seen as a command regardless of what
- // changes are ever made. That's why this section occurs below the command-name lookup.
- // The following converts x++ to "ACT_ADD x,1".
- aActionType = (*action_args == '+') ? ACT_ADD : ACT_SUB;
- *action_args = g_delimiter;
- action_args[1] = '1';
- }
- action_args = aLineText; // Since this is an assignment and/or expression, use the line's full text for later parsing.
- }
- else if (*action_args == '?' // Don't need a leading space if first char is '?' (though should have a trailing, but for simplicity it isn't checked).
- || (question_mark = strstr(action_args, " ? ")) && strchr(question_mark, ':')) // Rough check (see comments below). Relies on short-circuit boolean order.
- {
- // To avoid hindering load-time error detection such as misspelled command names, allow stand-alone
- // expressions only for things that can produce a side-effect (currently only ternaries like
- // the ones mentioned later below need to be checked since the following other things were
- // previously recognized as ACT_EXPRESSION if appropriate: function-calls, post- and
- // pre-inc/dec (++/--), and assignment operators like := += *= (though these don't necessarily
- // need to be ACT_EXPRESSION to support multi-statement; they can be ACT_ASSIGNEXPR, ACT_ADD, etc.
- // and still support comma-separated statements.
- // Stand-alone ternaries are checked for here rather than earlier to allow a command name
- // (of present) to take precedence (since stand-alone ternaries seem much rarer than
- // "Command ? something" such as "MsgBox ? something". Could also check for a colon somewhere
- // to the right if further ambiguity-resolution is ever needed. Also, a stand-alone ternary
- // should have at least one function-call and/or assignment; otherwise it would serve no purpose.
- // A line may contain a stand-alone ternary operator to call functions that have side-effects
- // or perform assignments. For example:
- // IsDone ? fn1() : fn2()
- // 3 > 2 ? x:=1 : y:=1
- // (3 > 2) ... not supported due to overlap with continuation sections.
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- action_args = aLineText; // Since this is an assignment and/or expression, use the line's full text for later parsing.
- }
- //else leave it as an unknown action to avoid hindering load-time error detection.
- // In other words, don't be too permissive about what gets marked as a stand-alone expression.
- }
- if (!aActionType) // Above still didn't find a valid action (i.e. check aActionType again in case the above changed it).
- {
- if (*action_args == '(') // v1.0.46.11: Recognize as multi-statements that start with a function, like "fn(), x:=4". v1.0.47.03: Removed the following check to allow a close-brace to be followed by a comma-less function-call: strchr(action_args, g_delimiter).
- {
- aActionType = ACT_EXPRESSION; // Mark this line as a stand-alone expression.
- action_args = aLineText; // Since this is a function-call followed by a comma and some other expression, use the line's full text for later parsing.
- }
- else
- // v1.0.40: Give a more specific error message now now that hotkeys can make it here due to
- // the change that avoids the need to escape double-colons:
- return ScriptError(strstr(aLineText, HOTKEY_FLAG) ? "Invalid hotkey." : ERR_UNRECOGNIZED_ACTION, aLineText);
- }
- }
- Action &this_action = aActionType ? g_act[aActionType] : g_old_act[aOldActionType];
- //////////////////////////////////////////////////////////////////////////////////////////////
- // Handle escaped-sequences (escaped delimiters and all others except variable deref symbols).
- // This section must occur after all other changes to the pointer value action_args have
- // occurred above.
- //////////////////////////////////////////////////////////////////////////////////////////////
- // The size of this relies on the fact that caller made sure that aLineText isn't
- // longer than LINE_SIZE. Also, it seems safer to use char rather than bool, even
- // though on most compilers they're the same size. Char is always of size 1, but bool
- // can be bigger depending on platform/compiler:
- char literal_map[LINE_SIZE];
- ZeroMemory(literal_map, sizeof(literal_map)); // Must be fully zeroed for this purpose.
- if (aLiteralMap)
- {
- // Since literal map is NOT a string, just an array of char values, be sure to
- // use memcpy() vs. strcpy() on it. Also, caller's aLiteralMap starts at aEndMarker,
- // so adjust it so that it starts at the newly found position of action_args instead:
- int map_offset = (int)(action_args - end_marker); // end_marker is known not to be NULL when aLiteralMap is non-NULL.
- int map_length = (int)(aLiteralMapLength - map_offset);
- if (map_length > 0)
- memcpy(literal_map, aLiteralMap + map_offset, map_length);
- }
- else
- {
- // Resolve escaped sequences and make a map of which characters in the string should
- // be interpreted literally rather than as their native function. In other words,
- // convert any escape sequences in order from left to right (this order is important,
- // e.g. ``% should evaluate to `g_DerefChar not `LITERAL_PERCENT. This part must be
- // done *after* checking for comment-flags that appear to the right of a valid line, above.
- // How literal comment-flags (e.g. semicolons) work:
- //string1; string2 <-- not a problem since string2 won't be considered a comment by the above.
- //string1 ; string2 <-- this would be a user mistake if string2 wasn't supposed to be a comment.
- //string1 `; string 2 <-- since esc seq. is resolved *after* checking for comments, this behaves as intended.
- // Current limitation: a comment-flag longer than 1 can't be escaped, so if "//" were used,
- // as a comment flag, it could never have whitespace to the left of it if it were meant to be literal.
- // Note: This section resolves all escape sequences except those involving g_DerefChar, which
- // are handled by a later section.
- char c;
- int i;
- for (i = 0; ; ++i) // Increment to skip over the symbol just found by the inner for().
- {
- for (; action_args[i] && action_args[i] != g_EscapeChar; ++i); // Find the next escape char.
- if (!action_args[i]) // end of string.
- break;
- c = action_args[i + 1];
- switch (c)
- {
- // Only lowercase is recognized for these:
- case 'a': action_args[i + 1] = '\a'; break; // alert (bell) character
- case 'b': action_args[i + 1] = '\b'; break; // backspace
- case 'f': action_args[i + 1] = '\f'; break; // formfeed
- case 'n': action_args[i + 1] = '\n'; break; // newline
- case 'r': action_args[i + 1] = '\r'; break; // carriage return
- case 't': action_args[i + 1] = '\t'; break; // horizontal tab
- case 'v': action_args[i + 1] = '\v'; break; // vertical tab
- }
- // Replace escape-sequence with its single-char value. This is done event if the pair isn't
- // a recognizable escape sequence (e.g. `? becomes ?), which is the Microsoft approach
- // and might not be a bad way of handing things. There are some exceptions, however.
- // The first of these exceptions (g_DerefChar) is mandatory because that char must be
- // handled at a later stage or escaped g_DerefChars won't work right. The others are
- // questionable, and might be worth further consideration. UPDATE: g_DerefChar is now
- // done here because otherwise, examples such as this fail:
- // - The escape char is backslash.
- // - any instances of \\%, such as c:\\%var% , will not work because the first escape
- // sequence (\\) is resolved to a single literal backslash. But then when \% is encountered
- // by the section that resolves escape sequences for g_DerefChar, the backslash is seen
- // as an escape char rather than a literal backslash, which is not correct. Thus, we
- // resolve all escapes sequences HERE in one go, from left to right.
- // AutoIt2 definitely treats an escape char that occurs at the very end of
- // a line as literal. It seems best to also do it for these other cases too.
- // UPDATE: I cannot reproduce the above behavior in AutoIt2. Maybe it only
- // does it for some commands or maybe I was mistaken. So for now, this part
- // is disabled:
- //if (c == '\0' || c == ' ' || c == '\t')
- // literal_map[i] = 1; // In the map, mark this char as literal.
- //else
- {
- // So these are also done as well, and don't need an explicit check:
- // g_EscapeChar , g_delimiter , (when g_CommentFlagLength > 1 ??): *g_CommentFlag
- // Below has a final +1 to include the terminator:
- MoveMemory(action_args + i, action_args + i + 1, strlen(action_args + i + 1) + 1);
- literal_map[i] = 1; // In the map, mark this char as literal.
- }
- // else: Do nothing, even if the value is zero (the string's terminator).
- }
- }
- ////////////////////////////////////////////////////////////////////////////////////////
- // Do some special preparsing of the MsgBox command, since it is so frequently used and
- // it is also the source of problem areas going from AutoIt2 to 3 and also due to the
- // new numeric parameter at the end. Whenever possible, we want to avoid the need for
- // the user to have to escape commas that are intended to be literal.
- ///////////////////////////////////////////////////////////////////////////////////////
- int mark, max_params_override = 0; // Set default.
- if (aActionType == ACT_MSGBOX)
- {
- // First find out how many non-literal (non-escaped) delimiters are present.
- // Use a high maximum so that we can almost always find and analyze the command's
- // last apparent parameter. This helps error-checking be more informative in a
- // case where the command specifies a timeout as its last param but it's next-to-last
- // param contains delimiters that the user forgot to escape. In other words, this
- // helps detect more often when the user is trying to use the timeout feature.
- // If this weren't done, the command would more often forgive improper syntax
- // and not report a load-time error, even though it's pretty obvious that a load-time
- // error should have been reported:
- #define MAX_MSGBOX_DELIMITERS 20
- char *delimiter[MAX_MSGBOX_DELIMITERS];
- int delimiter_count;
- for (mark = delimiter_count = 0; action_args[mark] && delimiter_count < MAX_MSGBOX_DELIMITERS;)
- {
- for (; action_args[mark]; ++mark)
- if (action_args[mark] == g_delimiter && !literal_map[mark]) // Match found: a non-literal delimiter.
- {
- delimiter[delimiter_count++] = action_args + mark;
- ++mark; // Skip over this delimiter for the next iteration of the outer loop.
- break;
- }
- }
- // If it has only 1 arg (i.e. 0 delimiters within the arg list) no override is needed.
- // Otherwise do more checking:
- if (delimiter_count)
- {
- char *cp;
- // If the first apparent arg is not a non-blank pure number or there are apparently
- // only 2 args present (i.e. 1 delimiter in the arg list), assume the command is being
- // used in its 1-parameter mode:
- if (delimiter_count <= 1) // 2 parameters or less.
- // Force it to be 1-param mode. In other words, we want to make MsgBox a very forgiving
- // command and have it rarely if ever report syntax errors:
- max_params_override = 1;
- else // It has more than 3 apparent params, but is the first param even numeric?
- {
- *delimiter[0] = '\0'; // Temporarily terminate action_args at the first delimiter.
- // Note: If it's a number inside a variable reference, it's still considered 1-parameter
- // mode to avoid ambiguity (unlike the new deref checking for param #4 mentioned below,
- // there seems to be too much ambiguity in this case to justify trying to figure out
- // if the first parameter is a pure deref, and thus that the command should use
- // 3-param or 4-param mode instead).
- if (!IsPureNumeric(action_args)) // No floats allowed. Allow all-whitespace for aut2 compatibility.
- max_params_override = 1;
- *delimiter[0] = g_delimiter; // Restore the string.
- if (!max_params_override)
- {
- // IMPORATANT: The MsgBox cmd effectively has 3 parameter modes:
- // 1-parameter (where all commas in the 1st parameter are automatically literal)
- // 3-parameter (where all commas in the 3rd parameter are automatically literal)
- // 4-parameter (whether the 4th parameter is the timeout value)
- // Thus, the below must be done in a way that recognizes & supports all 3 modes.
- // The above has determined that the cmd isn't in 1-parameter mode.
- // If at this point it has exactly 3 apparent params, allow the command to be
- // processed normally without an override. Otherwise, do more checking:
- if (delimiter_count == 3) // i.e. 3 delimiters, which means 4 params.
- {
- // If the 4th parameter isn't blank or pure numeric, assume the user didn't
- // intend it to be the MsgBox timeout (since that feature is rarely used),
- // instead intending it to be part of parameter #3.
- if (!IsPureNumeric(delimiter[2] + 1, false, true, true))
- {
- // Not blank and not a int or float. Update for v1.0.20: Check if it's a
- // single deref. If so, assume that deref contains the timeout and thus
- // 4-param mode is in effect. This allows the timeout to be contained in
- // a variable, which was requested by one user:
- cp = omit_leading_whitespace(delimiter[2] + 1);
- // Relies on short-circuit boolean order:
- if (*cp != g_DerefChar || literal_map[cp - action_args]) // not a proper deref char.
- max_params_override = 3;
- // else since it does start with a real deref symbol, it must end with one otherwise
- // that will be caught later on as a syntax error anyway. Therefore, don't override
- // max_params, just let it be parsed as 4 parameters.
- }
- // If it has more than 4 params or it has exactly 4 but the 4th isn't blank,
- // pure numeric, or a deref: assume it's being used in 3-parameter mode and
- // that all the other delimiters were intended to be literal.
- }
- else if (delimiter_count > 3) // i.e. 4 or more delimiters, which means 5 or more params.
- {
- // v1.0.48: This section extends smart comma handling so that if parameter #3 (Text)
- // is an expression, any commas in it won't interfere with the Timeout parameter.
- // For example, the timeout parameter below should work now:
- // MsgBox 0, Title, % Func(x,y), 1
- //
- // If the "Text" parameter is an expression then commas inside it can't be intended to
- // be literal/displayed by the user unless they're enclosed in quotes; but in that case,
- // the smartness below isn't needed because it's provided by the parameter-parsing logic
- // in a later section. So in that case it seems safe to avoid setting max_params_override,
- // which fixes examples like the one above. The code further below does this.
- //
- // By contrast, fixing the second parameter (Title) in a similar way would be more
- // difficult and/or would be more likely to break existing scripts. For example if "title"
- // is an expression but "text" is NOT an expression, there might be some commas in "text"
- // that are currently handled as smart/auto-literal, and those cases should be preserved
- // for backward compatibility.
- //
- // If expressions in the "title" parameter ever are fixed to not interfere with the
- // Timeout parameter, perhaps the best way to do it would be to verify that it's an
- // expression, then skip over any commas that are enclosed in quotes or parentheses so that
- // the "real" commas can be counted and used by the rest of the smart-comma logic.
- //
- // For the section below, see comments at the similar code section above:
- cp = omit_leading_whitespace(delimiter[1] + 1); // Parameter #3, the "text" parameter.
- if ( *cp != g_DerefChar || literal_map[cp - action_args] // not a proper deref char...
- || !IS_SPACE_OR_TAB(cp[1]) ) // ...or it's not followed by a space or tab, so it isn't "% ".
- // Since it has too many delimiters to be 4-param mode and since there is no "% "
- // expression present, assume it's 3-param mode so that non-escaped commas in
- // parameters 4 and beyond will be all treated as strings that are part of parameter #3.
- max_params_override = 3;
- }
- //else if 3 params or less: Don't override via max_params_override, just parse it normally.
- }
- }
- }
- } // end of special handling for MsgBox.
- /////////////////////////////////////////////////////////////
- // Parse the parameter string into a list of separate params.
- /////////////////////////////////////////////////////////////
- // MaxParams has already been verified as being <= MAX_ARGS.
- // Any g_delimiter-delimited items beyond MaxParams will be included in a lump inside the last param:
- int nArgs, nArgs_plus_one;
- char *arg[MAX_ARGS], *arg_map[MAX_ARGS];
- ActionTypeType subaction_type = ACT_INVALID; // Must init these.
- ActionTypeType suboldaction_type = OLD_INVALID;
- char subaction_name[MAX_VAR_NAME_LENGTH + 1], *subaction_end_marker = NULL, *subaction_start = NULL;
- int max_params = max_params_override ? max_params_override
- : (mIsAutoIt2 ? (this_action.MaxParamsAu2WithHighBit & 0x7F) // 0x7F removes the high-bit from consideration; that bit is used for an unrelated purpose.
- : this_action.MaxParams);
- int max_params_minus_one = max_params - 1;
- bool is_expression;
- ActionTypeType *np;
- for (nArgs = mark = 0; action_args[mark] && nArgs < max_params; ++nArgs)
- {
- if (nArgs == 2) // i.e. the 3rd arg is about to be added.
- {
- switch (aActionType) // will be ACT_INVALID if this_action is an old-style command.
- {
- case ACT_IFWINEXIST:
- case ACT_IFWINNOTEXIST:
- case ACT_IFWINACTIVE:
- case ACT_IFWINNOTACTIVE:
- subaction_start = action_args + mark;
- if (subaction_end_marker = ParseActionType(subaction_name, subaction_start, false))
- if ( !(subaction_type = ConvertActionType(subaction_name)) )
- suboldaction_type = ConvertOldActionType(subaction_name);
- break;
- }
- if (subaction_type || suboldaction_type)
- // A valid command was found (i.e. AutoIt2-style) in place of this commands Exclude Title
- // parameter, so don't add this item as a param to the command.
- break;
- }
- arg[nArgs] = action_args + mark;
- arg_map[nArgs] = literal_map + mark;
- if (nArgs == max_params_minus_one)
- {
- // Don't terminate the last param, just put all the rest of the line
- // into it. This avoids the need for the user to escape any commas
- // that may appear in the last param. i.e. any commas beyond this
- // point can't be delimiters because we've already reached MaxArgs
- // for this command:
- ++nArgs;
- break;
- }
- // The above does not need the in_quotes and in_parens checks because commas in the last arg
- // are always literal, so there's no problem even in expressions.
- // The following implements the "% " prefix as a means of forcing an expression:
- is_expression = *arg[nArgs] == g_DerefChar && !*arg_map[nArgs] // It's a non-literal deref character.
- && IS_SPACE_OR_TAB(arg[nArgs][1]); // Followed by a space or tab.
- // Find the end of the above arg:
- for (in_quotes = false, open_parens = 0; action_args[mark]; ++mark)
- {
- switch (action_args[mark])
- {
- case '"':
- // The simple method below is sufficient for our purpose even if a quoted string contains
- // pairs of double-quotes to represent a single literal quote, e.g. "quoted ""word""".
- // In other words, it relies on the fact that there must be an even number of quotes
- // inside any mandatory-numeric arg that is an expression such as x=="red,blue"
- in_quotes = !in_quotes;
- break;
- case '(':
- if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
- ++open_parens;
- break;
- case ')':
- if (!in_quotes)
- --open_parens;
- break;
- }
- if (action_args[mark] == g_delimiter && !literal_map[mark]) // A non-literal delimiter (unless its within double-quotes of a mandatory-numeric arg) is a match.
- {
- // If we're inside a pair of quotes or parentheses and this arg is known to be an expression, this
- // delimiter is part this arg and thus not to be used as a delimiter between command args:
- if (in_quotes || open_parens > 0)
- {
- if (is_expression)
- continue;
- if (aActionType == ACT_TRANSFORM && (nArgs == 2 || nArgs == 3)) // i.e. the 3rd or 4th arg is about to be added.
- {
- // Somewhat inefficient in the case where it has to be called for both Arg#2 and Arg#3,
- // but that is pretty rare. Overall, expressions and quoted strings in these args
- // is rare too, so the inefficiency of redundant calls to ConvertTransformCmd() is
- // very small on average, and seems worth the benefit in terms of code simplification.
- // Note that the following might return TRANS_CMD_INVALID just because the sub-command
- // is containined in a variable reference. That is why TRANS_CMD_INVALID does not
- // produce an error at this stage, but only later when the line has been constructed
- // far enough to call ArgHasDeref():
- // i.e. Not the first param, only the third and fourth, which currently are either both numeric or both non-numeric for all cases.
- switch(Line::ConvertTransformCmd(arg[1])) // arg[1] is the second arg.
- {
- // See comment above for why TRANS_CMD_INVALID isn't yet reported as an error:
- #define TRANSFORM_NON_EXPRESSION_CASES \
- case TRANS_CMD_INVALID:\
- case TRANS_CMD_ASC:\
- case TRANS_CMD_UNICODE:\
- case TRANS_CMD_DEREF:\
- case TRANS_CMD_HTML:\
- break; // Do nothing. Leave this_new_arg.is_expression set to its default of false.
- TRANSFORM_NON_EXPRESSION_CASES
- default:
- // For all other sub-commands, Arg #3 and #4 are expression-capable. It doesn't
- // seem necessary to call LegacyArgIsExpression() because the mere fact that
- // we're inside a pair of quotes or parentheses seems enough to indicate that this
- // really is an expression.
- continue;
- }
- }
- // v1.0.43.07: Fixed below to use this_action instead of g_act[aActionType] so that the
- // numeric params of legacy commands like EnvAdd/Sub/LeftClick can be detected. Without
- // this fix, the last comma in a line like "EnvSub, var, Add(2, 3)" is seen as a parameter
- // delimiter, which causes a loadtime syntax error.
- if (np = this_action.NumericParams) // This command has at least one numeric parameter.
- {
- // As of v1.0.25, pure numeric parameters can optionally be numeric expressions, so check for that:
- nArgs_plus_one = nArgs + 1;
- for (; *np; ++np)
- if (*np == nArgs_plus_one) // This arg is enforced to be purely numeric.
- break;
- if (*np) // Match found, so this is a purely numeric arg.
- continue; // This delimiter is disqualified, so look for the next one.
- }
- } // if in quotes or parentheses
- // Since above didn't "continue", this is a real delimiter.
- action_args[mark] = '\0'; // Terminate the previous arg.
- // Trim any whitespace from the previous arg. This operation
- // will not alter the contents of anything beyond action_args[i],
- // so it should be safe. In addition, even though it changes
- // the contents of the arg[nArgs] substring, we don't have to
- // update literal_map because the map is still accurate due
- // to the nature of rtrim). UPDATE: Note that this version
- // of rtrim() specifically avoids trimming newline characters,
- // since the user may have included literal newlines at the end
- // of the string by using an escape sequence:
- rtrim(arg[nArgs]);
- // Omit the leading whitespace from the next arg:
- for (++mark; IS_SPACE_OR_TAB(action_args[mark]); ++mark);
- // Now <mark> marks the end of the string, the start of the next arg,
- // or a delimiter-char (if the next arg is blank).
- break; // Arg was found, so let the outer loop handle it.
- }
- }
- }
- ///////////////////////////////////////////////////////////////////////////////
- // Ensure there are sufficient parameters for this command. Note: If MinParams
- // is greater than 0, the param numbers 1 through MinParams are required to be
- // non-blank.
- ///////////////////////////////////////////////////////////////////////////////
- char error_msg[1024];
- if (nArgs < this_action.MinParams)
- {
- snprintf(error_msg, sizeof(error_msg), "\"%s\" requires at least %d parameter%s."
- , this_action.Name, this_action.MinParams
- , this_action.MinParams > 1 ? "s" : "");
- return ScriptError(error_msg, aLineText);
- }
- for (int i = 0; i < this_action.MinParams; ++i) // It's only safe to do this after the above.
- if (!*arg[i])
- {
- snprintf(error_msg, sizeof(error_msg), "\"%s\" requires that parameter #%u be non-blank."
- , this_action.Name, i + 1);
- return ScriptError(error_msg, aLineText);
- }
- ////////////////////////////////////////////////////////////////////////
- // Handle legacy commands that are supported for backward compatibility.
- ////////////////////////////////////////////////////////////////////////
- if (aOldActionType)
- {
- switch(aOldActionType)
- {
- case OLD_LEFTCLICK:
- case OLD_RIGHTCLICK:
- // Insert an arg at the beginning of the list to indicate the mouse button.
- arg[2] = arg[1]; arg_map[2] = arg_map[1];
- arg[1] = arg[0]; arg_map[1] = arg_map[0];
- arg[0] = aOldActionType == OLD_LEFTCLICK ? "" : "Right"; arg_map[0] = NULL; // "" is treated the same as "Left"
- return AddLine(ACT_MOUSECLICK, arg, ++nArgs, arg_map);
- case OLD_LEFTCLICKDRAG:
- case OLD_RIGHTCLICKDRAG:
- // Insert an arg at the beginning of the list to indicate the mouse button.
- arg[4] = arg[3]; arg_map[4] = arg_map[3]; // Set the 5th arg to be the 4th, etc.
- arg[3] = arg[2]; arg_map[3] = arg_map[2];
- arg[2] = arg[1]; arg_map[2] = arg_map[1];
- arg[1] = arg[0]; arg_map[1] = arg_map[0];
- arg[0] = (aOldActionType == OLD_LEFTCLICKDRAG) ? "Left" : "Right"; arg_map[0] = NULL;
- return AddLine(ACT_MOUSECLICKDRAG, arg, ++nArgs, arg_map);
- case OLD_HIDEAUTOITWIN:
- // This isn't a perfect mapping because the word "on" or "off" might be contained
- // in a variable reference, in which case this conversion will be incorrect.
- // However, variable ref. is exceedingly rare.
- arg[1] = stricmp(arg[0], "On") ? "Icon" : "NoIcon";
- arg[0] = "Tray"; // Assign only after we're done using the old arg[0] value above.
- return AddLine(ACT_MENU, arg, 2, arg_map);
- case OLD_REPEAT:
- if (!AddLine(ACT_REPEAT, arg, nArgs, arg_map))
- return FAIL;
- // For simplicity, always enclose repeat-loop's contents in in a block rather
- // than trying to detect if it has only one line:
- return AddLine(ACT_BLOCK_BEGIN);
- case OLD_ENDREPEAT:
- return AddLine(ACT_BLOCK_END);
- case OLD_WINGETACTIVETITLE:
- arg[nArgs] = "A"; arg_map[nArgs] = NULL; // "A" signifies the active window.
- ++nArgs;
- return AddLine(ACT_WINGETTITLE, arg, nArgs, arg_map);
- case OLD_WINGETACTIVESTATS:
- {
- // Convert OLD_WINGETACTIVESTATS into *two* new commands:
- // Command #1: WinGetTitle, OutputVar, A
- char *width = arg[1]; // Temporary placeholder.
- arg[1] = "A"; arg_map[1] = NULL; // Signifies the active window.
- if (!AddLine(ACT_WINGETTITLE, arg, 2, arg_map))
- return FAIL;
- // Command #2: WinGetPos, XPos, YPos, Width, Height, A
- // Reassign args in the new command's ordering. These lines must occur
- // in this exact order for the copy to work properly:
- arg[0] = arg[3]; arg_map[0] = arg_map[3]; // xpos
- arg[3] = arg[2]; arg_map[3] = arg_map[2]; // height
- arg[2] = width; arg_map[2] = arg_map[1]; // width
- arg[1] = arg[4]; arg_map[1] = arg_map[4]; // ypos
- arg[4] = "A"; arg_map[4] = NULL; // "A" signifies the active window.
- return AddLine(ACT_WINGETPOS, arg, 5, arg_map);
- }
- case OLD_SETENV:
- return AddLine(ACT_ASSIGN, arg, nArgs, arg_map);
- case OLD_ENVADD:
- return AddLine(ACT_ADD, arg, nArgs, arg_map);
- case OLD_ENVSUB:
- return AddLine(ACT_SUB, arg, nArgs, arg_map);
- case OLD_ENVMULT:
- return AddLine(ACT_MULT, arg, nArgs, arg_map);
- case OLD_ENVDIV:
- return AddLine(ACT_DIV, arg, nArgs, arg_map);
- // For these, break rather than return so that further processing can be done:
- case OLD_IFEQUAL:
- aActionType = ACT_IFEQUAL;
- break;
- case OLD_IFNOTEQUAL:
- aActionType = ACT_IFNOTEQUAL;
- break;
- case OLD_IFGREATER:
- aActionType = ACT_IFGREATER;
- break;
- case OLD_IFGREATEROREQUAL:
- aActionType = ACT_IFGREATEROREQUAL;
- break;
- case OLD_IFLESS:
- aActionType = ACT_IFLESS;
- break;
- case OLD_IFLESSOREQUAL:
- aActionType = ACT_IFLESSOREQUAL;
- break;
- #ifdef _DEBUG
- default:
- return ScriptError("DEBUG: Unhandled Old-Command.", action_name);
- #endif
- } // switch()
- }
- //////////////////////////////////////////////////////////////////////////////////////////////////
- // Handle AutoIt2-style IF-statements (i.e. the IF's action is on the same line as the condition).
- //////////////////////////////////////////////////////////////////////////////////////////////////
- // The check below: Don't bother if this IF (e.g. IfWinActive) has zero params or if the
- // subaction was already found above:
- if (nArgs && !subaction_type && !suboldaction_type && ACT_IS_IF_OLD(aActionType, aOldActionType))
- {
- char *delimiter;
- char *last_arg = arg[nArgs - 1];
- for (mark = (int)(last_arg - action_args); action_args[mark]; ++mark)
- {
- if (action_args[mark] == g_delimiter && !literal_map[mark]) // Match found: a non-literal delimiter.
- {
- delimiter = action_args + mark; // save the location of this delimiter
- // Omit the leading whitespace from the next arg:
- for (++mark; IS_SPACE_OR_TAB(action_args[mark]); ++mark);
- // Now <mark> marks the end of the string, the start of the next arg,
- // or a delimiter-char (if the next arg is blank).
- subaction_start = action_args + mark;
- if (subaction_end_marker = ParseActionType(subaction_name, subaction_start, false))
- {
- if ( !(subaction_type = ConvertActionType(subaction_name)) )
- suboldaction_type = ConvertOldActionType(subaction_name);
- if (subaction_type || suboldaction_type) // A valid sub-action (command) was found.
- {
- // Remove this subaction from its parent line; we want it separate:
- *delimiter = '\0';
- rtrim(last_arg);
- }
- // else leave it as-is, i.e. as part of the last param, because the delimiter
- // found above is probably being used as a literal char even though it isn't
- // escaped, e.g. "ifequal, var1, string with embedded, but non-escaped, commas"
- }
- // else, do nothing; reasoning perhaps similar to above comment.
- break;
- }
- }
- }
- // In v1.0.41, the following one-true-brace styles are also supported:
- // Loop { ; Known limitation: Overlaps with file-pattern loop that retrieves single file of name "{".
- // Loop 5 { ; Also overlaps, this time with file-pattern loop that retrieves numeric filename ending in '{'.
- // Loop %Var% { ; Similar, but like the above seems acceptable given extreme rarity of user intending a file pattern.
- if ((aActionType == ACT_LOOP || aActionType == ACT_WHILE)
- && nArgs == 1 && arg[0][0]) // A loop with exactly one, non-blank arg.
- {
- char *arg1 = arg[0]; // For readability and possibly performance.
- // A loop with the above criteria (exactly one arg) can only validly be a normal/counting loop or
- // a file-pattern loop if its parameter's last character is '{'. For the following reasons, any
- // single-parameter loop that ends in '{' is considered to be one-true brace:
- // 1) Extremely rare that a file-pattern loop such as "Loop filename {" would ever be used,
- // and even if it is, the syntax checker will report an unclosed block, making it apparent
- // to the user that a workaround is needed, such as putting the filename into a variable first.
- // 2) Difficulty and code size of distinguishing all possible valid-one-true-braces from those
- // that aren't. For example, the following are ambiguous, so it seems best for consistency
- // and code size reduction just to treat them as one-truce-brace, which will immediately alert
- // the user if the brace isn't closed:
- // a) Loop % (expression) { ; Ambiguous because expression could resolve to a string, thus it would be seen as a file-pattern loop.
- // b) Loop %Var% { ; Similar as above, which means all three of these unintentionally support
- // c) Loop filename{ ; OTB for some types of file loops because it's not worth the code size to "unsupport" them.
- // d) Loop *.txt { ; Like the above: Unintentionally supported, but not documnented.
- // e) (While-loops are also checked here now)
- // Insist that no characters follow the '{' in case the user intended it to be a file-pattern loop
- // such as "Loop {literal-filename".
- char *arg1_last_char = arg1 + strlen(arg1) - 1;
- if (*arg1_last_char == '{')
- {
- add_openbrace_afterward = true;
- *arg1_last_char = '\0'; // Since it will be fully handled here, remove the brace from further consideration.
- if (!rtrim(arg1)) // Trimmed down to nothing, so only a brace was present: remove the arg completely.
- if (aActionType == ACT_LOOP)
- nArgs = 0; // This makes later stages recognize it as an infinite loop rather than a zero-iteration loop.
- else // ACT_WHILE
- return ScriptError(ERR_PARAM1_REQUIRED, aLineText);
- }
- }
- if (!AddLine(aActionType, arg, nArgs, arg_map))
- return FAIL;
- if (add_openbrace_afterward)
- if (!AddLine(ACT_BLOCK_BEGIN))
- return FAIL;
- if (!subaction_type && !suboldaction_type) // There is no subaction in this case.
- return OK;
- // Otherwise, recursively add the subaction, and any subactions it might have, beneath
- // the line just added. The following example:
- // IfWinExist, x, y, IfWinNotExist, a, b, Gosub, Sub1
- // would break down into these lines:
- // IfWinExist, x, y
- // IfWinNotExist, a, b
- // Gosub, Sub1
- return ParseAndAddLine(subaction_start, subaction_type, suboldaction_type, subaction_name, subaction_end_marker
- , literal_map + (subaction_end_marker - action_args) // Pass only the relevant substring of literal_map.
- , strlen(subaction_end_marker));
- }
- inline char *Script::ParseActionType(char *aBufTarget, char *aBufSource, bool aDisplayErrors)
- // inline since it's called so often.
- // aBufTarget should be at least MAX_VAR_NAME_LENGTH + 1 in size.
- // Returns NULL if a failure condition occurs; otherwise, the address of the last
- // character of the action name in aBufSource.
- {
- ////////////////////////////////////////////////////////
- // Find the action name and the start of the param list.
- ////////////////////////////////////////////////////////
- // Allows the delimiter between action-type-name and the first param to be optional by
- // relying on the fact that action-type-names can't contain spaces. Find first char in
- // aLineText that is a space, a delimiter, or a tab. Also search for operator symbols
- // so that assignments and IFs without whitespace are supported, e.g. var1=5,
- // if var2<%var3%. Not static in case g_delimiter is allowed to vary:
- DEFINE_END_FLAGS
- char *end_marker = StrChrAny(aBufSource, end_flags);
- if (end_marker) // Found a delimiter.
- {
- if (*end_marker == '=' && end_marker > aBufSource && end_marker[-1] == '.') // Relies on short-circuit boolean order.
- --end_marker; // v1.0.46.01: Support .=, but not any use of '.' because that is reserved as a struct/member operator.
- if (end_marker > aBufSource) // The delimiter isn't very first char in aBufSource.
- --end_marker;
- // else we allow it to be the first char to support "++i" etc.
- }
- else // No delimiter found, so set end_marker to the location of the last char in string.
- end_marker = aBufSource + strlen(aBufSource) - 1;
- // Now end_marker is the character just prior to the first delimiter or whitespace,
- // or (in the case of ++ and --) the first delimiter itself. Find the end of
- // the action-type name by omitting trailing whitespace:
- end_marker = omit_trailing_whitespace(aBufSource, end_marker);
- // If first char in aBufSource is a delimiter, action_name will consist of just that first char:
- size_t action_name_length = end_marker - aBufSource + 1;
- if (action_name_length > MAX_VAR_NAME_LENGTH)
- {
- if (aDisplayErrors)
- ScriptError(ERR_UNRECOGNIZED_ACTION, aBufSource); // Short/vague message since so rare.
- return NULL;
- }
- strlcpy(aBufTarget, aBufSource, action_name_length + 1);
- return end_marker;
- }
- inline ActionTypeType Script::ConvertActionType(char *aActionTypeString)
- // inline since it's called so often, but don't keep it in the .h due to #include issues.
- {
- // For the loop's index:
- // Use an int rather than ActionTypeType since it's sure to be large enough to go beyond
- // 256 if there happen to be exactly 256 actions in the array:
- for (int action_type = ACT_FIRST_COMMAND; action_type < g_ActionCount; ++action_type)
- if (!stricmp(aActionTypeString, g_act[action_type].Name)) // Match found.
- return action_type;
- return ACT_INVALID; // On failure to find a match.
- }
- inline ActionTypeType Script::ConvertOldActionType(char *aActionTypeString)
- // inline since it's called so often, but don't keep it in the .h due to #include issues.
- {
- for (int action_type = OLD_INVALID + 1; action_type < g_OldActionCount; ++action_type)
- if (!stricmp(aActionTypeString, g_old_act[action_type].Name)) // Match found.
- return action_type;
- return OLD_INVALID; // On failure to find a match.
- }
- bool LegacyArgIsExpression(char *aArgText, char *aArgMap)
- // Helper function for AddLine
- {
- // The section below is here in light of rare legacy cases such as the below:
- // -%y% ; i.e. make it negative.
- // +%y% ; might happen with up/down adjustments on SoundSet, GuiControl progress/slider, etc?
- // Although the above are detected as non-expressions and thus non-double-derefs,
- // the following are not because they're too rare or would sacrifice too much flexibility:
- // 1%y%.0 ; i.e. at a tens/hundreds place and make it a floating point. In addition,
- // 1%y% could be an array, so best not to tag that as non-expression.
- // For that matter, %y%.0 could be an obscure kind of reverse-notation array itself.
- // However, as of v1.0.29, things like %y%000 are allowed, e.g. Sleep %Seconds%000
- // 0x%y% ; i.e. make it hex (too rare to check for, plus it could be an array).
- // %y%%z% ; i.e. concatenate two numbers to make a larger number (too rare to check for)
- char *cp = aArgText + (*aArgText == '-' || *aArgText == '+'); // i.e. +1 if second term evaluates to true.
- return *cp != g_DerefChar // If no deref, for simplicity assume it's an expression since any such non-numeric item would be extremely rare in pre-expression era.
- || !aArgMap || *(aArgMap + (cp != aArgText)) // There's no literal-map or this deref char is not really a deref char because it's marked as a literal.
- || !(cp = strchr(cp + 1, g_DerefChar)) // There is no next deref char.
- || (cp[1] && !IsPureNumeric(cp + 1, false, true, true)); // But that next deref char is not the last char, which means this is not a single isolated deref. v1.0.29: Allow things like Sleep %Var%000.
- // Above does not need to check whether last deref char is marked literal in the
- // arg map because if it is, it would mean the first deref char lacks a matching
- // close-symbol, which will be caught as a syntax error below regardless of whether
- // this is an expression.
- }
- ResultType Script::AddLine(ActionTypeType aActionType, char *aArg[], ArgCountType aArgc, char *aArgMap[])
- // aArg must be a collection of pointers to memory areas that are modifiable, and there
- // must be at least aArgc number of pointers in the aArg array. In v1.0.40, a caller (namely
- // the "macro expansion" for remappings such as "a::b") is allowed to pass a non-NULL value for
- // aArg but a NULL value for aArgMap.
- // Returns OK or FAIL.
- {
- #ifdef _DEBUG
- if (aActionType == ACT_INVALID)
- return ScriptError("DEBUG: BAD AddLine", aArgc > 0 ? aArg[0] : "");
- #endif
- bool do_update_labels;
- if (!aArg && aArgc == UCHAR_MAX) // Special signal from caller to avoid pointing any pending labels to this particular line.
- {
- aArgc = 0;
- do_update_labels = false;
- }
- else
- do_update_labels = true;
- Var *target_var;
- DerefType deref[MAX_DEREFS_PER_ARG]; // Will be used to temporarily store the var-deref locations in each arg.
- int deref_count; // How many items are in deref array.
- ArgStruct *new_arg; // We will allocate some dynamic memory for this, then hang it onto the new line.
- size_t operand_length;
- char *op_begin, *op_end, orig_char;
- char *this_aArgMap, *this_aArg, *cp;
- int open_parens;
- ActionTypeType *np;
- TransformCmds trans_cmd;
- bool is_function;
- //////////////////////////////////////////////////////////
- // Build the new arg list in dynamic memory.
- // The allocated structs will be attached to the new line.
- //////////////////////////////////////////////////////////
- if (!aArgc)
- new_arg = NULL; // Just need an empty array in this case.
- else
- {
- if ( !(new_arg = (ArgStruct *)SimpleHeap::Malloc(aArgc * sizeof(ArgStruct))) )
- return ScriptError(ERR_OUTOFMEM);
- int i, j, i_plus_one;
- bool in_quotes;
- for (i = 0; i < aArgc; ++i)
- {
- ////////////////
- // FOR EACH ARG:
- ////////////////
- this_aArg = aArg[i]; // For performance and convenience.
- this_aArgMap = aArgMap ? aArgMap[i] : NULL; // Same.
- ArgStruct &this_new_arg = new_arg[i]; // Same.
- this_new_arg.is_expression = false; // Set default early, for maintainability.
- if (aActionType == ACT_TRANSFORM)
- {
- if (i == 1) // The second parameter (since the first is the OutputVar).
- // Note that the following might return TRANS_CMD_INVALID just because the sub-command
- // is containined in a variable reference. That is why TRANS_CMD_INVALID does not
- // produce an error at this stage, but only later when the line has been constructed
- // far enough to call ArgHasDeref():
- trans_cmd = Line::ConvertTransformCmd(this_aArg);
- // The value of trans_cmd is also used by the syntax checker further below.
- else if (i > 1) // i.e. Not the first param, only the third and fourth, which currently are either both numeric or both non-numeric for all cases.
- {
- switch(trans_cmd)
- {
- TRANSFORM_NON_EXPRESSION_CASES
- default:
- // For all other sub-commands, Arg #3 and #4 are expression-capable and will be made so
- // if they pass the following check:
- this_new_arg.is_expression = LegacyArgIsExpression(this_aArg, this_aArgMap);
- }
- }
- }
- // Before allocating memory for this Arg's text, first check if it's a pure
- // variable. If it is, we store it differently (and there's no need to resolve
- // escape sequences in these cases, since var names can't contain them):
- if (aActionType == ACT_LOOP && i == 1 && aArg[0] && !stricmp(aArg[0], "Parse")) // Verified.
- // i==1 --> 2nd arg's type is based on 1st arg's text.
- this_new_arg.type = ARG_TYPE_INPUT_VAR;
- else
- this_new_arg.type = Line::ArgIsVar(aActionType, i);
- // Since some vars are optional, the below allows them all to be blank or
- // not present in the arg list. If a mandatory var is blank at this stage,
- // it's okay because all mandatory args are validated to be non-blank elsewhere:
- if (this_new_arg.type != ARG_TYPE_NORMAL)
- {
- if (!*this_aArg)
- // An optional input or output variable has been omitted, so indicate
- // that this arg is not a variable, just a normal empty arg. Functions
- // such as ListLines() rely on this having been done because they assume,
- // for performance reasons, that args marked as variables really are
- // variables. In addition, ExpandArgs() relies on this having been done
- // as does the load-time validation for ACT_DRIVEGET:
- this_new_arg.type = ARG_TYPE_NORMAL;
- else
- {
- // Does this input or output variable contain a dereference? If so, it must
- // be resolved at runtime (to support arrays, etc.).
- // Find the first non-escaped dereference symbol:
- for (j = 0; this_aArg[j] && (this_aArg[j] != g_DerefChar || (this_aArgMap && this_aArgMap[j])); ++j);
- if (!this_aArg[j])
- {
- // A non-escaped deref symbol wasn't found, therefore this variable does not
- // appear to be something that must be resolved dynamically at runtime.
- if ( !(target_var = FindOrAddVar(this_aArg)) )
- return FAIL; // The above already displayed the error.
- // If this action type is something that modifies the contents of the var, ensure the var
- // isn't a special/reserved one:
- if (this_new_arg.type == ARG_TYPE_OUTPUT_VAR && VAR_IS_READONLY(*target_var))
- return ScriptError(ERR_VAR_IS_READONLY, this_aArg);
- // Rather than removing this arg from the list altogether -- which would distrub
- // the ordering and hurt the maintainability of the code -- the next best thing
- // in terms of saving memory is to store an empty string in place of the arg's
- // text if that arg is a pure variable (i.e. since the name of the variable is already
- // stored in the Var object, we don't need to store it twice):
- this_new_arg.text = "";
- this_new_arg.length = 0;
- this_new_arg.deref = (DerefType *)target_var;
- continue;
- }
- // else continue on to the below so that this input or output variable name's dynamic part
- // (e.g. array%i%) can be partially resolved.
- }
- }
- else // this_new_arg.type == ARG_TYPE_NORMAL (excluding those input/output_vars that were converted to normal because they were blank, above).
- {
- // v1.0.29: Allow expressions in any parameter that starts with % followed by a space
- // or tab. This should be unambiguous because spaces and tabs are illegal in variable names.
- // Since there's little if any benefit to allowing input and output variables to be
- // dynamically built via expression, for now it is disallowed. If ever allow it,
- // need to review other sections to ensure they will tolerate it. Also, the following
- // would probably need revision to get it to be detected as an output-variable:
- // % Array%i% = value
- if (*this_aArg == g_DerefChar && !(this_aArgMap && *this_aArgMap) // It's a non-literal deref character.
- && IS_SPACE_OR_TAB(this_aArg[1])) // Followed by a space or tab.
- {
- this_new_arg.is_expression = true;
- // Omit the percent sign and the space after it from further consideration.
- this_aArg += 2;
- if (this_aArgMap)
- this_aArgMap += 2;
- // ACT_ASSIGN isn't capable of dealing with expressions because ExecUntil() does not
- // call ExpandArgs() automatically for it. Thus its function, PerformAssign(), would
- // not be given the expanded result of the expression.
- if (aActionType == ACT_ASSIGN)
- aActionType = ACT_ASSIGNEXPR;
- }
- }
- // Below will set the new var to be the constant empty string if the
- // source var is NULL or blank.
- // e.g. If WinTitle is unspecified (blank), but WinText is non-blank.
- // Using empty string is much safer than NULL because these args
- // will be frequently accessed by various functions that might
- // not be equipped to handle NULLs. Rather than having to remember
- // to check for NULL in every such case, setting it to a constant
- // empty string here should make things a lot more maintainable
- // and less bug-prone. If there's ever a need for the contents
- // of this_new_arg to be modifiable (perhaps some obscure API calls require
- // modifiable strings?) can malloc a single-char to contain the empty string.
- //
- // So that it can be passed to Malloc(), first update the length to match what the text will be
- // (if the alloc fails, an inaccurate length won't matter because it's an program-abort situation).
- // The length must fit into a WORD, which it will since each arg is literal text from a script's line,
- // which is limited to LINE_SIZE. The length member was added in v1.0.44.14 to boost runtime performance.
- this_new_arg.length = (WORD)strlen(this_aArg);
- if ( !(this_new_arg.text = SimpleHeap::Malloc(this_aArg, this_new_arg.length)) )
- return FAIL; // It already displayed the error for us.
- ////////////////////////////////////////////////////
- // Build the list of dereferenced vars for this arg.
- ////////////////////////////////////////////////////
- // Now that any escaped g_DerefChars have been marked, scan new_arg.text to
- // determine where the variable dereferences are (if any). In addition to helping
- // runtime performance, this also serves to validate the script at load-time
- // so that some errors can be caught early. Note: this_new_arg.text is scanned rather
- // than this_aArg because we want to establish pointers to the correct area of
- // memory:
- deref_count = 0; // Init for each arg.
- if (np = g_act[aActionType].NumericParams) // This command has at least one numeric parameter.
- {
- // As of v1.0.25, pure numeric parameters can optionally be numeric expressions, so check for that:
- i_plus_one = i + 1;
- for (; *np; ++np)
- {
- if (*np == i_plus_one) // This arg is enforced to be purely numeric.
- {
- if (aActionType == ACT_WINMOVE)
- {
- if (i > 1)
- {
- // i indicates this is Arg #3 or beyond, which is one of the args that is
- // either the word "default" or a number/expression.
- if (!stricmp(this_new_arg.text, "default")) // It's not an expression.
- break; // The loop is over because this arg was found in the list.
- }
- else // This is the first or second arg, which are title/text vs. X/Y when aArgc > 2.
- if (aArgc > 2) // Title/text are not numeric/expressions.
- break; // The loop is over because this arg was found in the list.
- }
- // Otherwise, it might be an expression so do the final checks.
- // Override the original false default of is_expression unless an exception applies.
- // Since ACT_ASSIGNEXPR and ACT_WHILE aren't legacy commands, don't call
- // LegacyArgIsExpression() for them because that would cause things like x:=%y% and
- // "while %x%" to behave the same as x:=y and "while x:, which would be inconsistent
- // with how expressions are supposed to work. ACT_RETURN should have been excluded
- // too; but it was left out for so long that it was thought best to document and keep
- // the unexpected behavior of "return %x%".
- // For other commands, if any telltale character is present it's definitely an
- // expression because this is an arg that's marked as a number-or-expression.
- // So telltales avoid the need for the complex check further below.
- if (aActionType == ACT_ASSIGNEXPR || aActionType == ACT_WHILE // See above.
- || StrChrAny(this_new_arg.text, EXPR_TELLTALES)) // See above.
- this_new_arg.is_expression = true;
- else
- this_new_arg.is_expression = LegacyArgIsExpression(this_new_arg.text, this_aArgMap);
- break; // The loop is over if this arg is found in the list of mandatory-numeric args.
- } // i is a mandatory-numeric arg
- } // for each mandatory-numeric arg of this command, see if this arg matches its number.
- } // this command has a list of mandatory numeric-args.
- // To help runtime performance, the below changes an ACT_ASSIGNEXPR, ACT_TRANSFORM, and
- // perhaps others in the future, to become non-expressions if they contain only a single
- // numeric literal (or are entirely blank). At runtime, such args are expanded normally
- // rather than having to run them through the expression evaluator:
- if (this_new_arg.is_expression && IsPureNumeric(this_new_arg.text, true, true, true))
- this_new_arg.is_expression = false;
- if (this_new_arg.is_expression)
- {
- // Ensure parentheses are balanced:
- for (cp = this_new_arg.text, in_quotes = false, open_parens = 0; *cp; ++cp)
- {
- switch (*cp)
- {
- // The simple method below is sufficient for our purpose even if a quoted string contains
- // pairs of double-quotes to represent a single literal quote, e.g. "quoted ""word""".
- // In other words, it relies on the fact that there must be an even number of quotes
- // inside any mandatory-numeric arg that is an expression such as x=="red,blue"
- case '"':
- in_quotes = !in_quotes;
- break;
- case '(':
- if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
- ++open_parens;
- break;
- case ')':
- if (!in_quotes)
- {
- if (!open_parens)
- return ScriptError(ERR_MISSING_OPEN_PAREN, cp); // And indicate cp as the exact spot.
- --open_parens;
- }
- break;
- }
- }
- if (open_parens) // At least one '(' is never closed.
- return ScriptError(ERR_MISSING_CLOSE_PAREN, this_new_arg.text);
- #define ERR_EXP_ILLEGAL_CHAR "The leftmost character above is illegal in an expression." // "above" refers to the layout of the error dialog.
- // ParseDerefs() won't consider escaped percent signs to be illegal, but in this case
- // they should be since they have no meaning in expressions. UPDATE for v1.0.44.11: The following
- // is now commented out because it causes false positives (and fixing that probably isn't worth the
- // performance & code size). Specifically, the section below reports an error for escaped delimiters
- // inside quotes such as x := "`%". More importantly, it defeats the continuation section's %
- // option; for example:
- // MsgBox %
- // (% ; <<< This option here is defeated because it causes % to be replaced with `% at an early stage.
- // "%"
- // )
- //if (this_aArgMap) // This arg has an arg map indicating which chars are escaped/literal vs. normal.
- // for (j = 0; this_new_arg.text[j]; ++j)
- // if (this_aArgMap[j] && this_new_arg.text[j] == g_DerefChar)
- // return ScriptError(ERR_EXP_ILLEGAL_CHAR, this_new_arg.text + j);
- // Resolve all operands (that aren't numbers) into variable references. Doing this here at
- // load-time greatly improves runtime performance, especially for scripts that have a lot
- // of variables.
- for (op_begin = this_new_arg.text; *op_begin; op_begin = op_end)
- {
- if (*op_begin == '.' && op_begin[1] == '=') // v1.0.46.01: Support .=, but not any use of '.' because that is reserved as a struct/member operator.
- op_begin += 2;
- for (; *op_begin && strchr(EXPR_OPERAND_TERMINATORS, *op_begin); ++op_begin); // Skip over whitespace, operators, and parentheses.
- if (!*op_begin) // The above loop reached the end of the string: No operands remaining.
- break;
- // Now op_begin is the start of an operand, which might be a variable reference, a numeric
- // literal, or a string literal. If it's a string literal, it is left as-is:
- if (*op_begin == '"')
- {
- // Find the end of this string literal, noting that a pair of double quotes is
- // a literal double quote inside the string:
- for (op_end = op_begin + 1;; ++op_end)
- {
- if (!*op_end)
- return ScriptError(ERR_MISSING_CLOSE_QUOTE, op_begin);
- if (*op_end == '"') // If not followed immediately by another, this is the end of it.
- {
- ++op_end;
- if (*op_end != '"') // String terminator or some non-quote character.
- break; // The previous char is the ending quote.
- //else a pair of quotes, which resolves to a single literal quote.
- // This pair is skipped over and the loop continues until the real end-quote is found.
- }
- }
- // op_end is now set correctly to allow the outer loop to continue.
- continue; // Ignore this literal string, letting the runtime expression parser recognize it.
- }
-
- // Find the end of this operand (if *op_end is '\0', strchr() will find that too):
- for (op_end = op_begin + 1; !strchr(EXPR_OPERAND_TERMINATORS, *op_end); ++op_end); // Find first whitespace, operator, or paren.
- if (*op_end == '=' && op_end[-1] == '.') // v1.0.46.01: Support .=, but not any use of '.' because that is reserved as a struct/member operator.
- --op_end;
- // Now op_end marks the end of this operand. The end might be the zero terminator, an operator, etc.
- // Must be done only after op_end has been set above (since loop uses op_end):
- if (*op_begin == '.' && strchr(" \t=", op_begin[1])) // If true, it can't be something like "5." because the dot inside would never be parsed separately in that case. Also allows ".=" operator.
- continue;
- //else any '.' not followed by a space, tab, or '=' is likely a number without a leading zero,
- // so continue on below to process it.
- operand_length = op_end - op_begin;
- // Check if it's AND/OR/NOT:
- if (operand_length < 4 && operand_length > 1) // Ordered for short-circuit performance.
- {
- if (operand_length == 2)
- {
- if ((*op_begin == 'o' || *op_begin == 'O') && (op_begin[1] == 'r' || op_begin[1] == 'R'))
- { // "OR" was found.
- op_begin[0] = '|'; // v1.0.45: Transform into easier-to-parse symbols for improved
- op_begin[1] = '|'; // runtime performance and reduced code size. v1.0.48: It no longer helps runtime performance, but it's kept because changing moving it to ExpressionToPostfix() isn't likely to have much benefit.
- continue;
- }
- }
- else // operand_length must be 3
- {
- switch (*op_begin)
- {
- case 'a':
- case 'A':
- if ( (op_begin[1] == 'n' || op_begin[1] == 'N') // Relies on short-circuit boolean order.
- && (op_begin[2] == 'd' || op_begin[2] == 'D') )
- { // "AND" was found.
- op_begin[0] = '&'; // v1.0.45: Transform into easier-to-parse symbols for
- op_begin[1] = '&'; // improved runtime performance and reduced code size. v1.0.48: It no longer helps runtime performance, but it's kept because changing moving it to ExpressionToPostfix() isn't likely to have much benefit.
- op_begin[2] = ' '; // A space is used lieu of the complexity of the below.
- // Above seems better than below even though below would make it look a little
- // nicer in ListLines. BELOW CAN'T WORK because this_new_arg.deref[] can contain
- // offsets that would also need to be adjusted:
- //memmove(op_begin + 2, op_begin + 3, strlen(op_begin+3)+1 ... or some expression involving this_new_arg.length this_new_arg.text);
- //--this_new_arg.length;
- //--op_end; // Ensure op_end is set up properly for the for-loop's post-iteration action.
- continue;
- }
- break;
- case 'n': // v1.0.45: Unlike "AND" and "OR" above, this one is not given a substitute
- case 'N': // because it's not the same as the "!" operator. See SYM_LOWNOT for comments.
- if ( (op_begin[1] == 'o' || op_begin[1] == 'O') // Relies on short-circuit boolean order.
- && (op_begin[2] == 't' || op_begin[2] == 'T') )
- continue; // "NOT" was found.
- break;
- }
- }
- } // End of check for AND/OR/NOT.
- // Temporarily terminate, which avoids at least the below issue:
- // Two or more extremely long var names together could exceed MAX_VAR_NAME_LENGTH
- // e.g. LongVar%LongVar2% would be too long to store in a buffer of size MAX_VAR_NAME_LENGTH.
- // This seems pretty darn unlikely, but perhaps doubling it would be okay.
- // UPDATE: Above is now not an issue since caller's string is temporarily terminated rather
- // than making a copy of it.
- orig_char = *op_end;
- *op_end = '\0';
- // Illegal characters are legal when enclosed in double quotes. So the following is
- // done only after the above has ensured this operand is not one enclosed entirely in
- // double quotes.
- // The following characters are either illegal in expressions or reserved for future use.
- // Rather than forbidding g_delimiter and g_DerefChar, it seems best to assume they are at
- // their default values for this purpose. Otherwise, if g_delimiter is an operator, that
- // operator would then become impossible inside the expression.
- if (cp = StrChrAny(op_begin, EXPR_ILLEGAL_CHARS))
- return ScriptError(ERR_EXP_ILLEGAL_CHAR, cp);
- // Below takes care of recognizing hexadecimal integers, which avoids the 'x' character
- // inside of something like 0xFF from being detected as the name of a variable:
- if ( !IsPureNumeric(op_begin, true, false, true) // Not a numeric literal...
- && !(*op_begin == '?' && !op_begin[1]) ) // ...and not an isolated '?' operator. Relies on short-circuit boolean order.
- {
- is_function = (orig_char == '(');
- // This operand must be a variable/function reference or string literal, otherwise it's
- // a syntax error.
- // Check explicitly for derefs since the vast majority don't have any, and this
- // avoids the function call in those cases:
- if (strchr(op_begin, g_DerefChar)) // This operand contains at least one double dereference.
- {
- // v1.0.47.06: Dynamic function calls are now supported.
- //if (is_function)
- // return ScriptError("Dynamic function calls are not supported.", op_begin);
- int first_deref = deref_count;
- // The percent-sign derefs are parsed and added to the deref array at this stage (on a
- // per-operand basis) rather than all at once for the entire arg because
- // the deref array must contain both percent-sign derefs and non-%-derefs interspersed
- // and ordered according to their physical position inside the arg, but ParseDerefs
- // only handles percent-sign derefs, not expresion derefs like x+y. In the following
- // example, the order of derefs must be x,i,y: if (x = Array%i% and y = 3)
- if (!ParseDerefs(op_begin, this_aArgMap ? this_aArgMap + (op_begin - this_new_arg.text) : NULL
- , deref, deref_count))
- return FAIL; // It already displayed the error. No need to undo temp. termination.
- // And now leave this operand "raw" so that it will later be dereferenced again.
- // In the following example, i made into a deref but the result (Array33) must be
- // dereferenced during a second stage at runtime: if (x = Array%i%).
- if (is_function) // Dynamic function call.
- {
- int param_count = 0;
- // Determine how many parameters there are.
- cp = omit_leading_whitespace(op_end + 1);
- if (*cp != ')')
- {
- int open_parens;
- bool in_quote = false;
- for (++param_count, open_parens = 1; *cp && open_parens; ++cp)
- {
- if (*cp == '"')
- in_quote = !in_quote;
- if (in_quote)
- continue;
- switch (*cp)
- {
- case '(':
- ++open_parens;
- break;
- case ')':
- --open_parens;
- break;
- case ',':
- if (open_parens == 1)
- ++param_count;
- break;
- }
- }
- }
- // Store param_count in the first deref. This will be picked up by the expression
- // infix processing code.
- deref[first_deref].param_count = param_count; // This is done only for dynamic function calls because non-dynamic ones have their param_count set by PreparseBlocks().
- }
- }
- else // This operand is a variable name or function name (single deref).
- {
- #define TOO_MANY_REFS "Too many var/func refs." // Short msg since so rare.
- if (deref_count >= MAX_DEREFS_PER_ARG)
- return ScriptError(TOO_MANY_REFS, op_begin); // Indicate which operand it ran out of space at.
- // Store the deref's starting location, even for functions (leave it set to the start
- // of the function's name for use when doing error reporting at other stages -- i.e.
- // don't set it to the address of the first param or closing-paren-if-no-params):
- deref[deref_count].marker = op_begin;
- deref[deref_count].length = (DerefLengthType)operand_length;
- if (deref[deref_count].is_function = is_function) // It's a function not a variable.
- // Set to NULL to catch bugs. It must and will be filled in at a later stage
- // because the setting of each function's mJumpToLine relies upon the fact that
- // functions are added to the linked list only upon being formally defined
- // so that the most recently defined function is always last in the linked
- // list, awaiting its mJumpToLine that will appear beneath it.
- deref[deref_count].func = NULL;
- else // It's a variable (or a scientific-notation literal) rather than a function.
- {
- if (toupper(op_end[-1]) == 'E' && (orig_char == '+' || orig_char == '-') // Listed first for short-circuit performance with the below.
- && strchr(op_begin, '.')) // v1.0.46.11: This item appears to be a scientific-notation literal with the OPTIONAL +/- sign PRESENT on the exponent (e.g. 1.0e+001), so check that before checking if it's a variable name.
- {
- *op_end = orig_char; // Undo the temporary termination.
- do // Skip over the sign and its exponent; e.g. the "+1" in "1.0e+1". There must be a sign in this particular sci-notation number or we would never have arrived here.
- ++op_end;
- while (*op_end >= '0' && *op_end <= '9'); // Avoid isdigit() because it sometimes causes a debug assertion failure at: (unsigned)(c + 1) <= 256 (probably only in debug mode), and maybe only when bad data got in it due to some other bug.
- // No need to do the following because a number can't validly be followed by the ".=" operator:
- //if (*op_end == '=' && op_end[-1] == '.') // v1.0.46.01: Support .=, but not any use of '.' because that is reserved as a struct/member operator.
- // --op_end;
- continue; // Pure number, which doesn't need any processing at this stage.
- }
- // Since above didn't "continue", treat this item as a variable name:
- if ( !(deref[deref_count].var = FindOrAddVar(op_begin, operand_length)) )
- return FAIL; // The called function already displayed the error.
- }
- ++deref_count; // Since above didn't "continue" or "return".
- }
- }
- //else purely numeric or '?'. Do nothing since pure numbers and '?' don't need any
- // processing at this stage.
- *op_end = orig_char; // Undo the temporary termination.
- } // expression pre-parsing loop.
- // Now that the derefs have all been recognized above, simplify any special cases --
- // such as single isolated derefs -- to enhance runtime performance.
- //
- // There used to be a section here that translated each expression that consisted
- // solely of a quoted/literal string into a non-expression (except Post/SendMessage).
- // However, that is no longer appropriate for ACT_ASSIGNEXPR (which was the main
- // beneficiary) because an optimization further below would wrongly apply
- // SetFormat to the assigning of a quoted/literal string like Var:="55".
- // Benchmarks show that performance of assigning quoted literal strings is only
- // slightly slower when is_expression==true; also, the savings in code size and the
- // fact that the translation made ListLines inaccurate (due to the omitted quotes)
- // seem to support getting rid of that section.
- //
- // Make things like "Sleep Var" and "Var := X" into non-expressions. At runtime,
- // such args are expanded normally rather than having to run them through the
- // expression evaluator. A simple test script shows that this one change can
- // double the runtime performance of certain commands such as EnvAdd:
- // Below is somewhat obsolete but kept for reference:
- // This policy is basically saying that expressions are allowed to evaluate to strings
- // everywhere appropriate, but that at the moment the only appropriate place is x := y
- // because all other expressions should resolve to a numeric value by virtue of the fact
- // that they *are* numeric parameters. ValidateName() serves to eliminate cases where
- // a single deref is accompanied by literal numbers, strings, or operators, e.g.
- // Var := X + 1 ... Var := Var2 "xyz" ... Var := -Var2
- if (deref_count == 1 && Var::ValidateName(this_new_arg.text, false, DISPLAY_NO_ERROR)) // Single isolated deref.
- {
- // ACT_WHILE performs less than 4% faster as a non-expression in these cases, and keeping
- // it as an expression avoids an extra check in a performance-sensitive spot of ExpandArgs
- // (near mActionType <= ACT_LAST_OPTIMIZED_IF).
- if (aActionType != ACT_WHILE) // If it is ACT_WHILE, it would be something like "while x" in this case. Keep those as expressions for the reason above.
- this_new_arg.is_expression = false; // In addition to being an optimization, doing this might also be necessary for things like "Var := ClipboardAll" to work properly.
- // But if aActionType is ACT_ASSIGNEXPR, it's left as ACT_ASSIGNEXPR vs. ACT_ASSIGN
- // because it might be necessary to avoid having AutoTrim take effect for := (which
- // it never should). In addition, ACT_ASSIGNEXPR probably performs better than
- // ACT_ASSIGN when is_expression==false.
- }
- else if (deref_count && !StrChrAny(this_new_arg.text, EXPR_OPERAND_TERMINATORS)) // No spaces, tabs, etc.
- {
- // Adjust if any of the following special cases apply:
- // x := y -> Mark as non-expression (after expression-parsing set up parsed derefs above)
- // so that the y deref will be only a single-deref to be directly stored in x.
- // This is done in case y contains a string. Since an expression normally
- // evaluates to a number, without this workaround, x := y would be useless for
- // a simple assignment of a string. This case is handled above.
- // x := %y% -> Mark the right-side arg as an input variable so that it will be doubly
- // dereferenced, similar to StringTrimRight, Out, %y%, 0. This seems best
- // because there would be little or no point to having it behave identically
- // to x := y. It might even be confusing in light of the next case below.
- // CASE #3:
- // x := Literal%y%Literal%z%Literal -> Same as above. This is done mostly to support
- // retrieving array elements whose contents are *non-numeric* without having to use
- // something like StringTrimRight.
-
- // Now we know it has at least one deref. But if any operators or other characters disallowed
- // in variables are present, it all three cases are disqualified and kept as expressions.
- // This check is necessary for all three cases:
- // No operators of any kind anywhere. Not even +/- prefix, since those imply a numeric
- // expression. No chars illegal in var names except the percent signs themselves,
- // e.g. *no* whitespace.
- // Also, the first deref (indeed, all of them) should point to a percent sign, since
- // there should not be any way for non-percent derefs to get mixed in with cases
- // 2 or 3.
- if (!deref[0].is_function && *deref[0].marker == g_DerefChar // This appears to be case #2 or #3.
- && aActionType != ACT_WHILE) // Nearly doubles the speed of "while %x%" and "while Array%i%" to leave WHILE as an expression. But y:=%x% and y:=Array%i% are about the same speed either way, and "if %x%" never reaches this point because for compatibility(?), it's the same as "if x".
- {
- // The comment below is probably obsolete -- and perhaps so is this entire optimization
- // because expressions are faster now. But in case it's necessary for anything related
- // to backward compatibility, it's kept (it may also reduce memory utilization a little
- // because it avoids making simple things into expressions, which require extra memory).
- // OLD: Set it up so that x:=Array%i% behaves the same as StringTrimRight, Out, Array%i%, 0.
- this_new_arg.is_expression = false;
- this_new_arg.type = ARG_TYPE_INPUT_VAR;
- }
- }
- } // if (this_new_arg.is_expression)
- else // this arg does not contain an expression.
- if (!ParseDerefs(this_new_arg.text, this_aArgMap, deref, deref_count))
- return FAIL; // It already displayed the error.
- //////////////////////////////////////////////////////////////
- // Allocate mem for this arg's list of dereferenced variables.
- //////////////////////////////////////////////////////////////
- if (deref_count)
- {
- // +1 for the "NULL-item" terminator:
- if ( !(this_new_arg.deref = (DerefType *)SimpleHeap::Malloc((deref_count + 1) * sizeof(DerefType))) )
- return ScriptError(ERR_OUTOFMEM);
- memcpy(this_new_arg.deref, deref, deref_count * sizeof(DerefType));
- // Terminate the list of derefs with a deref that has a NULL marker:
- this_new_arg.deref[deref_count].marker = NULL;
- }
- else
- this_new_arg.deref = NULL;
- } // for each arg.
- } // there are more than zero args.
- //////////////////////////////////////////////////////////////////////////////////////
- // Now the above has allocated some dynamic memory, the pointers to which we turn over
- // to Line's constructor so that they can be anchored to the new line.
- //////////////////////////////////////////////////////////////////////////////////////
- Line *the_new_line = new Line(mCurrFileIndex, mCombinedLineNumber, aActionType, new_arg, aArgc);
- if (!the_new_line)
- return ScriptError(ERR_OUTOFMEM);
- Line &line = *the_new_line; // For performance and convenience.
- line.mPrevLine = mLastLine; // Whether NULL or not.
- if (mFirstLine == NULL)
- mFirstLine = the_new_line;
- else
- mLastLine->mNextLine = the_new_line;
- // This must be done after the above:
- mLastLine = the_new_line;
- mCurrLine = the_new_line; // To help error reporting.
- ///////////////////////////////////////////////////////////////////
- // Do any post-add validation & handling for specific action types.
- ///////////////////////////////////////////////////////////////////
- #ifndef AUTOHOTKEYSC // For v1.0.35.01, some syntax checking is removed in compiled scripts to reduce their size.
- int value; // For temp use during validation.
- double value_float;
- SYSTEMTIME st; // same.
- #endif
- // v1.0.38: The following should help reduce code size, and for some commands helps load-time
- // performance by avoiding multiple resolutions of a given macro:
- char *new_raw_arg1 = NEW_RAW_ARG1;
- char *new_raw_arg2 = NEW_RAW_ARG2;
- char *new_raw_arg3 = NEW_RAW_ARG3;
- char *new_raw_arg4 = NEW_RAW_ARG4;
- switch(aActionType)
- {
- // Fix for v1.0.35.02:
- // THESE FIRST FEW CASES MUST EXIST IN BOTH SELF-CONTAINED AND NORMAL VERSION since they alter the
- // attributes/members of some types of lines:
- case ACT_SUB:
- if (aArgc < 2) // Validate at loadtime so that at runtime, DETERMINE_NUMERIC_TYPES and ARG2_AS_INT64 don't have to check that mArgc > 1.
- return ScriptError(ERR_PARAM2_REQUIRED);
- // ** DON'T BREAK; FALL INTO NEXT SECTION **
- case ACT_ADD: // ************ OR IT FELL INTO THIS SECTION FROM ABOVE ************
- case ACT_MULT:
- case ACT_DIV:
- #ifndef AUTOHOTKEYSC // For v1.0.35.01, some syntax checking is removed in compiled scripts to reduce their size.
- if (aArgc > 2) // Then this is ACT_ADD OR ACT_SUB with a 3rd parameter (TimeUnits)
- {
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- if (!strchr("SMHD", toupper(*new_raw_arg3))) // (S)econds, (M)inutes, (H)ours, or (D)ays
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- if (aActionType == ACT_SUB && *new_raw_arg2 && !line.ArgHasDeref(2))
- if (!YYYYMMDDToSystemTime(new_raw_arg2, st, true))
- return ScriptError(ERR_INVALID_DATETIME, new_raw_arg2);
- new_arg[1].postfix = NULL; // It's necessary to indicate that there is no cached binary number for arg #2 in the 3-arg mode of ACT_ADD due to runtime logic that checks it. For the others, this helps maintainability.
- break; // Don't allow processing to continue. Other sections below rely on this.
- }
- if (aActionType == ACT_DIV && !line.ArgHasDeref(2) && !new_arg[1].is_expression) // i.e. don't validate the following until runtime:
- if (!ATOF(new_raw_arg2)) // x/=y ... x/=(4/4)/4 (v1.0.46.01: added is_expression check for expressions with no variables or function-calls).
- return ScriptError(ERR_DIVIDEBYZERO, new_raw_arg2);
- // ** DON'T BREAK; FALL INTO NEXT SECTION **
- #endif
- case ACT_ASSIGN: // **** OR IT FELL INTO THIS SECTION FROM ABOVE ****
- case ACT_ASSIGNEXPR:
- case ACT_IFEQUAL:
- case ACT_IFNOTEQUAL:
- case ACT_IFGREATER:
- case ACT_IFGREATEROREQUAL:
- case ACT_IFLESS:
- case ACT_IFLESSOREQUAL:
- case ACT_IFBETWEEN:
- case ACT_IFNOTBETWEEN:
- int arg_index;
- for (arg_index = 1; arg_index < aArgc; ++arg_index) // Up to two iterations: arg #2 and arg#3 (for If[Not]Between)).
- {
- if ( *new_arg[arg_index].text && !line.ArgHasDeref(arg_index+1)
- && !new_arg[arg_index].is_expression // Expressions don't make sense for this, plus they need their postfix member for other purposes.
- && IsPureNumeric(new_arg[arg_index].text, true, false, false) // aAllowImpure==false even for ACT_ADD/SUB/MULT/DIV because those would see almost all impure *LITERAL* numbers like 123abc as variables (too rare anyway). Check for purity to rule out floats and expressions consisting only of literals such as 1+2 (in case they can ever be encountered here).
- && !((aActionType == ACT_ASSIGN || aActionType == ACT_ASSIGNEXPR) // Only these need extra checking because the display format of the number doesn't matter for ADD/SUB/IFEQUAL/IFGREATER/etc. because they treat anything that looks like a number (any format) as a pure number.
- && (
- *new_raw_arg2 == '0' || *new_raw_arg2 == '+' // Assign hex or any unusually-formatted integers the old way so that the format is retained in case its important to the operation of the script (e.g. x:="005", x:=005, x:="0x5", x:="+5", x:=+5).
- || new_arg[1].length > 18 // See below.
- // Integers that are too long are probably intended to be a series of characters/digits,
- // so assign them the old way to keep all of the digits. Fix for v1.0.48.01: Reduced the
- // limit from MAX_INTEGER_LENGTH (20) to 18 so that the assignment (:= and =) of integers
- // that are 19 or 20 digits long work as they did prior to v1.0.48 (some of such integers
- // would overflow a signed 64-bit value, so keep all of them as strings).
- //
- // The following can't happen anymore because x:="string" is no longer translated
- // into is_expression==false. There are some reasons given in a section higher above:
- //|| IS_SPACE_OR_TAB(new_raw_arg2[new_arg[1].length-1]) // Trailing whitespace, which can happen from something like x:="abc ".
- //|| IS_SPACE_OR_TAB(*new_raw_arg2) // This can happen via translation of x:=" abc " to x:= abc at an earlier stage.
- // Any LITERAL whitespace around a LITERAL number has always been ignored/omitted,
- // so storing binary integers for things like "x = 1" and "x := 1" should behave
- // as before, with the exception of "SetFormat, Integer, Hex", which will now be obeyed
- // by such assignments when it wasn't before.
- )) )
- {
- if ( !(new_arg[arg_index].postfix = (ExprTokenType *)SimpleHeap::Malloc(sizeof(__int64))) )
- return ScriptError(ERR_OUTOFMEM);
- *(__int64 *)new_arg[arg_index].postfix = ATOI64(new_arg[arg_index].text);
- }
- else
- new_arg[arg_index].postfix = NULL; // Indicate that there is no cached binary number.
- }
- break;
- case ACT_LOOP:
- // If possible, determine the type of loop so that the preparser can better
- // validate some things:
- switch (aArgc)
- {
- case 0:
- line.mAttribute = ATTR_LOOP_NORMAL;
- break;
- case 1: // With only 1 arg, it must be a normal loop, file-pattern loop, or registry loop.
- // v1.0.43.07: Added check for new_arg[0].is_expression so that an expression without any variables
- // it it works (e.g. Loop % 1+1):
- if (line.ArgHasDeref(1) || new_arg[0].is_expression) // Impossible to know now what type of loop (only at runtime).
- line.mAttribute = ATTR_LOOP_UNKNOWN;
- else
- {
- if (IsPureNumeric(new_raw_arg1, false))
- line.mAttribute = ATTR_LOOP_NORMAL;
- else
- line.mAttribute = line.RegConvertRootKey(new_raw_arg1) ? ATTR_LOOP_REG : ATTR_LOOP_FILEPATTERN;
- }
- break;
- default: // has 2 or more args.
- if (line.ArgHasDeref(1)) // Impossible to know now what type of loop (only at runtime).
- line.mAttribute = ATTR_LOOP_UNKNOWN;
- else if (!stricmp(new_raw_arg1, "Read"))
- line.mAttribute = ATTR_LOOP_READ_FILE;
- else if (!stricmp(new_raw_arg1, "Parse"))
- line.mAttribute = ATTR_LOOP_PARSE;
- else // the 1st arg can either be a Root Key or a File Pattern, depending on the type of loop.
- {
- line.mAttribute = line.RegConvertRootKey(new_raw_arg1) ? ATTR_LOOP_REG : ATTR_LOOP_FILEPATTERN;
- if (line.mAttribute == ATTR_LOOP_FILEPATTERN)
- {
- // Validate whatever we can rather than waiting for runtime validation:
- if (!line.ArgHasDeref(2) && Line::ConvertLoopMode(new_raw_arg2) == FILE_LOOP_INVALID)
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- if (strlen(new_raw_arg3) > 1 || (*new_raw_arg3 != '0' && *new_raw_arg3 != '1'))
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- }
- else // Registry loop.
- {
- if (aArgc > 2 && !line.ArgHasDeref(3) && Line::ConvertLoopMode(new_raw_arg3) == FILE_LOOP_INVALID)
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- if (*new_raw_arg4 && !line.ArgHasDeref(4))
- if (strlen(new_raw_arg4) > 1 || (*new_raw_arg4 != '0' && *new_raw_arg4 != '1'))
- return ScriptError(ERR_PARAM4_INVALID, new_raw_arg4);
- }
- }
- }
- break; // Outer switch().
- case ACT_REPEAT: // These types of loops are always "NORMAL".
- line.mAttribute = ATTR_LOOP_NORMAL;
- break;
- case ACT_WHILE: // Lexikos: ATTR_LOOP_WHILE is used to differentiate ACT_WHILE from ACT_LOOP, allowing code to be shared.
- line.mAttribute = ATTR_LOOP_WHILE;
- break;
- // This one alters g_persistent so is present in its entirety (for simplicity) in both SC an non-SC version.
- case ACT_GUI:
- // By design, scripts that use the GUI cmd anywhere are persistent. Doing this here
- // also allows WinMain() to later detect whether this script should become #SingleInstance.
- // Note: Don't directly change g_AllowOnlyOneInstance here in case the remainder of the
- // script-loading process comes across any explicit uses of #SingleInstance, which would
- // override the default set here.
- g_persistent = true;
- #ifndef AUTOHOTKEYSC // For v1.0.35.01, some syntax checking is removed in compiled scripts to reduce their size.
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- GuiCommands gui_cmd = line.ConvertGuiCommand(new_raw_arg1);
- switch (gui_cmd)
- {
- case GUI_CMD_INVALID:
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- case GUI_CMD_ADD:
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- GuiControls control_type;
- if ( !(control_type = line.ConvertGuiControl(new_raw_arg2)) )
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- if (control_type == GUI_CONTROL_TREEVIEW && aArgc > 3) // Reserve it for future use such as a tab-indented continuation section that lists the tree hierarchy.
- return ScriptError(ERR_PARAM4_OMIT, new_raw_arg4);
- }
- break;
- case GUI_CMD_CANCEL:
- case GUI_CMD_MINIMIZE:
- case GUI_CMD_MAXIMIZE:
- case GUI_CMD_RESTORE:
- case GUI_CMD_DESTROY:
- case GUI_CMD_DEFAULT:
- case GUI_CMD_OPTIONS:
- if (aArgc > 1)
- return ScriptError("Parameter #2 and beyond should be omitted in this case.", new_raw_arg2);
- break;
- case GUI_CMD_SUBMIT:
- case GUI_CMD_MENU:
- case GUI_CMD_LISTVIEW:
- case GUI_CMD_TREEVIEW:
- case GUI_CMD_FLASH:
- if (aArgc > 2)
- return ScriptError("Parameter #3 and beyond should be omitted in this case.", new_raw_arg3);
- break;
- // No action for these since they have a varying number of optional params:
- //case GUI_CMD_SHOW:
- //case GUI_CMD_FONT:
- //case GUI_CMD_MARGIN:
- //case GUI_CMD_TAB:
- //case GUI_CMD_COLOR: No load-time param validation to avoid larger EXE size.
- }
- }
- #endif
- break;
- case ACT_GROUPADD:
- case ACT_GROUPACTIVATE:
- case ACT_GROUPDEACTIVATE:
- case ACT_GROUPCLOSE:
- // For all these, store a pointer to the group to help performance.
- // We create a non-existent group even for ACT_GROUPACTIVATE, ACT_GROUPDEACTIVATE
- // and ACT_GROUPCLOSE because we can't rely on the ACT_GROUPADD commands having
- // been parsed prior to them (e.g. something like "Gosub, DefineGroups" may appear
- // in the auto-execute portion of the script).
- if (!line.ArgHasDeref(1))
- if ( !(line.mAttribute = FindGroup(new_raw_arg1, true)) ) // Create-if-not-found so that performance is enhanced at runtime.
- return FAIL; // The above already displayed the error.
- if (aActionType == ACT_GROUPACTIVATE || aActionType == ACT_GROUPDEACTIVATE)
- {
- if (*new_raw_arg2 && !line.ArgHasDeref(2))
- if (strlen(new_raw_arg2) > 1 || toupper(*new_raw_arg2) != 'R')
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- }
- else if (aActionType == ACT_GROUPCLOSE)
- if (*new_raw_arg2 && !line.ArgHasDeref(2))
- if (strlen(new_raw_arg2) > 1 || !strchr("RA", toupper(*new_raw_arg2)))
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- break;
- case ACT_SETFORMAT: // Must be done even when AUTOHOTKEYSC is defined so that g_WriteCacheDisabledInt64/Double is properly updated.
- if (aArgc < 1)
- break;
- if (line.ArgHasDeref(1)) // Something like "SetFormat, %Var%, ..."
- {
- // For the following and other sections further below that disable the cache, can't wait until
- // runtime execution encounters SetFormat to disable caching because script might rely on the
- // *default* format being *immediately* written out prior to the script changing SetFormat at
- // some later time.
- g_WriteCacheDisabledInt64 = TRUE;
- g_WriteCacheDisabledDouble = TRUE;
- }
- else
- {
- if (!strnicmp(new_raw_arg1, "Float", 5))
- {
- if (stricmp(new_raw_arg1 + 5, "Fast")) // Cache is left enabled when the new FloatFast/IntegerFast mode is present.
- g_WriteCacheDisabledDouble = TRUE;
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- if (!IsPureNumeric(new_raw_arg2, true, false, true, true) // v1.0.46.11: Allow impure numbers to support scientific notation; e.g. 0.6e or 0.6E.
- || strlen(new_raw_arg2) >= sizeof(g->FormatFloat) - 2)
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- }
- }
- else if (!strnicmp(new_raw_arg1, "Integer", 7))
- {
- if (stricmp(new_raw_arg1 + 7, "Fast")) // Cache is left enabled when the new FloatFast/IntegerFast mode is present.
- g_WriteCacheDisabledInt64 = TRUE;
- if (aArgc > 1 && !line.ArgHasDeref(2) && toupper(*new_raw_arg2) != 'H' && toupper(*new_raw_arg2) != 'D')
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- }
- else
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- }
- // Size must be less than sizeof() minus 2 because need room to prepend the '%' and append
- // the 'f' to make it a valid format specifier string:
- break;
- case ACT_STRINGSPLIT: // v1.0.48.04: Moved this section so that it is done even when AUTOHOTKEYSC is defined, because the steps below are necessary for both.
- if (*new_raw_arg1 && !line.ArgHasDeref(1)) // The output array must be a legal name.
- {
- // 1.0.46.10: Fixed to look up ArrayName0 in advance (here at loadtime) so that runtime can
- // know whether it's local or global. This is necessary because only here at loadtime
- // is there any awareness of the current function's list of declared variables (to conserve
- // memory, that list is longer available at runtime).
- char temp_var_name[MAX_VAR_NAME_LENGTH + 10]; // Provide extra room for trailing "0", and to detect names that are too long.
- snprintf(temp_var_name, sizeof(temp_var_name), "%s0", new_raw_arg1);
- if ( !(the_new_line->mAttribute = FindOrAddVar(temp_var_name)) )
- return FAIL; // The above already displayed the error.
- }
- //else it's a dynamic array name. Since that's very rare, just use the old runtime behavior for
- // backward compatibility.
- break;
- #ifndef AUTOHOTKEYSC // For v1.0.35.01, some syntax checking is removed in compiled scripts to reduce their size.
- case ACT_RETURN:
- if (aArgc > 0 && !g->CurrentFunc)
- return ScriptError("Return's parameter should be blank except inside a function.");
- break;
- case ACT_AUTOTRIM:
- case ACT_DETECTHIDDENWINDOWS:
- case ACT_DETECTHIDDENTEXT:
- case ACT_SETSTORECAPSLOCKMODE:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertOnOff(new_raw_arg1))
- return ScriptError(ERR_ON_OFF, new_raw_arg1);
- break;
- case ACT_STRINGCASESENSE:
- if (aArgc > 0 && !line.ArgHasDeref(1) && line.ConvertStringCaseSense(new_raw_arg1) == SCS_INVALID)
- return ScriptError(ERR_ON_OFF_LOCALE, new_raw_arg1);
- break;
- case ACT_SETBATCHLINES:
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- if (!strcasestr(new_raw_arg1, "ms") && !IsPureNumeric(new_raw_arg1, true, false)) // For simplicity and due to rarity, new_arg[0].is_expression isn't checked, so a line with no variables or function-calls like "SetBatchLines % 1+1" will be wrongly seen as a syntax error.
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- }
- break;
- case ACT_SUSPEND:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertOnOffTogglePermit(new_raw_arg1))
- return ScriptError(ERR_ON_OFF_TOGGLE_PERMIT, new_raw_arg1);
- break;
- case ACT_BLOCKINPUT:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertBlockInput(new_raw_arg1))
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- break;
- case ACT_SENDMODE:
- if (aArgc > 0 && !line.ArgHasDeref(1) && line.ConvertSendMode(new_raw_arg1, SM_INVALID) == SM_INVALID)
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- break;
- case ACT_PAUSE:
- case ACT_KEYHISTORY:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertOnOffToggle(new_raw_arg1))
- return ScriptError(ERR_ON_OFF_TOGGLE, new_raw_arg1);
- break;
- case ACT_SETNUMLOCKSTATE:
- case ACT_SETSCROLLLOCKSTATE:
- case ACT_SETCAPSLOCKSTATE:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertOnOffAlways(new_raw_arg1))
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- break;
- case ACT_STRINGMID:
- if (aArgc > 4 && !line.ArgHasDeref(5) && stricmp(NEW_RAW_ARG5, "L"))
- return ScriptError(ERR_PARAM5_INVALID, NEW_RAW_ARG5);
- break;
- case ACT_STRINGGETPOS:
- if (*new_raw_arg4 && !line.ArgHasDeref(4) && !strchr("LR1", toupper(*new_raw_arg4)))
- return ScriptError(ERR_PARAM4_INVALID, new_raw_arg4);
- break;
- case ACT_REGREAD:
- // The below has two checks in case the user is using the 5-param method with the 5th parameter
- // being blank to indicate that the key's "default" value should be read. For example:
- // RegRead, OutVar, REG_SZ, HKEY_CURRENT_USER, Software\Winamp,
- if (aArgc > 4 || line.RegConvertValueType(new_raw_arg2))
- {
- // The obsolete 5-param method is being used, wherein ValueType is the 2nd param.
- if (*new_raw_arg3 && !line.ArgHasDeref(3) && !line.RegConvertRootKey(new_raw_arg3))
- return ScriptError(ERR_REG_KEY, new_raw_arg3);
- }
- else // 4-param method.
- if (*new_raw_arg2 && !line.ArgHasDeref(2) && !line.RegConvertRootKey(new_raw_arg2))
- return ScriptError(ERR_REG_KEY, new_raw_arg2);
- break;
- case ACT_REGWRITE:
- // Both of these checks require that at least two parameters be present. Otherwise, the command
- // is being used in its registry-loop mode and is validated elsewhere:
- if (aArgc > 1)
- {
- if (*new_raw_arg1 && !line.ArgHasDeref(1) && !line.RegConvertValueType(new_raw_arg1))
- return ScriptError(ERR_REG_VALUE_TYPE, new_raw_arg1);
- if (*new_raw_arg2 && !line.ArgHasDeref(2) && !line.RegConvertRootKey(new_raw_arg2))
- return ScriptError(ERR_REG_KEY, new_raw_arg2);
- }
- break;
- case ACT_REGDELETE:
- if (*new_raw_arg1 && !line.ArgHasDeref(1) && !line.RegConvertRootKey(new_raw_arg1))
- return ScriptError(ERR_REG_KEY, new_raw_arg1);
- break;
- case ACT_SOUNDGET:
- case ACT_SOUNDSET:
- if (aActionType == ACT_SOUNDSET && aArgc > 0 && !line.ArgHasDeref(1))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 300-250 as invalid.
- value_float = ATOF(new_raw_arg1);
- if (value_float < -100 || value_float > 100)
- return ScriptError(ERR_PERCENT, new_raw_arg1);
- }
- if (*new_raw_arg2 && !line.ArgHasDeref(2) && !line.SoundConvertComponentType(new_raw_arg2))
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- if (*new_raw_arg3 && !line.ArgHasDeref(3) && line.SoundConvertControlType(new_raw_arg3) == MIXERCONTROL_CONTROLTYPE_INVALID)
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- break;
- case ACT_SOUNDSETWAVEVOLUME:
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 300-250 as invalid.
- value_float = ATOF(new_raw_arg1);
- if (value_float < -100 || value_float > 100)
- return ScriptError(ERR_PERCENT, new_raw_arg1);
- }
- break;
- case ACT_SOUNDPLAY:
- if (*new_raw_arg2 && !line.ArgHasDeref(2) && stricmp(new_raw_arg2, "wait") && stricmp(new_raw_arg2, "1"))
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- break;
- case ACT_PIXELSEARCH:
- case ACT_IMAGESEARCH:
- if (!*new_raw_arg3 || !*new_raw_arg4 || !*NEW_RAW_ARG5 || !*NEW_RAW_ARG6 || !*NEW_RAW_ARG7)
- return ScriptError("Parameters 3 through 7 must not be blank.");
- if (aActionType != ACT_IMAGESEARCH)
- {
- if (*NEW_RAW_ARG8 && !line.ArgHasDeref(8))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 300-200 as invalid.
- value = ATOI(NEW_RAW_ARG8);
- if (value < 0 || value > 255)
- return ScriptError(ERR_PARAM8_INVALID, NEW_RAW_ARG8);
- }
- }
- break;
- case ACT_COORDMODE:
- if (*new_raw_arg1 && !line.ArgHasDeref(1) && !line.ConvertCoordModeAttrib(new_raw_arg1))
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- break;
- case ACT_SETDEFAULTMOUSESPEED:
- if (*new_raw_arg1 && !line.ArgHasDeref(1))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 1+2 as invalid.
- value = ATOI(new_raw_arg1);
- if (value < 0 || value > MAX_MOUSE_SPEED)
- return ScriptError(ERR_MOUSE_SPEED, new_raw_arg1);
- }
- break;
- case ACT_MOUSEMOVE:
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 200-150 as invalid.
- value = ATOI(new_raw_arg3);
- if (value < 0 || value > MAX_MOUSE_SPEED)
- return ScriptError(ERR_MOUSE_SPEED, new_raw_arg3);
- }
- if (*new_raw_arg4 && !line.ArgHasDeref(4) && toupper(*new_raw_arg4) != 'R')
- return ScriptError(ERR_PARAM4_INVALID, new_raw_arg4);
- if (!line.ValidateMouseCoords(new_raw_arg1, new_raw_arg2))
- return ScriptError(ERR_MOUSE_COORD, new_raw_arg1);
- break;
- case ACT_MOUSECLICK:
- if (*NEW_RAW_ARG5 && !line.ArgHasDeref(5))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 200-150 as invalid.
- value = ATOI(NEW_RAW_ARG5);
- if (value < 0 || value > MAX_MOUSE_SPEED)
- return ScriptError(ERR_MOUSE_SPEED, NEW_RAW_ARG5);
- }
- if (*NEW_RAW_ARG6 && !line.ArgHasDeref(6))
- if (strlen(NEW_RAW_ARG6) > 1 || !strchr("UD", toupper(*NEW_RAW_ARG6))) // Up / Down
- return ScriptError(ERR_PARAM6_INVALID, NEW_RAW_ARG6);
- if (*NEW_RAW_ARG7 && !line.ArgHasDeref(7) && toupper(*NEW_RAW_ARG7) != 'R')
- return ScriptError(ERR_PARAM7_INVALID, NEW_RAW_ARG7);
- // Check that the button is valid (e.g. left/right/middle):
- if (*new_raw_arg1 && !line.ArgHasDeref(1) && !line.ConvertMouseButton(new_raw_arg1)) // Treats blank as "Left".
- return ScriptError(ERR_MOUSE_BUTTON, new_raw_arg1);
- if (!line.ValidateMouseCoords(new_raw_arg2, new_raw_arg3))
- return ScriptError(ERR_MOUSE_COORD, new_raw_arg2);
- break;
- case ACT_MOUSECLICKDRAG:
- // Even though we check for blanks here at load-time, we don't bother to do so at runtime
- // (i.e. if a dereferenced var resolved to blank, it will be treated as a zero):
- if (!*new_raw_arg4 || !*NEW_RAW_ARG5)
- return ScriptError("Parameter #4 and 5 required.");
- if (*NEW_RAW_ARG6 && !line.ArgHasDeref(6))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 200-150 as invalid.
- value = ATOI(NEW_RAW_ARG6);
- if (value < 0 || value > MAX_MOUSE_SPEED)
- return ScriptError(ERR_MOUSE_SPEED, NEW_RAW_ARG6);
- }
- if (*NEW_RAW_ARG7 && !line.ArgHasDeref(7) && toupper(*NEW_RAW_ARG7) != 'R')
- return ScriptError(ERR_PARAM7_INVALID, NEW_RAW_ARG7);
- if (!line.ArgHasDeref(1))
- if (!line.ConvertMouseButton(new_raw_arg1, false))
- return ScriptError(ERR_MOUSE_BUTTON, new_raw_arg1);
- if (!line.ValidateMouseCoords(new_raw_arg2, new_raw_arg3))
- return ScriptError(ERR_MOUSE_COORD, new_raw_arg2);
- if (!line.ValidateMouseCoords(new_raw_arg4, NEW_RAW_ARG5))
- return ScriptError(ERR_MOUSE_COORD, new_raw_arg4);
- break;
- case ACT_CONTROLSEND:
- case ACT_CONTROLSENDRAW:
- // Window params can all be blank in this case, but characters to send should
- // be non-blank (but it's ok if its a dereferenced var that resolves to blank
- // at runtime):
- if (!*new_raw_arg2)
- return ScriptError(ERR_PARAM2_REQUIRED);
- break;
- case ACT_CONTROLCLICK:
- // Check that the button is valid (e.g. left/right/middle):
- if (*new_raw_arg4 && !line.ArgHasDeref(4)) // i.e. it's allowed to be blank (defaults to left).
- if (!line.ConvertMouseButton(new_raw_arg4)) // Treats blank as "Left".
- return ScriptError(ERR_MOUSE_BUTTON, new_raw_arg4);
- break;
- case ACT_FILEINSTALL:
- case ACT_FILECOPY:
- case ACT_FILEMOVE:
- case ACT_FILECOPYDIR:
- case ACT_FILEMOVEDIR:
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 2-1 as invalid.
- value = ATOI(new_raw_arg3);
- bool is_pure_numeric = IsPureNumeric(new_raw_arg3, false, true); // Consider negatives to be non-numeric.
- if (aActionType == ACT_FILEMOVEDIR)
- {
- if (!is_pure_numeric && toupper(*new_raw_arg3) != 'R'
- || is_pure_numeric && value > 2) // IsPureNumeric() already checked if value < 0.
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- }
- else
- {
- if (!is_pure_numeric || value > 1) // IsPureNumeric() already checked if value < 0.
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- }
- }
- if (aActionType == ACT_FILEINSTALL)
- {
- if (aArgc > 0 && line.ArgHasDeref(1))
- return ScriptError("Must not contain variables.", new_raw_arg1);
- }
- break;
- case ACT_FILEREMOVEDIR:
- if (*new_raw_arg2 && !line.ArgHasDeref(2))
- {
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 3-2 as invalid.
- value = ATOI(new_raw_arg2);
- if (!IsPureNumeric(new_raw_arg2, false, true) || value > 1) // IsPureNumeric() prevents negatives.
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- }
- break;
- case ACT_FILESETATTRIB:
- if (*new_raw_arg1 && !line.ArgHasDeref(1))
- {
- for (char *cp = new_raw_arg1; *cp; ++cp)
- if (!strchr("+-^RASHNOT", toupper(*cp)))
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- }
- // For the next two checks:
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 300-200 as invalid.
- if (aArgc > 2 && !line.ArgHasDeref(3) && line.ConvertLoopMode(new_raw_arg3) == FILE_LOOP_INVALID)
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- if (*new_raw_arg4 && !line.ArgHasDeref(4))
- if (strlen(new_raw_arg4) > 1 || (*new_raw_arg4 != '0' && *new_raw_arg4 != '1'))
- return ScriptError(ERR_PARAM4_INVALID, new_raw_arg4);
- break;
- case ACT_FILEGETTIME:
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- if (strlen(new_raw_arg3) > 1 || !strchr("MCA", toupper(*new_raw_arg3)))
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- break;
- case ACT_FILESETTIME:
- if (*new_raw_arg1 && !line.ArgHasDeref(1))
- if (!YYYYMMDDToSystemTime(new_raw_arg1, st, true))
- return ScriptError(ERR_INVALID_DATETIME, new_raw_arg1);
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- if (strlen(new_raw_arg3) > 1 || !strchr("MCA", toupper(*new_raw_arg3)))
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- // For the next two checks:
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 300-200 as invalid.
- if (aArgc > 3 && !line.ArgHasDeref(4) && line.ConvertLoopMode(new_raw_arg4) == FILE_LOOP_INVALID)
- return ScriptError(ERR_PARAM4_INVALID, new_raw_arg4);
- if (*NEW_RAW_ARG5 && !line.ArgHasDeref(5))
- if (strlen(NEW_RAW_ARG5) > 1 || (*NEW_RAW_ARG5 != '0' && *NEW_RAW_ARG5 != '1'))
- return ScriptError(ERR_PARAM5_INVALID, NEW_RAW_ARG5);
- break;
- case ACT_FILEGETSIZE:
- if (*new_raw_arg3 && !line.ArgHasDeref(3))
- if (strlen(new_raw_arg3) > 1 || !strchr("BKM", toupper(*new_raw_arg3))) // Allow B=Bytes as undocumented.
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- break;
- case ACT_SETTITLEMATCHMODE:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertTitleMatchMode(new_raw_arg1))
- return ScriptError(ERR_TITLEMATCHMODE, new_raw_arg1);
- break;
- case ACT_TRANSFORM:
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- // The value of trans_cmd was already set at an earlier stage, but only here can the error
- // for new_raw_arg3 be displayed because only here was it finally possible to call
- // ArgHasDeref() [above].
- if (trans_cmd == TRANS_CMD_INVALID)
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- if (trans_cmd == TRANS_CMD_UNICODE && !*line.mArg[0].text) // blank text means output-var is not a dynamically built one.
- {
- // If the output var isn't the clipboard, the mode is "retrieve clipboard text as UTF-8".
- // Therefore, Param#3 should be blank in that case to avoid unnecessary fetching of the
- // entire clipboard contents as plain text when in fact the command itself will be
- // directly accessing the clipboard rather than relying on the automatic parameter and
- // deref handling.
- if (VAR(line.mArg[0])->Type() == VAR_CLIPBOARD)
- {
- if (aArgc < 3)
- return ScriptError("Parameter #3 must not be blank in this case.");
- }
- else
- if (aArgc > 2)
- return ScriptError(ERR_PARAM3_MUST_BE_BLANK, new_raw_arg3);
- break; // This type has been fully checked above.
- }
- // The value of catching syntax errors at load-time seems to outweigh the fact that this check
- // sees a valid no-deref expression such as 1+2 as invalid.
- if (!line.ArgHasDeref(3)) // "true" since it might have been made into an InputVar due to being a simple expression.
- {
- switch(trans_cmd)
- {
- case TRANS_CMD_CHR:
- case TRANS_CMD_BITNOT:
- case TRANS_CMD_BITSHIFTLEFT:
- case TRANS_CMD_BITSHIFTRIGHT:
- case TRANS_CMD_BITAND:
- case TRANS_CMD_BITOR:
- case TRANS_CMD_BITXOR:
- if (!IsPureNumeric(new_raw_arg3, true, false))
- return ScriptError("Parameter #3 must be an integer in this case.", new_raw_arg3);
- break;
- case TRANS_CMD_MOD:
- case TRANS_CMD_EXP:
- case TRANS_CMD_ROUND:
- case TRANS_CMD_CEIL:
- case TRANS_CMD_FLOOR:
- case TRANS_CMD_ABS:
- case TRANS_CMD_SIN:
- case TRANS_CMD_COS:
- case TRANS_CMD_TAN:
- case TRANS_CMD_ASIN:
- case TRANS_CMD_ACOS:
- case TRANS_CMD_ATAN:
- if (!IsPureNumeric(new_raw_arg3, true, false, true))
- return ScriptError("Parameter #3 must be a number in this case.", new_raw_arg3);
- break;
- case TRANS_CMD_POW:
- case TRANS_CMD_SQRT:
- case TRANS_CMD_LOG:
- case TRANS_CMD_LN:
- if (!IsPureNumeric(new_raw_arg3, false, false, true))
- return ScriptError("Parameter #3 must be a positive integer in this case.", new_raw_arg3);
- break;
- // The following are not listed above because no validation of Parameter #3 is needed at this stage:
- // TRANS_CMD_ASC
- // TRANS_CMD_UNICODE
- // TRANS_CMD_HTML
- // TRANS_CMD_DEREF
- }
- }
- switch(trans_cmd)
- {
- case TRANS_CMD_ASC:
- case TRANS_CMD_CHR:
- case TRANS_CMD_DEREF:
- case TRANS_CMD_UNICODE:
- case TRANS_CMD_HTML:
- case TRANS_CMD_EXP:
- case TRANS_CMD_SQRT:
- case TRANS_CMD_LOG:
- case TRANS_CMD_LN:
- case TRANS_CMD_CEIL:
- case TRANS_CMD_FLOOR:
- case TRANS_CMD_ABS:
- case TRANS_CMD_SIN:
- case TRANS_CMD_COS:
- case TRANS_CMD_TAN:
- case TRANS_CMD_ASIN:
- case TRANS_CMD_ACOS:
- case TRANS_CMD_ATAN:
- case TRANS_CMD_BITNOT:
- if (*new_raw_arg4)
- return ScriptError(ERR_PARAM4_OMIT, new_raw_arg4);
- break;
- case TRANS_CMD_BITAND:
- case TRANS_CMD_BITOR:
- case TRANS_CMD_BITXOR:
- if (!line.ArgHasDeref(4) && !IsPureNumeric(new_raw_arg4, true, false))
- return ScriptError("Parameter #4 must be an integer in this case.", new_raw_arg4);
- break;
- case TRANS_CMD_BITSHIFTLEFT:
- case TRANS_CMD_BITSHIFTRIGHT:
- if (!line.ArgHasDeref(4) && !IsPureNumeric(new_raw_arg4, false, false))
- return ScriptError("Parameter #4 must be a positive integer in this case.", new_raw_arg4);
- break;
- case TRANS_CMD_ROUND:
- if (*new_raw_arg4 && !line.ArgHasDeref(4) && !IsPureNumeric(new_raw_arg4, true, false))
- return ScriptError("Parameter #4 must be blank or an integer in this case.", new_raw_arg4);
- break;
- case TRANS_CMD_MOD:
- case TRANS_CMD_POW:
- if (!line.ArgHasDeref(4) && !IsPureNumeric(new_raw_arg4, true, false, true))
- return ScriptError("Parameter #4 must be a number in this case.", new_raw_arg4);
- break;
- #ifdef _DEBUG
- default:
- return ScriptError("DEBUG: Unhandled", new_raw_arg2); // To improve maintainability.
- #endif
- }
- switch(trans_cmd)
- {
- case TRANS_CMD_CHR:
- if (!line.ArgHasDeref(3))
- {
- value = ATOI(new_raw_arg3);
- if (!IsPureNumeric(new_raw_arg3, false, false) || value > 255) // IsPureNumeric() checks for value < 0 too.
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- }
- break;
- case TRANS_CMD_MOD:
- if (!line.ArgHasDeref(4) && !ATOF(new_raw_arg4)) // Parameter is omitted or something that resolves to zero.
- return ScriptError(ERR_DIVIDEBYZERO, new_raw_arg4);
- break;
- }
- }
- break;
- case ACT_MENU:
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- MenuCommands menu_cmd = line.ConvertMenuCommand(new_raw_arg2);
- switch(menu_cmd)
- {
- case MENU_CMD_TIP:
- case MENU_CMD_ICON:
- case MENU_CMD_NOICON:
- case MENU_CMD_MAINWINDOW:
- case MENU_CMD_NOMAINWINDOW:
- case MENU_CMD_CLICK:
- {
- bool is_tray = true; // Assume true if unknown.
- if (aArgc > 0 && !line.ArgHasDeref(1))
- if (stricmp(new_raw_arg1, "tray"))
- is_tray = false;
- if (!is_tray)
- return ScriptError(ERR_MENUTRAY, new_raw_arg1);
- break;
- }
- }
- switch (menu_cmd)
- {
- case MENU_CMD_INVALID:
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- case MENU_CMD_NODEFAULT:
- case MENU_CMD_STANDARD:
- case MENU_CMD_NOSTANDARD:
- case MENU_CMD_DELETEALL:
- case MENU_CMD_NOICON:
- case MENU_CMD_MAINWINDOW:
- case MENU_CMD_NOMAINWINDOW:
- if (*new_raw_arg3 || *new_raw_arg4 || *NEW_RAW_ARG5 || *NEW_RAW_ARG6)
- return ScriptError("Parameter #3 and beyond should be omitted in this case.", new_raw_arg3);
- break;
- case MENU_CMD_RENAME:
- case MENU_CMD_USEERRORLEVEL:
- case MENU_CMD_CHECK:
- case MENU_CMD_UNCHECK:
- case MENU_CMD_TOGGLECHECK:
- case MENU_CMD_ENABLE:
- case MENU_CMD_DISABLE:
- case MENU_CMD_TOGGLEENABLE:
- case MENU_CMD_DEFAULT:
- case MENU_CMD_DELETE:
- case MENU_CMD_TIP:
- case MENU_CMD_CLICK:
- if ( menu_cmd != MENU_CMD_RENAME && (*new_raw_arg4 || *NEW_RAW_ARG5 || *NEW_RAW_ARG6) )
- return ScriptError("Parameter #4 and beyond should be omitted in this case.", new_raw_arg4);
- switch(menu_cmd)
- {
- case MENU_CMD_USEERRORLEVEL:
- case MENU_CMD_TIP:
- case MENU_CMD_DEFAULT:
- case MENU_CMD_DELETE:
- break; // i.e. for commands other than the above, do the default below.
- default:
- if (!*new_raw_arg3)
- return ScriptError("Parameter #3 must not be blank in this case.");
- }
- break;
- // These have a highly variable number of parameters, or are too rarely used
- // to warrant detailed load-time checking, so are not validated here:
- //case MENU_CMD_SHOW:
- //case MENU_CMD_ADD:
- //case MENU_CMD_COLOR:
- //case MENU_CMD_ICON:
- }
- }
- break;
- case ACT_THREAD:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertThreadCommand(new_raw_arg1))
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- break;
- case ACT_CONTROL:
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- ControlCmds control_cmd = line.ConvertControlCmd(new_raw_arg1);
- switch (control_cmd)
- {
- case CONTROL_CMD_INVALID:
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- case CONTROL_CMD_STYLE:
- case CONTROL_CMD_EXSTYLE:
- case CONTROL_CMD_TABLEFT:
- case CONTROL_CMD_TABRIGHT:
- case CONTROL_CMD_ADD:
- case CONTROL_CMD_DELETE:
- case CONTROL_CMD_CHOOSE:
- case CONTROL_CMD_CHOOSESTRING:
- case CONTROL_CMD_EDITPASTE:
- if (control_cmd != CONTROL_CMD_TABLEFT && control_cmd != CONTROL_CMD_TABRIGHT && !*new_raw_arg2)
- return ScriptError("Parameter #2 must not be blank in this case.");
- break;
- default: // All commands except the above should have a blank Value parameter.
- if (*new_raw_arg2)
- return ScriptError(ERR_PARAM2_MUST_BE_BLANK, new_raw_arg2);
- }
- }
- break;
- case ACT_CONTROLGET:
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- ControlGetCmds control_get_cmd = line.ConvertControlGetCmd(new_raw_arg2);
- switch (control_get_cmd)
- {
- case CONTROLGET_CMD_INVALID:
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- case CONTROLGET_CMD_FINDSTRING:
- case CONTROLGET_CMD_LINE:
- if (!*new_raw_arg3)
- return ScriptError("Parameter #3 must not be blank in this case.");
- break;
- case CONTROLGET_CMD_LIST:
- break; // Simply break for any sub-commands that have an optional parameter 3.
- default: // All commands except the above should have a blank Value parameter.
- if (*new_raw_arg3)
- return ScriptError(ERR_PARAM3_MUST_BE_BLANK, new_raw_arg3);
- }
- }
- break;
- case ACT_GUICONTROL:
- if (!*new_raw_arg2) // ControlID
- return ScriptError(ERR_PARAM2_REQUIRED);
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- GuiControlCmds guicontrol_cmd = line.ConvertGuiControlCmd(new_raw_arg1);
- switch (guicontrol_cmd)
- {
- case GUICONTROL_CMD_INVALID:
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- case GUICONTROL_CMD_CONTENTS:
- case GUICONTROL_CMD_TEXT:
- case GUICONTROL_CMD_MOVEDRAW:
- break; // Do nothing for the above commands since Param3 is optional.
- case GUICONTROL_CMD_MOVE:
- case GUICONTROL_CMD_CHOOSE:
- case GUICONTROL_CMD_CHOOSESTRING:
- if (!*new_raw_arg3)
- return ScriptError("Parameter #3 must not be blank in this case.");
- break;
- default: // All commands except the above should have a blank Text parameter.
- if (*new_raw_arg3)
- return ScriptError(ERR_PARAM3_MUST_BE_BLANK, new_raw_arg3);
- }
- }
- break;
- case ACT_GUICONTROLGET:
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- GuiControlGetCmds guicontrolget_cmd = line.ConvertGuiControlGetCmd(new_raw_arg2);
- // This first check's error messages take precedence over the next check's:
- switch (guicontrolget_cmd)
- {
- case GUICONTROLGET_CMD_INVALID:
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- case GUICONTROLGET_CMD_CONTENTS:
- break; // Do nothing, since Param4 is optional in this case.
- default: // All commands except the above should have a blank parameter here.
- if (*new_raw_arg4) // Currently true for all, since it's a FutureUse param.
- return ScriptError(ERR_PARAM4_MUST_BE_BLANK, new_raw_arg4);
- }
- if (guicontrolget_cmd == GUICONTROLGET_CMD_FOCUS || guicontrolget_cmd == GUICONTROLGET_CMD_FOCUSV)
- {
- if (*new_raw_arg3)
- return ScriptError(ERR_PARAM3_MUST_BE_BLANK, new_raw_arg3);
- }
- // else it can be optionally blank, in which case the output variable is used as the
- // ControlID also.
- }
- break;
- case ACT_DRIVE:
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- DriveCmds drive_cmd = line.ConvertDriveCmd(new_raw_arg1);
- if (!drive_cmd)
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- if (drive_cmd != DRIVE_CMD_EJECT && !*new_raw_arg2)
- return ScriptError("Parameter #2 must not be blank in this case.");
- // For DRIVE_CMD_LABEL: Note that is is possible and allowed for the new label to be blank.
- // Not currently done since all sub-commands take a mandatory or optional ARG3:
- //if (drive_cmd != ... && *new_raw_arg3)
- // return ScriptError(ERR_PARAM3_MUST_BE_BLANK, new_raw_arg3);
- }
- break;
- case ACT_DRIVEGET:
- if (!line.ArgHasDeref(2)) // Don't check "aArgc > 1" because of DRIVEGET_CMD_SETLABEL's param format.
- {
- DriveGetCmds drive_get_cmd = line.ConvertDriveGetCmd(new_raw_arg2);
- if (!drive_get_cmd)
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- if (drive_get_cmd != DRIVEGET_CMD_LIST && drive_get_cmd != DRIVEGET_CMD_STATUSCD && !*new_raw_arg3)
- return ScriptError("Parameter #3 must not be blank in this case.");
- if (drive_get_cmd != DRIVEGET_CMD_SETLABEL && (aArgc < 1 || line.mArg[0].type == ARG_TYPE_NORMAL))
- // The output variable has been omitted.
- return ScriptError("Parameter #1 must not be blank in this case.");
- }
- break;
- case ACT_PROCESS:
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- ProcessCmds process_cmd = line.ConvertProcessCmd(new_raw_arg1);
- if (process_cmd != PROCESS_CMD_PRIORITY && process_cmd != PROCESS_CMD_EXIST && !*new_raw_arg2)
- return ScriptError("Parameter #2 must not be blank in this case.");
- switch (process_cmd)
- {
- case PROCESS_CMD_INVALID:
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- case PROCESS_CMD_EXIST:
- case PROCESS_CMD_CLOSE:
- if (*new_raw_arg3)
- return ScriptError(ERR_PARAM3_MUST_BE_BLANK, new_raw_arg3);
- break;
- case PROCESS_CMD_PRIORITY:
- if (!*new_raw_arg3 || (!line.ArgHasDeref(3) && !strchr(PROCESS_PRIORITY_LETTERS, toupper(*new_raw_arg3))))
- return ScriptError(ERR_PARAM3_INVALID, new_raw_arg3);
- break;
- case PROCESS_CMD_WAIT:
- case PROCESS_CMD_WAITCLOSE:
- if (*new_raw_arg3 && !line.ArgHasDeref(3) && !IsPureNumeric(new_raw_arg3, false, true, true))
- return ScriptError("If present, parameter #3 must be a positive number in this case.", new_raw_arg3);
- break;
- }
- }
- break;
- // For ACT_WINMOVE, don't validate anything for mandatory args so that its two modes of
- // operation can be supported: 2-param mode and normal-param mode.
- // For these, although we validate that at least one is non-blank here, it's okay at
- // runtime for them all to resolve to be blank, without an error being reported.
- // It's probably more flexible that way since the commands are equipped to handle
- // all-blank params.
- // Not these because they can be used with the "last-used window" mode:
- //case ACT_IFWINEXIST:
- //case ACT_IFWINNOTEXIST:
- // Not these because they can have their window params all-blank to work in "last-used window" mode:
- //case ACT_IFWINACTIVE:
- //case ACT_IFWINNOTACTIVE:
- //case ACT_WINACTIVATE:
- //case ACT_WINWAITCLOSE:
- //case ACT_WINWAITACTIVE:
- //case ACT_WINWAITNOTACTIVE:
- case ACT_WINACTIVATEBOTTOM:
- if (!*new_raw_arg1 && !*new_raw_arg2 && !*new_raw_arg3 && !*new_raw_arg4)
- return ScriptError(ERR_WINDOW_PARAM);
- break;
- case ACT_WINWAIT:
- if (!*new_raw_arg1 && !*new_raw_arg2 && !*new_raw_arg4 && !*NEW_RAW_ARG5) // ARG3 is omitted because it's the timeout.
- return ScriptError(ERR_WINDOW_PARAM);
- break;
- case ACT_WINMENUSELECTITEM:
- // Window params can all be blank in this case, but the first menu param should
- // be non-blank (but it's ok if its a dereferenced var that resolves to blank
- // at runtime):
- if (!*new_raw_arg3)
- return ScriptError(ERR_PARAM3_REQUIRED);
- break;
- case ACT_WINSET:
- if (aArgc > 0 && !line.ArgHasDeref(1))
- {
- switch(line.ConvertWinSetAttribute(new_raw_arg1))
- {
- case WINSET_TRANSPARENT:
- if (aArgc > 1 && !line.ArgHasDeref(2))
- {
- value = ATOI(new_raw_arg2);
- if (value < 0 || value > 255)
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- }
- break;
- case WINSET_TRANSCOLOR:
- if (!*new_raw_arg2)
- return ScriptError("Parameter #2 must not be blank in this case.");
- break;
- case WINSET_ALWAYSONTOP:
- if (aArgc > 1 && !line.ArgHasDeref(2) && !line.ConvertOnOffToggle(new_raw_arg2))
- return ScriptError(ERR_ON_OFF_TOGGLE, new_raw_arg2);
- break;
- case WINSET_BOTTOM:
- case WINSET_TOP:
- case WINSET_REDRAW:
- case WINSET_ENABLE:
- case WINSET_DISABLE:
- if (*new_raw_arg2)
- return ScriptError(ERR_PARAM2_MUST_BE_BLANK);
- break;
- case WINSET_INVALID:
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- }
- }
- break;
- case ACT_WINGET:
- if (!line.ArgHasDeref(2) && !line.ConvertWinGetCmd(new_raw_arg2)) // It's okay if ARG2 is blank.
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- break;
- case ACT_SYSGET:
- if (!line.ArgHasDeref(2) && !line.ConvertSysGetCmd(new_raw_arg2))
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- break;
- case ACT_INPUTBOX:
- if (*NEW_RAW_ARG9) // && !line.ArgHasDeref(9)
- return ScriptError("Parameter #9 must be blank.", NEW_RAW_ARG9);
- break;
- case ACT_MSGBOX:
- if (aArgc > 1) // i.e. this MsgBox is using the 3-param or 4-param style.
- if (!line.ArgHasDeref(1)) // i.e. if it's a deref, we won't try to validate it now.
- if (!IsPureNumeric(new_raw_arg1)) // Allow it to be entirely whitespace to indicate 0, like Aut2.
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- if (aArgc > 3) // EVEN THOUGH IT'S NUMERIC, due to MsgBox's smart-comma handling, this cannot be an expression because it would never have been detected as the fourth parameter to begin with.
- if (!line.ArgHasDeref(4)) // i.e. if it's a deref, we won't try to validate it now.
- if (!IsPureNumeric(new_raw_arg4, false, true, true))
- return ScriptError(ERR_PARAM4_INVALID, new_raw_arg4);
- break;
- case ACT_IFMSGBOX:
- if (aArgc > 0 && !line.ArgHasDeref(1) && !line.ConvertMsgBoxResult(new_raw_arg1))
- return ScriptError(ERR_PARAM1_INVALID, new_raw_arg1);
- break;
- case ACT_IFIS:
- case ACT_IFISNOT:
- if (aArgc > 1 && !line.ArgHasDeref(2) && !line.ConvertVariableTypeName(new_raw_arg2))
- // Don't refer to it as "Parameter #2" because this command isn't formatted/displayed that way.
- // Update: Param2 is more descriptive than the other (short) alternatives:
- return ScriptError(ERR_PARAM2_INVALID, new_raw_arg2);
- break;
- case ACT_GETKEYSTATE:
- // v1.0.44.03: Don't validate single-character key names because although a character like ü might have no
- // matching VK in system's default layout, that layout could change to something which does have a VK for it.
- if (aArgc > 1 && !line.ArgHasDeref(2) && strlen(new_raw_arg2) > 1 && !TextToVK(new_raw_arg2) && !ConvertJoy(new_raw_arg2))
- return ScriptError(ERR_INVALID_KEY_OR_BUTTON, new_raw_arg2);
- break;
- case ACT_KEYWAIT: // v1.0.44.03: See comment above.
- if (aArgc > 0 && !line.ArgHasDeref(1) && strlen(new_raw_arg1) > 1 && !TextToVK(new_raw_arg1) && !ConvertJoy(new_raw_arg1))
- return ScriptError(ERR_INVALID_KEY_OR_BUTTON, new_raw_arg1);
- break;
- #endif // The above section is in place only if when not AUTOHOTKEYSC.
- }
- if (mNextLineIsFunctionBody)
- {
- mLastFunc->mJumpToLine = the_new_line;
- mNextLineIsFunctionBody = false;
- if (g->CurrentFunc->mDefaultVarType == VAR_DECLARE_NONE)
- g->CurrentFunc->mDefaultVarType = VAR_DECLARE_LOCAL; // Set default since no override was discovered at the top of the body.
- }
- // No checking for unbalanced blocks is done here. That is done by PreparseBlocks() because
- // it displays more informative error messages:
- if (aActionType == ACT_BLOCK_BEGIN)
- {
- ++mCurrentFuncOpenBlockCount; // It's okay to increment unconditionally because it is reset to zero every time a new function definition is entered.
- // It's only necessary to check mLastFunc, not the one(s) that come before it, to see if its
- // mJumpToLine is NULL. This is because our caller has made it impossible for a function
- // to ever have been defined in the first place if it lacked its opening brace. Search on
- // "consecutive function" for more comments. In addition, the following does not check
- // that mCurrentFuncOpenBlockCount is exactly 1, because: 1) Want to be able to support function
- // definitions inside of other function definitions (to help script maintainability); 2) If
- // mCurrentFuncOpenBlockCount is 0 or negative, that will be caught as a syntax error by PreparseBlocks(),
- // which yields a more informative error message that we could here.
- if (mLastFunc && !mLastFunc->mJumpToLine) // If this stmt is true, caller has ensured that g->CurrentFunc isn't NULL.
- {
- // The above check relies upon the fact that mLastFunc->mIsBuiltIn must be false at this stage,
- // which is the case because any non-overridden built-in function won't get added until after all
- // lines have been added, namely PreparseBlocks().
- line.mAttribute = ATTR_TRUE; // Flag this ACT_BLOCK_BEGIN as the opening brace of the function's body.
- // For efficiency, and to prevent ExecUntil from starting a new recursion layer for the function's
- // body, the function's execution should begin at the first line after its open-brace (even if that
- // first line is another open-brace or the function's close-brace (i.e. an empty function):
- mNextLineIsFunctionBody = true;
- }
- }
- else if (aActionType == ACT_BLOCK_END)
- {
- --mCurrentFuncOpenBlockCount; // It's okay to increment unconditionally because it is reset to zero every time a new function definition is entered.
- if (g->CurrentFunc && !mCurrentFuncOpenBlockCount) // Any negative mCurrentFuncOpenBlockCount is caught by a different stage.
- {
- line.mAttribute = ATTR_TRUE; // Flag this ACT_BLOCK_END as the ending brace of a function's body.
- g->CurrentFunc = NULL;
- mFuncExceptionVar = NULL; // Notify FindVar() that there is no exception list to search.
- }
- }
- // Above must be done prior to the below, since it sometimes sets mAttribute for use below.
- ///////////////////////////////////////////////////////////////
- // Update any labels that should refer to the newly added line.
- ///////////////////////////////////////////////////////////////
- // If the label most recently added doesn't yet have an anchor in the script, provide it.
- // UPDATE: In addition, keep searching backward through the labels until a non-NULL
- // mJumpToLine is found. All the ones with a NULL should point to this new line to
- // support cases where one label immediately follows another in the script.
- // Example:
- // #a:: <-- don't leave this label with a NULL jumppoint.
- // LaunchA:
- // ...
- // return
- if (do_update_labels)
- {
- for (Label *label = mLastLabel; label != NULL && label->mJumpToLine == NULL; label = label->mPrevLabel)
- {
- if (line.mActionType == ACT_BLOCK_BEGIN && line.mAttribute == ATTR_TRUE) // Non-zero mAttribute signfies the open-brace of a function body.
- return ScriptError("A label must not point to a function.");
- if (line.mActionType == ACT_ELSE)
- return ScriptError("A label must not point to an ELSE.");
- // Don't allow this because it may cause problems in a case such as this because
- // label1 points to the end-block which is at the same level (and thus normally
- // an allowable jumppoint) as the goto. But we don't want to allow jumping into
- // a block that belongs to a control structure. In this case, it would probably
- // result in a runtime error when the execution unexpectedly encounters the ELSE
- // after performing the goto:
- // goto, label1
- // if x
- // {
- // ...
- // label1:
- // }
- // else
- // ...
- //
- // An alternate way to deal with the above would be to make each block-end be owned
- // by its block-begin rather than the block that encloses them both.
- if (line.mActionType == ACT_BLOCK_END)
- return ScriptError("A label must not point to the end of a block. For loops, use Continue vs. Goto.");
- label->mJumpToLine = the_new_line;
- }
- }
- ++mLineCount; // Right before returning "success", increment our count.
- return OK;
- }
- ResultType Script::ParseDerefs(char *aArgText, char *aArgMap, DerefType *aDeref, int &aDerefCount)
- // Caller provides modifiable aDerefCount, which might be non-zero to indicate that there are already
- // some items in the aDeref array.
- // Returns FAIL or OK.
- {
- size_t deref_string_length; // So that overflow can be detected, this is not of type DerefLengthType.
- // For each dereference found in aArgText:
- for (int j = 0;; ++j) // Increment to skip over the symbol just found by the inner for().
- {
- // Find next non-literal g_DerefChar:
- for (; aArgText[j] && (aArgText[j] != g_DerefChar || (aArgMap && aArgMap[j])); ++j);
- if (!aArgText[j])
- break;
- // else: Match was found; this is the deref's open-symbol.
- if (aDerefCount >= MAX_DEREFS_PER_ARG)
- return ScriptError(TOO_MANY_REFS, aArgText); // Short msg since so rare.
- DerefType &this_deref = aDeref[aDerefCount]; // For performance.
- this_deref.marker = aArgText + j; // Store the deref's starting location.
- // Find next g_DerefChar, even if it's a literal.
- for (++j; aArgText[j] && aArgText[j] != g_DerefChar; ++j);
- if (!aArgText[j])
- return ScriptError("This parameter contains a variable name missing its ending percent sign.", aArgText);
- // Otherwise: Match was found; this should be the deref's close-symbol.
- if (aArgMap && aArgMap[j]) // But it's mapped as literal g_DerefChar.
- return ScriptError("Invalid `%.", aArgText); // Short msg. since so rare.
- deref_string_length = aArgText + j - this_deref.marker + 1;
- if (deref_string_length == 2) // The percent signs were empty, e.g. %%
- return ScriptError("Empty variable reference (%%).", aArgText); // Short msg. since so rare.
- if (deref_string_length - 2 > MAX_VAR_NAME_LENGTH) // -2 for the opening & closing g_DerefChars
- return ScriptError("Variable name too long.", aArgText); // Short msg. since so rare.
- this_deref.is_function = false;
- this_deref.length = (DerefLengthType)deref_string_length;
- if ( !(this_deref.var = FindOrAddVar(this_deref.marker + 1, this_deref.length - 2)) )
- return FAIL; // The called function already displayed the error.
- ++aDerefCount;
- } // for each dereference.
- return OK;
- }
- ResultType Script::DefineFunc(char *aBuf, Var *aFuncExceptionVar[])
- // Returns OK or FAIL.
- // Caller has already called ValidateName() on the function, and it is known that this valid name
- // is followed immediately by an open-paren. aFuncExceptionVar is the address of an array on
- // the caller's stack that will hold the list of exception variables (those that must be explicitly
- // declared as either local or global) within the body of the function.
- {
- char *param_end, *param_start = strchr(aBuf, '('); // Caller has ensured that this will return non-NULL.
- Func *found_func = FindFunc(aBuf, param_start - aBuf);
- if (found_func)
- {
- if (!found_func->mIsBuiltIn)
- return ScriptError("Duplicate function definition.", aBuf); // Seems more descriptive than "Function already defined."
- else // It's a built-in function that the user wants to override with a custom definition.
- {
- found_func->mIsBuiltIn = false; // Override built-in with custom.
- found_func->mParamCount = 0; // Revert to the default appropriate for non-built-in functions.
- found_func->mMinParams = 0; //
- found_func->mJumpToLine = NULL; // Fixed for v1.0.35.12: Must reset for detection elsewhere.
- g->CurrentFunc = found_func;
- }
- }
- else
- // The value of g->CurrentFunc must be set here rather than by our caller since AddVar(), which we call,
- // relies upon it having been done.
- if ( !(g->CurrentFunc = AddFunc(aBuf, param_start - aBuf, false)) )
- return FAIL; // It already displayed the error.
- mCurrentFuncOpenBlockCount = 0; // v1.0.48.01: Initializing this here makes function definions work properly when they're inside a block.
- Func &func = *g->CurrentFunc; // For performance and convenience.
- int insert_pos;
- size_t param_length, value_length;
- FuncParam param[MAX_FUNCTION_PARAMS];
- int param_count = 0;
- char buf[LINE_SIZE], *target;
- bool param_must_have_default = false;
- for (param_start = omit_leading_whitespace(param_start + 1);;)
- {
- if (*param_start == ')') // No more params.
- break;
- // Must start the search at param_start, not param_start+1, so that something like fn(, x) will be properly handled:
- if ( !*param_start || !(param_end = StrChrAny(param_start, ", \t=)")) ) // Look for first comma, space, tab, =, or close-paren.
- return ScriptError(ERR_MISSING_CLOSE_PAREN, aBuf);
- if (param_count >= MAX_FUNCTION_PARAMS)
- return ScriptError("Too many params.", param_start); // Short msg since so rare.
- FuncParam &this_param = param[param_count]; // For performance and convenience.
- // To enhance syntax error catching, consider ByRef to be a keyword; i.e. that can never be the name
- // of a formal parameter:
- if (this_param.is_byref = !strlicmp(param_start, "ByRef", (UINT)(param_end - param_start))) // ByRef.
- {
- // Omit the ByRef keyword from further consideration:
- param_start = omit_leading_whitespace(param_end);
- if ( !*param_start || !(param_end = StrChrAny(param_start, ", \t=)")) ) // Look for first comma, space, tab, =, or close-paren.
- return ScriptError(ERR_MISSING_CLOSE_PAREN, aBuf);
- }
- if ( !(param_length = param_end - param_start) )
- return ScriptError(ERR_BLANK_PARAM, aBuf); // Reporting aBuf vs. param_start seems more informative since Vicinity isn't shown.
- // This will search for local variables, never globals, by virtue of the fact that this
- // new function's mDefaultVarType is always VAR_DECLARE_NONE at this early stage of its creation:
- if (this_param.var = FindVar(param_start, param_length, &insert_pos)) // Assign.
- return ScriptError("Duplicate parameter.", param_start);
- if ( !(this_param.var = AddVar(param_start, param_length, insert_pos, 2)) ) // Pass 2 as last parameter to mean "it's a local but more specifically a function's parameter".
- return FAIL; // It already displayed the error, including attempts to have reserved names as parameter names.
- // v1.0.35: Check if a default value is specified for this parameter and set up for the next iteration.
- // The following section is similar to that used to support initializers for static variables.
- // So maybe maintain them together.
- this_param.default_type = PARAM_DEFAULT_NONE; // Set default.
- param_start = omit_leading_whitespace(param_end);
- if (*param_start == '=') // This is the default value of the param just added.
- {
- param_start = omit_leading_whitespace(param_start + 1); // Start of the default value.
- if (*param_start == '"') // Quoted literal string, or the empty string.
- {
- // v1.0.46.13: Adde support for quoted/literal strings beyond simply "".
- // The following section is nearly identical to one in ExpandExpression().
- // Find the end of this string literal, noting that a pair of double quotes is
- // a literal double quote inside the string.
- for (target = buf, param_end = param_start + 1;;) // Omit the starting-quote from consideration, and from the resulting/built string.
- {
- if (!*param_end) // No matching end-quote. Probably impossible due to load-time validation.
- return ScriptError(ERR_MISSING_CLOSE_QUOTE, param_start); // Reporting param_start vs. aBuf seems more informative in the case of quoted/literal strings.
- if (*param_end == '"') // And if it's not followed immediately by another, this is the end of it.
- {
- ++param_end;
- if (*param_end != '"') // String terminator or some non-quote character.
- break; // The previous char is the ending quote.
- //else a pair of quotes, which resolves to a single literal quote. So fall through
- // to the below, which will copy of quote character to the buffer. Then this pair
- // is skipped over and the loop continues until the real end-quote is found.
- }
- //else some character other than '\0' or '"'.
- *target++ = *param_end++;
- }
- *target = '\0'; // Terminate it in the buffer.
- // The above has also set param_end for use near the bottom of the loop.
- ConvertEscapeSequences(buf, g_EscapeChar, false); // Raw escape sequences like `n haven't been converted yet, so do it now.
- this_param.default_type = PARAM_DEFAULT_STR;
- this_param.default_str = *buf ? SimpleHeap::Malloc(buf, target-buf) : "";
- }
- else // A default value other than a quoted/literal string.
- {
- if (!(param_end = StrChrAny(param_start, ", \t=)"))) // Somewhat debatable but stricter seems better.
- return ScriptError(ERR_MISSING_COMMA, aBuf); // Reporting aBuf vs. param_start seems more informative since Vicinity isn't shown.
- value_length = param_end - param_start;
- if (value_length > MAX_NUMBER_LENGTH) // Too rare to justify elaborate handling or error reporting.
- value_length = MAX_NUMBER_LENGTH;
- strlcpy(buf, param_start, value_length + 1); // Make a temp copy to simplify the below (especially IsPureNumeric).
- if (!stricmp(buf, "false"))
- {
- this_param.default_type = PARAM_DEFAULT_INT;
- this_param.default_int64 = 0;
- }
- else if (!stricmp(buf, "true"))
- {
- this_param.default_type = PARAM_DEFAULT_INT;
- this_param.default_int64 = 1;
- }
- else // The only things supported other than the above are integers and floats.
- {
- // Vars could be supported here via FindVar(), but only globals ABOVE this point in
- // the script would be supported (since other globals don't exist yet). So it seems
- // best to wait until full/comprehesive support for expressions is studied/designed
- // for both static initializers and parameter-default-values.
- switch(IsPureNumeric(buf, true, false, true))
- {
- case PURE_INTEGER:
- // It's always been somewhat inconsistent that for parameter default values,
- // numbers like 0xFF and 0123 do not preserve their formatting (unlike func(0123)
- // and y:=0xFF, which do preserve it). But for backward compatibility and
- // performance, it seems best to keep it this way.
- this_param.default_type = PARAM_DEFAULT_INT;
- this_param.default_int64 = ATOI64(buf);
- break;
- case PURE_FLOAT:
- this_param.default_type = PARAM_DEFAULT_FLOAT;
- this_param.default_double = ATOF(buf);
- break;
- default: // Not numeric (and also not a quoted string because that was handled earlier).
- return ScriptError("Unsupported parameter default.", aBuf);
- }
- }
- }
- param_must_have_default = true; // For now, all other params after this one must also have default values.
- // Set up for the next iteration:
- param_start = omit_leading_whitespace(param_end);
- }
- else // This parameter does not have a default value specified.
- {
- if (param_must_have_default)
- return ScriptError("Parameter default required.", this_param.var->mName);
- ++func.mMinParams;
- }
- ++param_count;
- if (*param_start != ',' && *param_start != ')') // Something like "fn(a, b c)" (missing comma) would cause this.
- return ScriptError(ERR_MISSING_COMMA, aBuf); // Reporting aBuf vs. param_start seems more informative since Vicinity isn't shown.
- if (*param_start == ',')
- {
- param_start = omit_leading_whitespace(param_start + 1);
- if (*param_start == ')') // If *param_start is ',' it will be caught as an error by the next iteration.
- return ScriptError(ERR_BLANK_PARAM, aBuf); // Reporting aBuf vs. param_start seems more informative since Vicinity isn't shown.
- }
- //else it's ')', in which case the next iteration will handle it.
- // Above has ensured that param_start now points to the next parameter, or ')' if none.
- } // for() each formal parameter.
- if (param_count)
- {
- // Allocate memory only for the actual number of parameters actually present.
- size_t size = param_count * sizeof(param[0]);
- if ( !(func.mParam = (FuncParam *)SimpleHeap::Malloc(size)) )
- return ScriptError(ERR_OUTOFMEM);
- func.mParamCount = param_count;
- memcpy(func.mParam, param, size);
- }
- //else leave func.mParam/mParamCount set to their NULL/0 defaults.
- // Indicate success:
- mFuncExceptionVar = aFuncExceptionVar; // Give mFuncExceptionVar its address, to be used for any var declarations inside this function's body.
- mFuncExceptionVarCount = 0; // Reset in preparation of declarations that appear beneath this function's definition.
- return OK;
- }
- #ifndef AUTOHOTKEYSC
- struct FuncLibrary
- {
- char *path;
- DWORD length;
- };
- Func *Script::FindFuncInLibrary(char *aFuncName, size_t aFuncNameLength, bool &aErrorWasShown)
- // Caller must ensure that aFuncName doesn't already exist as a defined function.
- // If aFuncNameLength is 0, the entire length of aFuncName is used.
- {
- aErrorWasShown = false; // Set default for this output parameter.
- int i;
- char *char_after_last_backslash, *terminate_here;
- DWORD attr;
- #define FUNC_LIB_EXT ".ahk"
- #define FUNC_LIB_EXT_LENGTH 4
- #define FUNC_USER_LIB "\\AutoHotkey\\Lib\\" // Needs leading and trailing backslash.
- #define FUNC_USER_LIB_LENGTH 16
- #define FUNC_STD_LIB "Lib\\" // Needs trailing but not leading backslash.
- #define FUNC_STD_LIB_LENGTH 4
- #define FUNC_LIB_COUNT 2
- static FuncLibrary sLib[FUNC_LIB_COUNT] = {0};
- if (!sLib[0].path) // Allocate & discover paths only upon first use because many scripts won't use anything from the library. This saves a bit of memory and performance.
- {
- for (i = 0; i < FUNC_LIB_COUNT; ++i)
- if ( !(sLib[i].path = SimpleHeap::Malloc(MAX_PATH)) ) // Need MAX_PATH for to allow room for appending each candidate file/function name.
- return NULL; // Due to rarity, simply pass the failure back to caller.
- // DETERMINE PATH TO "USER" LIBRARY:
- FuncLibrary *this_lib = sLib; // For convenience and maintainability.
- this_lib->length = BIV_MyDocuments(this_lib->path, "");
- if (this_lib->length < MAX_PATH-FUNC_USER_LIB_LENGTH)
- {
- strcpy(this_lib->path + this_lib->length, FUNC_USER_LIB);
- this_lib->length += FUNC_USER_LIB_LENGTH;
- }
- else // Insufficient room to build the path name.
- {
- *this_lib->path = '\0'; // Mark this library as disabled.
- this_lib->length = 0; //
- }
- // DETERMINE PATH TO "STANDARD" LIBRARY:
- this_lib = sLib + 1; // For convenience and maintainability.
- GetModuleFileName(NULL, this_lib->path, MAX_PATH); // The full path to the currently-running AutoHotkey.exe.
- char_after_last_backslash = 1 + strrchr(this_lib->path, '\\'); // Should always be found, so failure isn't checked.
- this_lib->length = (DWORD)(char_after_last_backslash - this_lib->path); // The length up to and including the last backslash.
- if (this_lib->length < MAX_PATH-FUNC_STD_LIB_LENGTH)
- {
- strcpy(this_lib->path + this_lib->length, FUNC_STD_LIB);
- this_lib->length += FUNC_STD_LIB_LENGTH;
- }
- else // Insufficient room to build the path name.
- {
- *this_lib->path = '\0'; // Mark this library as disabled.
- this_lib->length = 0; //
- }
- for (i = 0; i < FUNC_LIB_COUNT; ++i)
- {
- attr = GetFileAttributes(sLib[i].path); // Seems to accept directories that have a trailing backslash, which is good because it simplifies the code.
- if (attr == 0xFFFFFFFF || !(attr & FILE_ATTRIBUTE_DIRECTORY)) // Directory doesn't exist or it's a file vs. directory. Relies on short-circuit boolean order.
- {
- *sLib[i].path = '\0'; // Mark this library as disabled.
- sLib[i].length = 0; //
- }
- }
- }
- // Above must ensure that all sLib[].path elements are non-NULL (but they can be "" to indicate "no library").
- if (!aFuncNameLength) // Caller didn't specify, so use the entire string.
- aFuncNameLength = strlen(aFuncName);
- char *dest, *first_underscore, class_name_buf[MAX_VAR_NAME_LENGTH + 1];
- char *naked_filename = aFuncName; // Set up for the first iteration.
- size_t naked_filename_length = aFuncNameLength; //
- for (int second_iteration = 0; second_iteration < 2; ++second_iteration)
- {
- for (i = 0; i < FUNC_LIB_COUNT; ++i)
- {
- if (!*sLib[i].path) // Library is marked disabled, so skip it.
- continue;
- if (sLib[i].length + naked_filename_length >= MAX_PATH-FUNC_LIB_EXT_LENGTH)
- continue; // Path too long to match in this library, but try others.
- dest = (char *)memcpy(sLib[i].path + sLib[i].length, naked_filename, naked_filename_length); // Append the filename to the library path.
- strcpy(dest + naked_filename_length, FUNC_LIB_EXT); // Append the file extension.
- attr = GetFileAttributes(sLib[i].path); // Testing confirms that GetFileAttributes() doesn't support wildcards; which is good because we want filenames containing question marks to be "not found" rather than being treated as a match-pattern.
- if (attr == 0xFFFFFFFF || (attr & FILE_ATTRIBUTE_DIRECTORY)) // File doesn't exist or it's a directory. Relies on short-circuit boolean order.
- continue;
- // Since above didn't "continue", a file exists whose name matches that of the requested function.
- // Before loading/including that file, set the working directory to its folder so that if it uses
- // #Include, it will be able to use more convenient/intuitive relative paths. This is similar to
- // the "#Include DirName" feature.
- // Call SetWorkingDir() vs. SetCurrentDirectory() so that it succeeds even for a root drive like
- // C: that lacks a backslash (see SetWorkingDir() for details).
- terminate_here = sLib[i].path + sLib[i].length - 1; // The trailing backslash in the full-path-name to this library.
- *terminate_here = '\0'; // Temporarily terminate it for use with SetWorkingDir().
- SetWorkingDir(sLib[i].path); // See similar section in the #Include directive.
- *terminate_here = '\\'; // Undo the termination.
- if (!LoadIncludedFile(sLib[i].path, false, false)) // Fix for v1.0.47.05: Pass false for allow-dupe because otherwise, it's possible for a stdlib file to attempt to include itself (especially via the LibNamePrefix_ method) and thus give a misleading "duplicate function" vs. "func does not exist" error message. Obsolete: For performance, pass true for allow-dupe so that it doesn't have to check for a duplicate file (seems too rare to worry about duplicates since by definition, the function doesn't yet exist so it's file shouldn't yet be included).
- {
- aErrorWasShown = true; // Above has just displayed its error (e.g. syntax error in a line, failed to open the include file, etc). So override the default set earlier.
- return NULL;
- }
- if (mIncludeLibraryFunctionsThenExit)
- {
- // For each included library-file, write out two #Include lines:
- // 1) Use #Include in its "change working directory" mode so that any explicit #include directives
- // or FileInstalls inside the library file itself will work consistently and properly.
- // 2) Use #IncludeAgain (to improve performance since no dupe-checking is needed) to include
- // the library file itself.
- // We don't directly append library files onto the main script here because:
- // 1) ahk2exe needs to be able to see and act upon FileInstall and #Include lines (i.e. library files
- // might contain #Include even though it's rare).
- // 2) #IncludeAgain and #Include directives that bring in fragments rather than entire functions or
- // subroutines wouldn't work properly if we resolved such includes in AutoHotkey.exe because they
- // wouldn't be properly interleaved/asynchronous, but instead brought out of their library file
- // and deposited separately/synchronously into the temp-include file by some new logic at the
- // AutoHotkey.exe's code for the #Include directive.
- // 3) ahk2exe prefers to omit comments from included files to minimize size of compiled scripts.
- fprintf(mIncludeLibraryFunctionsThenExit, "#Include %-0.*s\n#IncludeAgain %s\n"
- , sLib[i].length, sLib[i].path, sLib[i].path);
- // Now continue on normally so that our caller can continue looking for syntax errors.
- }
- // Now that a matching filename has been found, it seems best to stop searching here even if that
- // file doesn't actually contain the requested function. This helps library authors catch bugs/typos.
- return FindFunc(aFuncName, aFuncNameLength);
- } // for() each library directory.
- // Now that the first iteration is done, set up for the second one that searches by class/prefix.
- // Notes about ambiguity and naming collisions:
- // By the time it gets to the prefix/class search, it's almost given up. Even if it wrongly finds a
- // match in a filename that isn't really a class, it seems inconsequential because at worst it will
- // still not find the function and will then say "call to nonexistent function". In addition, the
- // ability to customize which libraries are searched is planned. This would allow a publicly
- // distributed script to turn off all libraries except stdlib.
- if ( !(first_underscore = strchr(aFuncName, '_')) ) // No second iteration needed.
- break; // All loops are done because second iteration is the last possible attempt.
- naked_filename_length = first_underscore - aFuncName;
- if (naked_filename_length >= sizeof(class_name_buf)) // Class name too long (probably impossible currently).
- break; // All loops are done because second iteration is the last possible attempt.
- naked_filename = class_name_buf; // Point it to a buffer for use below.
- memcpy(naked_filename, aFuncName, naked_filename_length);
- naked_filename[naked_filename_length] = '\0';
- } // 2-iteration for().
- // Since above didn't return, no match found in any library.
- return NULL;
- }
- #endif
- Func *Script::FindFunc(char *aFuncName, size_t aFuncNameLength)
- // Returns the Function whose name matches aFuncName (which caller has ensured isn't NULL).
- // If it doesn't exist, NULL is returned.
- {
- if (!aFuncNameLength) // Caller didn't specify, so use the entire string.
- aFuncNameLength = strlen(aFuncName);
- // For the below, no error is reported because callers don't want that. Instead, simply return
- // NULL to indicate that names that are illegal or too long are not found. If the caller later
- // tries to add the function, it will get an error then:
- if (aFuncNameLength > MAX_VAR_NAME_LENGTH)
- return NULL;
- // The following copy is made because it allows the name searching to use stricmp() instead of
- // strlicmp(), which close to doubles the performance. The copy includes only the first aVarNameLength
- // characters from aVarName:
- char func_name[MAX_VAR_NAME_LENGTH + 1];
- strlcpy(func_name, aFuncName, aFuncNameLength + 1); // +1 to convert length to size.
- Func *pfunc;
- for (pfunc = mFirstFunc; pfunc; pfunc = pfunc->mNextFunc)
- if (!stricmp(func_name, pfunc->mName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- return pfunc; // Match found.
- // Since above didn't return, there is no match. See if it's a built-in function that hasn't yet
- // been added to the function list.
- // Set defaults to be possibly overridden below:
- int min_params = 1;
- int max_params = 1;
- BuiltInFunctionType bif;
- char *suffix = func_name + 3;
- if (!strnicmp(func_name, "LV_", 3)) // As a built-in function, LV_* can only be a ListView function.
- {
- suffix = func_name + 3;
- if (!stricmp(suffix, "GetNext"))
- {
- bif = BIF_LV_GetNextOrCount;
- min_params = 0;
- max_params = 2;
- }
- else if (!stricmp(suffix, "GetCount"))
- {
- bif = BIF_LV_GetNextOrCount;
- min_params = 0; // But leave max at its default of 1.
- }
- else if (!stricmp(suffix, "GetText"))
- {
- bif = BIF_LV_GetText;
- min_params = 2;
- max_params = 3;
- }
- else if (!stricmp(suffix, "Add"))
- {
- bif = BIF_LV_AddInsertModify;
- min_params = 0; // 0 params means append a blank row.
- max_params = 10000; // An arbitrarily high limit that will never realistically be reached.
- }
- else if (!stricmp(suffix, "Insert"))
- {
- bif = BIF_LV_AddInsertModify;
- // Leave min_params at 1. Passing only 1 param to it means "insert a blank row".
- max_params = 10000; // An arbitrarily high limit that will never realistically be reached.
- }
- else if (!stricmp(suffix, "Modify"))
- {
- bif = BIF_LV_AddInsertModify; // Although it shares the same function with "Insert", it can still have its own min/max params.
- min_params = 2;
- max_params = 10000; // An arbitrarily high limit that will never realistically be reached.
- }
- else if (!stricmp(suffix, "Delete"))
- {
- bif = BIF_LV_Delete;
- min_params = 0; // Leave max at its default of 1.
- }
- else if (!stricmp(suffix, "InsertCol"))
- {
- bif = BIF_LV_InsertModifyDeleteCol;
- // Leave min_params at 1 because inserting a blank column ahead of the first column
- // does not seem useful enough to sacrifice the no-parameter mode, which might have
- // potential future uses.
- max_params = 3;
- }
- else if (!stricmp(suffix, "ModifyCol"))
- {
- bif = BIF_LV_InsertModifyDeleteCol;
- min_params = 0;
- max_params = 3;
- }
- else if (!stricmp(suffix, "DeleteCol"))
- bif = BIF_LV_InsertModifyDeleteCol; // Leave min/max set to 1.
- else if (!stricmp(suffix, "SetImageList"))
- {
- bif = BIF_LV_SetImageList;
- max_params = 2; // Leave min at 1.
- }
- else
- return NULL;
- }
- else if (!strnicmp(func_name, "TV_", 3)) // As a built-in function, TV_* can only be a TreeView function.
- {
- suffix = func_name + 3;
- if (!stricmp(suffix, "Add"))
- {
- bif = BIF_TV_AddModifyDelete;
- max_params = 3; // Leave min at its default of 1.
- }
- else if (!stricmp(suffix, "Modify"))
- {
- bif = BIF_TV_AddModifyDelete;
- max_params = 3; // One-parameter mode is "select specified item".
- }
- else if (!stricmp(suffix, "Delete"))
- {
- bif = BIF_TV_AddModifyDelete;
- min_params = 0;
- }
- else if (!stricmp(suffix, "GetParent") || !stricmp(suffix, "GetChild") || !stricmp(suffix, "GetPrev"))
- bif = BIF_TV_GetRelatedItem;
- else if (!stricmp(suffix, "GetCount") || !stricmp(suffix, "GetSelection"))
- {
- bif = BIF_TV_GetRelatedItem;
- min_params = 0;
- max_params = 0;
- }
- else if (!stricmp(suffix, "GetNext")) // Unlike "Prev", Next also supports 0 or 2 parameters.
- {
- bif = BIF_TV_GetRelatedItem;
- min_params = 0;
- max_params = 2;
- }
- else if (!stricmp(suffix, "Get") || !stricmp(suffix, "GetText"))
- {
- bif = BIF_TV_Get;
- min_params = 2;
- max_params = 2;
- }
- else
- return NULL;
- }
- else if (!strnicmp(func_name, "IL_", 3)) // It's an ImageList function.
- {
- suffix = func_name + 3;
- if (!stricmp(suffix, "Create"))
- {
- bif = BIF_IL_Create;
- min_params = 0;
- max_params = 3;
- }
- else if (!stricmp(suffix, "Destroy"))
- {
- bif = BIF_IL_Destroy; // Leave Min/Max set to 1.
- }
- else if (!stricmp(suffix, "Add"))
- {
- bif = BIF_IL_Add;
- min_params = 2;
- max_params = 4;
- }
- else
- return NULL;
- }
- else if (!stricmp(func_name, "SB_SetText"))
- {
- bif = BIF_StatusBar;
- max_params = 3; // Leave min_params at its default of 1.
- }
- else if (!stricmp(func_name, "SB_SetParts"))
- {
- bif = BIF_StatusBar;
- min_params = 0;
- max_params = 255; // 255 params alllows for up to 256 parts, which is SB's max.
- }
- else if (!stricmp(func_name, "SB_SetIcon"))
- {
- bif = BIF_StatusBar;
- max_params = 3; // Leave min_params at its default of 1.
- }
- else if (!stricmp(func_name, "StrLen"))
- bif = BIF_StrLen;
- else if (!stricmp(func_name, "SubStr"))
- {
- bif = BIF_SubStr;
- min_params = 2;
- max_params = 3;
- }
- else if (!stricmp(func_name, "InStr"))
- {
- bif = BIF_InStr;
- min_params = 2;
- max_params = 4;
- }
- else if (!stricmp(func_name, "RegExMatch"))
- {
- bif = BIF_RegEx;
- min_params = 2;
- max_params = 4;
- }
- else if (!stricmp(func_name, "RegExReplace"))
- {
- bif = BIF_RegEx;
- min_params = 2;
- max_params = 6;
- }
- else if (!stricmp(func_name, "GetKeyState"))
- {
- bif = BIF_GetKeyState;
- max_params = 2;
- }
- else if (!stricmp(func_name, "Asc"))
- bif = BIF_Asc;
- else if (!stricmp(func_name, "Chr"))
- bif = BIF_Chr;
- else if (!stricmp(func_name, "NumGet"))
- {
- bif = BIF_NumGet;
- max_params = 3;
- }
- else if (!stricmp(func_name, "NumPut"))
- {
- bif = BIF_NumPut;
- min_params = 2;
- max_params = 4;
- }
- else if (!stricmp(func_name, "IsLabel"))
- bif = BIF_IsLabel;
- else if (!stricmp(func_name, "IsFunc"))
- bif = BIF_IsFunc;
- else if (!stricmp(func_name, "DllCall"))
- {
- bif = BIF_DllCall;
- max_params = 10000; // An arbitrarily high limit that will never realistically be reached.
- }
- else if (!stricmp(func_name, "VarSetCapacity"))
- {
- bif = BIF_VarSetCapacity;
- max_params = 3;
- }
- else if (!stricmp(func_name, "FileExist"))
- bif = BIF_FileExist;
- else if (!stricmp(func_name, "WinExist") || !stricmp(func_name, "WinActive"))
- {
- bif = BIF_WinExistActive;
- min_params = 0;
- max_params = 4;
- }
- else if (!stricmp(func_name, "Round"))
- {
- bif = BIF_Round;
- max_params = 2;
- }
- else if (!stricmp(func_name, "Floor") || !stricmp(func_name, "Ceil"))
- bif = BIF_FloorCeil;
- else if (!stricmp(func_name, "Mod"))
- {
- bif = BIF_Mod;
- min_params = 2;
- max_params = 2;
- }
- else if (!stricmp(func_name, "Abs"))
- bif = BIF_Abs;
- else if (!stricmp(func_name, "Sin"))
- bif = BIF_Sin;
- else if (!stricmp(func_name, "Cos"))
- bif = BIF_Cos;
- else if (!stricmp(func_name, "Tan"))
- bif = BIF_Tan;
- else if (!stricmp(func_name, "ASin") || !stricmp(func_name, "ACos"))
- bif = BIF_ASinACos;
- else if (!stricmp(func_name, "ATan"))
- bif = BIF_ATan;
- else if (!stricmp(func_name, "Exp"))
- bif = BIF_Exp;
- else if (!stricmp(func_name, "Sqrt") || !stricmp(func_name, "Log") || !stricmp(func_name, "Ln"))
- bif = BIF_SqrtLogLn;
- else if (!stricmp(func_name, "OnMessage"))
- {
- bif = BIF_OnMessage;
- max_params = 3; // Leave min at 1.
- // By design, scripts that use OnMessage are persistent by default. Doing this here
- // also allows WinMain() to later detect whether this script should become #SingleInstance.
- // Note: Don't directly change g_AllowOnlyOneInstance here in case the remainder of the
- // script-loading process comes across any explicit uses of #SingleInstance, which would
- // override the default set here.
- g_persistent = true;
- }
- else if (!stricmp(func_name, "RegisterCallback"))
- {
- bif = BIF_RegisterCallback;
- max_params = 4; // Leave min_params at 1.
- }
- else
- return NULL; // Maint: There may be other lines above that also return NULL.
- // Since above didn't return, this is a built-in function that hasn't yet been added to the list.
- // Add it now:
- if ( !(pfunc = AddFunc(func_name, aFuncNameLength, true)) )
- return NULL;
- pfunc->mBIF = bif;
- pfunc->mMinParams = min_params;
- pfunc->mParamCount = max_params;
- return pfunc;
- }
- Func *Script::AddFunc(char *aFuncName, size_t aFuncNameLength, bool aIsBuiltIn)
- // This function should probably not be called by anyone except FindOrAddFunc, which has already done
- // the dupe-checking.
- // Returns the address of the new function or NULL on failure.
- // The caller must already have verified that this isn't a duplicate function.
- {
- if (!aFuncNameLength) // Caller didn't specify, so use the entire string.
- aFuncNameLength = strlen(aFuncName);
- if (aFuncNameLength > MAX_VAR_NAME_LENGTH)
- {
- // Dynamic function-calls such as MyFuncArray%i%() aren't currently supported, so the first
- // item below is commented out:
- // Load-time callers should check for this. But at runtime, it's possible for a dynamically
- // resolved function name to be too long. Note that aFuncName should be the exact variable
- // name and does not need to be truncated to aFuncNameLength whenever this error occurs
- // (i.e. at runtime):
- //if (mIsReadyToExecute) // Runtime error.
- // ScriptError("Function name too long." ERR_ABORT, aFuncName);
- //else
- ScriptError("Function name too long.", aFuncName);
- return NULL;
- }
- // Make a temporary copy that includes only the first aFuncNameLength characters from aFuncName:
- char func_name[MAX_VAR_NAME_LENGTH + 1];
- strlcpy(func_name, aFuncName, aFuncNameLength + 1); // See explanation above. +1 to convert length to size.
- // In the future, it might be best to add another check here to disallow function names that consist
- // entirely of numbers. However, this hasn't been done yet because:
- // 1) Not sure if there will ever be a good enough reason.
- // 2) Even if it's done in the far future, it won't break many scripts (pure-numeric functions should be very rare).
- // 3) Those scripts that are broken are not broken in a bad way because the pre-parser will generate a
- // load-time error, which is easy to fix (unlike runtime errors, which require that part of the script
- // to actually execute).
- if (!Var::ValidateName(func_name, mIsReadyToExecute, DISPLAY_FUNC_ERROR)) // Variable and function names are both validated the same way.
- // Above already displayed error for us. This can happen at loadtime or runtime (e.g. StringSplit).
- return NULL;
- // Allocate some dynamic memory to pass to the constructor:
- char *new_name = SimpleHeap::Malloc(func_name, aFuncNameLength);
- if (!new_name)
- // It already displayed the error for us. These mem errors are so unusual that we're not going
- // to bother varying the error message to include ERR_ABORT if this occurs during runtime.
- return NULL;
- Func *the_new_func = new Func(new_name, aIsBuiltIn);
- if (!the_new_func)
- {
- ScriptError(ERR_OUTOFMEM);
- return NULL;
- }
- // v1.0.47: The following ISN'T done because it would slow down commonly used functions. This is because
- // commonly-called functions like InStr() tend to be added first (since they appear so often throughout
- // the script); thus subsequent lookups are fast if they are kept at the beginning of the list rather
- // than being displaced to the end by all other functions).
- // NOT DONE for the reason above:
- // Unlike most of the other link lists, attach new items at the beginning of the list because
- // that allows the standard/user library feature to perform much better for scripts that have hundreds
- // of functions. This is because functions brought in dynamically from a library will then be at the
- // beginning of the list, which allows the function lookup that immediately follows library-loading to
- // find a match almost immediately.
- if (!mFirstFunc) // The list is empty, so this will be the first and last item.
- mFirstFunc = the_new_func;
- else
- mLastFunc->mNextFunc = the_new_func;
- // This must be done after the above:
- mLastFunc = the_new_func; // There's at least one spot in the code that relies on mLastFunc being the most recently added function.
- return the_new_func;
- }
- size_t Line::ArgIndexLength(int aArgIndex)
- // This function is similar to ArgToInt(), so maintain them together.
- // "ArgLength" is the arg's fully resolved, dereferenced length during runtime.
- // Callers must call this only at times when sArgDeref and sArgVar are defined/meaningful.
- // Caller must ensure that aArgIndex is 0 or greater.
- // ArgLength() was added in v1.0.44.14 to help its callers improve performance by avoiding
- // costly calls to strlen() (which is especially beneficial for huge strings).
- {
- #ifdef _DEBUG
- if (aArgIndex < 0)
- {
- LineError("DEBUG: BAD", WARN);
- aArgIndex = 0; // But let it continue.
- }
- #endif
- if (aArgIndex >= mArgc) // Arg doesn't exist, so don't try accessing sArgVar (unlike sArgDeref, it wouldn't be valid to do so).
- return 0; // i.e. treat it as the empty string.
- // The length is not known and must be calculcated in the following situations:
- // - The arg consists of more than just a single isolated variable name (not possible if the arg is
- // ARG_TYPE_INPUT_VAR).
- // - The arg is a built-in variable, in which case the length isn't known, so it must be derived from
- // the string copied into sArgDeref[] by an earlier stage.
- // - The arg is a normal variable but it's VAR_ATTRIB_BINARY_CLIP. In such cases, our callers do not
- // recognize/support binary-clipboard as binary and want the apparent length of the string returned
- // (i.e. strlen(), which takes into account the position of the first binary zero wherever it may be).
- if (sArgVar[aArgIndex])
- {
- Var &var = *sArgVar[aArgIndex]; // For performance and convenience.
- if ( var.Type() == VAR_NORMAL // This and below ordered for short-circuit performance based on types of input expected from caller.
- && !(g_act[mActionType].MaxParamsAu2WithHighBit & 0x80) // Although the ones that have the highbit set are hereby omitted from the fast method, the nature of almost all of the highbit commands is such that their performance won't be measurably affected. See ArgMustBeDereferenced() for more info.
- && (g_NoEnv || var.HasContents()) // v1.0.46.02: Recognize environment variables (when g_NoEnv==FALSE) by falling through to strlen() for them.
- && &var != g_ErrorLevel ) // Mostly for maintainability because the following situation is very rare: If it's g_ErrorLevel, use the deref version instead because if g_ErrorLevel is an input variable in the caller's command, and the caller changes ErrorLevel (such as to set a default) prior to calling this function, the changed/new ErrorLevel will be used rather than its original value (which is usually undesirable).
- //&& !var.IsBinaryClip()) // This check isn't necessary because the line below handles it.
- return var.LengthIgnoreBinaryClip(); // Do it the fast way (unless it's binary clipboard, in which case this call will internally call strlen()).
- }
- // Otherwise, length isn't known due to no variable, a built-in variable, or an environment variable.
- // So do it the slow way.
- return strlen(sArgDeref[aArgIndex]);
- }
- __int64 Line::ArgIndexToInt64(int aArgIndex)
- // This function is similar to ArgIndexLength(), so maintain them together.
- // Callers must call this only at times when sArgDeref and sArgVar are defined/meaningful.
- // Caller must ensure that aArgIndex is 0 or greater.
- {
- #ifdef _DEBUG
- if (aArgIndex < 0)
- {
- LineError("DEBUG: BAD", WARN);
- aArgIndex = 0; // But let it continue.
- }
- #endif
- if (aArgIndex >= mArgc) // See ArgIndexLength() for comments.
- return 0; // i.e. treat it as ATOI64("").
- // SEE THIS POSITION IN ArgIndexLength() FOR IMPORTANT COMMENTS ABOUT THE BELOW.
- if (sArgVar[aArgIndex])
- {
- Var &var = *sArgVar[aArgIndex];
- if ( var.Type() == VAR_NORMAL // See ArgIndexLength() for comments about this line and below.
- && !(g_act[mActionType].MaxParamsAu2WithHighBit & 0x80)
- && (g_NoEnv || var.HasContents())
- && &var != g_ErrorLevel
- && !var.IsBinaryClip() )
- return var.ToInt64(FALSE);
- }
- // Otherwise:
- return ATOI64(sArgDeref[aArgIndex]); // See ArgIndexLength() for comments.
- }
- double Line::ArgIndexToDouble(int aArgIndex)
- // This function is similar to ArgIndexLength(), so maintain them together.
- // Callers must call this only at times when sArgDeref and sArgVar are defined/meaningful.
- // Caller must ensure that aArgIndex is 0 or greater.
- {
- #ifdef _DEBUG
- if (aArgIndex < 0)
- {
- LineError("DEBUG: BAD", WARN);
- aArgIndex = 0; // But let it continue.
- }
- #endif
- if (aArgIndex >= mArgc) // See ArgIndexLength() for comments.
- return 0.0; // i.e. treat it as ATOF("").
- // SEE THIS POSITION IN ARGLENGTH() FOR IMPORTANT COMMENTS ABOUT THE BELOW.
- if (sArgVar[aArgIndex])
- {
- Var &var = *sArgVar[aArgIndex];
- if ( var.Type() == VAR_NORMAL // See ArgIndexLength() for comments about this line and below.
- && !(g_act[mActionType].MaxParamsAu2WithHighBit & 0x80)
- && (g_NoEnv || var.HasContents())
- && &var != g_ErrorLevel
- && !var.IsBinaryClip() )
- return var.ToDouble(FALSE);
- }
- // Otherwise:
- return ATOF(sArgDeref[aArgIndex]); // See ArgIndexLength() for comments.
- }
- Var *Line::ResolveVarOfArg(int aArgIndex, bool aCreateIfNecessary)
- // Returns NULL on failure. Caller has ensured that none of this arg's derefs are function-calls.
- // Args that are input or output variables are normally resolved at load-time, so that
- // they contain a pointer to their Var object. This is done for performance. However,
- // in order to support dynamically resolved variables names like AutoIt2 (e.g. arrays),
- // we have to do some extra work here at runtime.
- // Callers specify false for aCreateIfNecessary whenever the contents of the variable
- // they're trying to find is unimportant. For example, dynamically built input variables,
- // such as "StringLen, length, array%i%", do not need to be created if they weren't
- // previously assigned to (i.e. they weren't previously used as an output variable).
- // In the above example, the array element would never be created here. But if the output
- // variable were dynamic, our call would have told us to create it.
- {
- // The requested ARG isn't even present, so it can't have a variable. Currently, this should
- // never happen because the loading procedure ensures that input/output args are not marked
- // as variables if they are blank (and our caller should check for this and not call in that case):
- if (aArgIndex >= mArgc)
- return NULL;
- ArgStruct &this_arg = mArg[aArgIndex]; // For performance and convenience.
- // Since this function isn't inline (since it's called so frequently), there isn't that much more
- // overhead to doing this check, even though it shouldn't be needed since it's the caller's
- // responsibility:
- if (this_arg.type == ARG_TYPE_NORMAL) // Arg isn't an input or output variable.
- return NULL;
- if (!*this_arg.text) // The arg's variable is not one that needs to be dynamically resolved.
- return VAR(this_arg); // Return the var's address that was already determined at load-time.
- // The above might return NULL in the case where the arg is optional (i.e. the command allows
- // the var name to be omitted). But in that case, the caller should either never have called this
- // function or should check for NULL upon return. UPDATE: This actually never happens, see
- // comment above the "if (aArgIndex >= mArgc)" line.
- // Static to correspond to the static empty_var further below. It needs the memory area
- // to support resolving dynamic environment variables. In the following example,
- // the result will be blank unless the first line is present (without this fix here):
- //null = %SystemRoot% ; bogus line as a required workaround in versions prior to v1.0.16
- //thing = SystemRoot
- //StringTrimLeft, output, %thing%, 0
- //msgbox %output%
- static char sVarName[MAX_VAR_NAME_LENGTH + 1]; // Will hold the dynamically built name.
- // At this point, we know the requested arg is a variable that must be dynamically resolved.
- // This section is similar to that in ExpandArg(), so they should be maintained together:
- char *pText = this_arg.text; // Start at the begining of this arg's text.
- int var_name_length = 0;
- if (this_arg.deref) // There's at least one deref.
- {
- // Caller has ensured that none of these derefs are function calls (i.e. deref->is_function is alway false).
- for (DerefType *deref = this_arg.deref // Start off by looking for the first deref.
- ; deref->marker; ++deref) // A deref with a NULL marker terminates the list.
- {
- // FOR EACH DEREF IN AN ARG (if we're here, there's at least one):
- // Copy the chars that occur prior to deref->marker into the buffer:
- for (; pText < deref->marker && var_name_length < MAX_VAR_NAME_LENGTH; sVarName[var_name_length++] = *pText++);
- if (var_name_length >= MAX_VAR_NAME_LENGTH && pText < deref->marker) // The variable name would be too long!
- {
- // This type of error is just a warning because this function isn't set up to cause a true
- // failure. This is because the use of dynamically named variables is rare, and only for
- // people who should know what they're doing. In any case, when the caller of this
- // function called it to resolve an output variable, it will see tha the result is
- // NULL and terminate the current subroutine.
- #define DYNAMIC_TOO_LONG "This dynamically built variable name is too long." \
- " If this variable was not intended to be dynamic, remove the % symbols from it."
- LineError(DYNAMIC_TOO_LONG, FAIL, this_arg.text);
- return NULL;
- }
- // Now copy the contents of the dereferenced var. For all cases, aBuf has already
- // been verified to be large enough, assuming the value hasn't changed between the
- // time we were called and the time the caller calculated the space needed.
- if (deref->var->Get() > (VarSizeType)(MAX_VAR_NAME_LENGTH - var_name_length)) // The variable name would be too long!
- {
- LineError(DYNAMIC_TOO_LONG, FAIL, this_arg.text);
- return NULL;
- }
- var_name_length += deref->var->Get(sVarName + var_name_length);
- // Finally, jump over the dereference text. Note that in the case of an expression, there might not
- // be any percent signs within the text of the dereference, e.g. x + y, not %x% + %y%.
- pText += deref->length;
- }
- }
- // Copy any chars that occur after the final deref into the buffer:
- for (; *pText && var_name_length < MAX_VAR_NAME_LENGTH; sVarName[var_name_length++] = *pText++);
- if (var_name_length >= MAX_VAR_NAME_LENGTH && *pText) // The variable name would be too long!
- {
- LineError(DYNAMIC_TOO_LONG, FAIL, this_arg.text);
- return NULL;
- }
-
- if (!var_name_length)
- {
- LineError("This dynamic variable is blank. If this variable was not intended to be dynamic,"
- " remove the % symbols from it.", FAIL, this_arg.text);
- return NULL;
- }
- // Terminate the buffer, even if nothing was written into it:
- sVarName[var_name_length] = '\0';
- static Var empty_var(sVarName, (void *)VAR_NORMAL, false); // Must use sVarName here. See comment above for why.
- Var *found_var;
- if (!aCreateIfNecessary)
- {
- // Now we've dynamically build the variable name. It's possible that the name is illegal,
- // so check that (the name is automatically checked by FindOrAddVar(), so we only need to
- // check it if we're not calling that):
- if (!Var::ValidateName(sVarName, g_script.mIsReadyToExecute))
- return NULL; // Above already displayed error for us.
- // The use of ALWAYS_PREFER_LOCAL below improves flexibility of assume-global functions
- // by allowing this command to resolve to a local first if such a local exists:
- if (found_var = g_script.FindVar(sVarName, var_name_length, NULL, ALWAYS_PREFER_LOCAL)) // Assign.
- return found_var;
- // At this point, this is either a non-existent variable or a reserved/built-in variable
- // that was never statically referenced in the script (only dynamically), e.g. A_IPAddress%A_Index%
- if (Script::GetVarType(sVarName) == (void *)VAR_NORMAL)
- // If not found: for performance reasons, don't create it because caller just wants an empty variable.
- return &empty_var;
- //else it's the clipboard or some other built-in variable, so continue onward so that the
- // variable gets created in the variable list, which is necessary to allow it to be properly
- // dereferenced, e.g. in a script consisting of only the following:
- // Loop, 4
- // StringTrimRight, IP, A_IPAddress%A_Index%, 0
- }
- // Otherwise, aCreateIfNecessary is true or we want to create this variable unconditionally for the
- // reason described above. ALWAYS_PREFER_LOCAL is used so that any existing local variable will
- // take precedence over a global of the same name when assume-global is in effect. If neither type
- // of variable exists, a global variable will be created if assume-global is in effect.
- if ( !(found_var = g_script.FindOrAddVar(sVarName, var_name_length, ALWAYS_PREFER_LOCAL)) )
- return NULL; // Above will already have displayed the error.
- if (this_arg.type == ARG_TYPE_OUTPUT_VAR && VAR_IS_READONLY(*found_var))
- {
- LineError(ERR_VAR_IS_READONLY, FAIL, sVarName);
- return NULL; // Don't return the var, preventing the caller from assigning to it.
- }
- else
- return found_var;
- }
- Var *Script::FindOrAddVar(char *aVarName, size_t aVarNameLength, int aAlwaysUse, bool *apIsException)
- // Caller has ensured that aVarName isn't NULL.
- // Returns the Var whose name matches aVarName. If it doesn't exist, it is created.
- {
- if (!*aVarName)
- return NULL;
- int insert_pos;
- bool is_local; // Used to detect which type of var should be added in case the result of the below is NULL.
- Var *var;
- if (var = FindVar(aVarName, aVarNameLength, &insert_pos, aAlwaysUse, apIsException, &is_local))
- return var;
- // Otherwise, no match found, so create a new var. This will return NULL if there was a problem,
- // in which case AddVar() will already have displayed the error:
- return AddVar(aVarName, aVarNameLength, insert_pos, is_local);
- }
- Var *Script::FindVar(char *aVarName, size_t aVarNameLength, int *apInsertPos, int aAlwaysUse
- , bool *apIsException, bool *apIsLocal)
- // Caller has ensured that aVarName isn't NULL. It must also ignore the contents of apInsertPos when
- // a match (non-NULL value) is returned.
- // Returns the Var whose name matches aVarName. If it doesn't exist, NULL is returned.
- // If caller provided a non-NULL apInsertPos, it will be given a the array index that a newly
- // inserted item should have to keep the list in sorted order (which also allows the ListVars command
- // to display the variables in alphabetical order).
- {
- if (!*aVarName)
- return NULL;
- if (!aVarNameLength) // Caller didn't specify, so use the entire string.
- aVarNameLength = strlen(aVarName);
- // For the below, no error is reported because callers don't want that. Instead, simply return
- // NULL to indicate that names that are illegal or too long are not found. When the caller later
- // tries to add the variable, it will get an error then:
- if (aVarNameLength > MAX_VAR_NAME_LENGTH)
- return NULL;
- // The following copy is made because it allows the various searches below to use stricmp() instead of
- // strlicmp(), which close to doubles their performance. The copy includes only the first aVarNameLength
- // characters from aVarName:
- char var_name[MAX_VAR_NAME_LENGTH + 1];
- strlcpy(var_name, aVarName, aVarNameLength + 1); // +1 to convert length to size.
- global_struct &g = *::g; // Reduces code size and may improve performance.
- Var *found_var = NULL; // Set default.
- bool is_local;
- if (aAlwaysUse == ALWAYS_USE_GLOBAL)
- is_local = false;
- else if (aAlwaysUse == ALWAYS_USE_LOCAL)
- // v1.0.44.10: The following was changed from it's former value of "true" so that places further below
- // (including passing is_local is call to AddVar()) don't have to ensure that g.CurrentFunc!=NULL.
- // This fixes a crash that occured when a caller specified ALWAYS_USE_LOCAL even though the current
- // thread isn't actually inside a *called* function (perhaps meaning things like a timed subroutine
- // that lies inside a "container function").
- // Some callers like SYSGET_CMD_MONITORAREA might try to find/add a local array if they see that their
- // base variable is classified as local (such classification occurs at loadtime, but only for non-dynamic
- // variable references). But the current thread entered a "container function" by means other than a
- // function-call (such as SetTimer), not only is g.CurrentFunc NULL, but there's no easy way to discover
- // which function owns the currently executing line (a means could be added to the class "Var" or "Line"
- // but doesn't seem worth it yet due to performance and memory reduction).
- is_local = (g.CurrentFunc != NULL);
- else if (aAlwaysUse == ALWAYS_PREFER_LOCAL)
- {
- if (g.CurrentFunc) // Caller relies on us to do this final check.
- is_local = true;
- else
- {
- is_local = false;
- aAlwaysUse = ALWAYS_USE_GLOBAL; // Override aAlwaysUse for maintainability, in case there are more references to it below.
- }
- }
- else // aAlwaysUse == ALWAYS_USE_DEFAULT
- {
- is_local = g.CurrentFunc && g.CurrentFunc->mDefaultVarType != VAR_DECLARE_GLOBAL; // i.e. ASSUME_LOCAL or ASSUME_NONE
- if (mFuncExceptionVar) // Caller has ensured that this non-NULL if and only if g.CurrentFunc is non-NULL.
- {
- int i;
- for (i = 0; i < mFuncExceptionVarCount; ++i)
- {
- if (!stricmp(var_name, mFuncExceptionVar[i]->mName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- {
- is_local = !is_local; // Since it's an exception, it's always the opposite of what it would have been.
- found_var = mFuncExceptionVar[i];
- break;
- }
- }
- // The following section is necessary because a function's parameters are not put into the
- // exception list during load-time. Thus, for an assume-global function, these are basically
- // treated as exceptions too.
- // If this function is one that assumes variables are global, the function's parameters are
- // implicitly declared local because parameters are always local:
- // Since the following is inside this block, it is checked only at loadtime. It doesn't need
- // to be checked at runtime because most things that resolve input variables or variables whose
- // contents will be read (as compared to something that tries to create a dynamic variable, such
- // as ResolveVarOfArg() for an output variable) at runtime use the ALWAYS_PREFER_LOCAL flag to
- // indicate that a local of the same name as a global should take precedence. This adds more
- // flexibility/benefit than its costs in terms of confusion because otherwise there would be
- // no way to dynamically reference the local variables of an assume-global function.
- if (g.CurrentFunc->mDefaultVarType == VAR_DECLARE_GLOBAL && !is_local) // g.CurrentFunc is also known to be non-NULL in this case.
- {
- for (i = 0; i < g.CurrentFunc->mParamCount; ++i)
- if (!stricmp(var_name, g.CurrentFunc->mParam[i].var->mName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- {
- is_local = true;
- found_var = g.CurrentFunc->mParam[i].var;
- break;
- }
- }
- } // if (there is an exception list)
- } // aAlwaysUse == ALWAYS_USE_DEFAULT
- // Above has ensured that g.CurrentFunc!=NULL whenever is_local==true.
- if (apIsLocal) // Its purpose is to inform caller of type it would have been in case we don't find a match.
- *apIsLocal = is_local; // And it stays this way even if globals will be searched because caller wants that. In other words, a local var is created by default when there is not existing global or local.
- if (apInsertPos) // Set default. Caller should ignore the value when match is found.
- *apInsertPos = -1;
- if (apIsException)
- *apIsException = (found_var != NULL);
- if (found_var) // Match found (as an exception or load-time "is parameter" exception).
- return found_var; // apInsertPos does not need to be set because caller doesn't need it when match is found.
- // Init for binary search loop:
- int left, right, mid, result; // left/right must be ints to allow them to go negative and detect underflow.
- Var **var; // An array of pointers-to-var.
- if (is_local)
- {
- var = g.CurrentFunc->mVar;
- right = g.CurrentFunc->mVarCount - 1;
- }
- else
- {
- var = mVar;
- right = mVarCount - 1;
- }
- // Binary search:
- for (left = 0; left <= right;) // "right" was already initialized above.
- {
- mid = (left + right) / 2;
- result = stricmp(var_name, var[mid]->mName); // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- if (result > 0)
- left = mid + 1;
- else if (result < 0)
- right = mid - 1;
- else // Match found.
- return var[mid];
- }
- // Since above didn't return, no match was found in the main list, so search the lazy list if there
- // is one. If there's no lazy list, the value of "left" established above will be used as the
- // insertion point further below:
- if (is_local)
- {
- var = g.CurrentFunc->mLazyVar;
- right = g.CurrentFunc->mLazyVarCount - 1;
- }
- else
- {
- var = mLazyVar;
- right = mLazyVarCount - 1;
- }
- if (var) // There is a lazy list to search (and even if the list is empty, left must be reset to 0 below).
- {
- // Binary search:
- for (left = 0; left <= right;) // "right" was already initialized above.
- {
- mid = (left + right) / 2;
- result = stricmp(var_name, var[mid]->mName); // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- if (result > 0)
- left = mid + 1;
- else if (result < 0)
- right = mid - 1;
- else // Match found.
- return var[mid];
- }
- }
- // Since above didn't return, no match was found and "left" always contains the position where aVarName
- // should be inserted to keep the list sorted. The item is always inserted into the lazy list unless
- // there is no lazy list.
- // Set the output parameter, if present:
- if (apInsertPos) // Caller wants this value even if we'll be resorting to searching the global list below.
- *apInsertPos = left; // This is the index a newly inserted item should have to keep alphabetical order.
- // Since no match was found, if this is a local fall back to searching the list of globals at runtime
- // if the caller didn't insist on a particular type:
- if (is_local)
- {
- if (aAlwaysUse == ALWAYS_PREFER_LOCAL)
- {
- // In this case, callers want to fall back to globals when a local wasn't found. However,
- // they want the insertion (if our caller will be doing one) to insert according to the
- // current assume-mode. Therefore, if the mode is assume-global, pass the apIsLocal
- // and apInsertPos variables to FindVar() so that it will update them to be global.
- // Otherwise, do not pass them since they were already set correctly by us above.
- if (g.CurrentFunc->mDefaultVarType == VAR_DECLARE_GLOBAL)
- return FindVar(aVarName, aVarNameLength, apInsertPos, ALWAYS_USE_GLOBAL, NULL, apIsLocal);
- else
- return FindVar(aVarName, aVarNameLength, NULL, ALWAYS_USE_GLOBAL);
- }
- if (aAlwaysUse == ALWAYS_USE_DEFAULT && mIsReadyToExecute) // In this case, fall back to globals only at runtime.
- return FindVar(aVarName, aVarNameLength, NULL, ALWAYS_USE_GLOBAL);
- }
- // Otherwise, since above didn't return:
- return NULL; // No match.
- }
- Var *Script::AddVar(char *aVarName, size_t aVarNameLength, int aInsertPos, int aIsLocal)
- // Returns the address of the new variable or NULL on failure.
- // Caller must ensure that g->CurrentFunc!=NULL whenever aIsLocal==true.
- // Caller must ensure that aVarName isn't NULL and that this isn't a duplicate variable name.
- // In addition, it has provided aInsertPos, which is the insertion point so that the list stays sorted.
- // Finally, aIsLocal has been provided to indicate which list, global or local, should receive this
- // new variable. aIsLocal is normally 0 or 1 (boolean), but it may be 2 to indicate "it's a local AND a
- // function's parameter".
- {
- if (!*aVarName) // Should never happen, so just silently indicate failure.
- return NULL;
- if (!aVarNameLength) // Caller didn't specify, so use the entire string.
- aVarNameLength = strlen(aVarName);
- if (aVarNameLength > MAX_VAR_NAME_LENGTH)
- {
- // Load-time callers should check for this. But at runtime, it's possible for a dynamically
- // resolved variable name to be too long. Note that aVarName should be the exact variable
- // name and does not need to be truncated to aVarNameLength whenever this error occurs
- // (i.e. at runtime):
- if (mIsReadyToExecute) // Runtime error.
- ScriptError("Variable name too long." ERR_ABORT, aVarName);
- else
- ScriptError("Variable name too long.", aVarName);
- return NULL;
- }
- // Make a temporary copy that includes only the first aVarNameLength characters from aVarName:
- char var_name[MAX_VAR_NAME_LENGTH + 1];
- strlcpy(var_name, aVarName, aVarNameLength + 1); // See explanation above. +1 to convert length to size.
- if (!Var::ValidateName(var_name, mIsReadyToExecute))
- // Above already displayed error for us. This can happen at loadtime or runtime (e.g. StringSplit).
- return NULL;
- // Not necessary or desirable to add built-in variables to a function's list of locals. Always keep
- // built-in vars in the global list for efficiency and to keep them out of ListVars. Note that another
- // section at loadtime displays an error for any attempt to explicitly declare built-in variables as
- // either global or local.
- void *var_type = GetVarType(var_name);
- if (aIsLocal && (var_type != (void *)VAR_NORMAL || !stricmp(var_name, "ErrorLevel"))) // Attempt to create built-in variable as local.
- {
- if (aIsLocal == 1) // It's not a UDF's parameter, so fall back to the global built-in variable of this name rather than displaying an error.
- return FindOrAddVar(var_name, aVarNameLength, ALWAYS_USE_GLOBAL); // Force find-or-create of global.
- else // aIsLocal == 2, which means "this is a local variable and a function's parameter".
- {
- ScriptError("Illegal parameter name.", aVarName); // Short message since so rare.
- return NULL;
- }
- }
- // Allocate some dynamic memory to pass to the constructor:
- char *new_name = SimpleHeap::Malloc(var_name, aVarNameLength);
- if (!new_name)
- // It already displayed the error for us. These mem errors are so unusual that we're not going
- // to bother varying the error message to include ERR_ABORT if this occurs during runtime.
- return NULL;
- Var *the_new_var = new Var(new_name, var_type, aIsLocal != 0); // , aAttrib);
- if (the_new_var == NULL)
- {
- ScriptError(ERR_OUTOFMEM);
- return NULL;
- }
- if (aIsLocal == 1 && g->CurrentFunc->mDefaultVarType == VAR_DECLARE_STATIC)
- // v1.0.48: Lexikos: Current function is assume-static, so set static attribute.
- // This will be overwritten (again) if this variable is being explicitly declared "local".
- the_new_var->ConvertToStatic();
- // If there's a lazy var list, aInsertPos provided by the caller is for it, so this new variable
- // always gets inserted into that list because there's always room for one more (because the
- // previously added variable would have purged it if it had reached capacity).
- Var **lazy_var = aIsLocal ? g->CurrentFunc->mLazyVar : mLazyVar;
- int &lazy_var_count = aIsLocal ? g->CurrentFunc->mLazyVarCount : mLazyVarCount; // Used further below too.
- if (lazy_var)
- {
- if (aInsertPos != lazy_var_count) // Need to make room at the indicated position for this variable.
- memmove(lazy_var + aInsertPos + 1, lazy_var + aInsertPos, (lazy_var_count - aInsertPos) * sizeof(Var *));
- //else both are zero or the item is being inserted at the end of the list, so it's easy.
- lazy_var[aInsertPos] = the_new_var;
- ++lazy_var_count;
- // In a testing creating between 200,000 and 400,000 variables, using a size of 1000 vs. 500 improves
- // the speed by 17%, but when you substract out the binary search time (leaving only the insert time),
- // the increase is more like 34%. But there is a diminishing return after that: Going to 2000 only
- // gains 20%, and to 4000 only gains an addition 10%. Therefore, to conserve memory in functions that
- // have so many variables that the lazy list is used, a good trade-off seems to be 2000 (8 KB of memory)
- // per function that needs it.
- #define MAX_LAZY_VARS 2000 // Don't make this larger than 90000 without altering the incremental increase of alloc_count further below.
- if (lazy_var_count < MAX_LAZY_VARS) // The lazy list hasn't yet reached capacity, so no need to merge it into the main list.
- return the_new_var;
- }
- // Since above didn't return, either there is no lazy list or the lazy list is full and needs to be
- // merged into the main list.
- // Create references to whichever variable list (local or global) is being acted upon. These
- // references simplify the code:
- Var **&var = aIsLocal ? g->CurrentFunc->mVar : mVar; // This needs to be a ref. too in case it needs to be realloc'd.
- int &var_count = aIsLocal ? g->CurrentFunc->mVarCount : mVarCount;
- int &var_count_max = aIsLocal ? g->CurrentFunc->mVarCountMax : mVarCountMax;
- int alloc_count;
- // Since the above would have returned if the lazy list is present but not yet full, if the left side
- // of the OR below is false, it also means that lazy_var is NULL. Thus lazy_var==NULL is implicit for the
- // right side of the OR:
- if ((lazy_var && var_count + MAX_LAZY_VARS > var_count_max) || var_count == var_count_max)
- {
- // Increase by orders of magnitude each time because realloc() is probably an expensive operation
- // in terms of hurting performance. So here, a little bit of memory is sacrificed to improve
- // the expected level of performance for scripts that use hundreds of thousands of variables.
- if (!var_count_max)
- alloc_count = aIsLocal ? 100 : 1000; // 100 conserves memory since every function needs such a block, and most functions have much fewer than 100 local variables.
- else if (var_count_max < 1000)
- alloc_count = 1000;
- else if (var_count_max < 9999) // Making this 9999 vs. 10000 allows an exact/whole number of lazy_var blocks to fit into main indices between 10000 and 99999
- alloc_count = 9999;
- else if (var_count_max < 100000)
- {
- alloc_count = 100000;
- // This is also the threshold beyond which the lazy list is used to accelerate performance.
- // Create the permanent lazy list:
- Var **&lazy_var = aIsLocal ? g->CurrentFunc->mLazyVar : mLazyVar;
- if ( !(lazy_var = (Var **)malloc(MAX_LAZY_VARS * sizeof(Var *))) )
- {
- ScriptError(ERR_OUTOFMEM);
- return NULL;
- }
- }
- else if (var_count_max < 1000000)
- alloc_count = 1000000;
- else
- alloc_count = var_count_max + 1000000; // i.e. continue to increase by 4MB (1M*4) each time.
- Var **temp = (Var **)realloc(var, alloc_count * sizeof(Var *)); // If passed NULL, realloc() will do a malloc().
- if (!temp)
- {
- ScriptError(ERR_OUTOFMEM);
- return NULL;
- }
- var = temp;
- var_count_max = alloc_count;
- }
- if (!lazy_var)
- {
- if (aInsertPos != var_count) // Need to make room at the indicated position for this variable.
- memmove(var + aInsertPos + 1, var + aInsertPos, (var_count - aInsertPos) * sizeof(Var *));
- //else both are zero or the item is being inserted at the end of the list, so it's easy.
- var[aInsertPos] = the_new_var;
- ++var_count;
- return the_new_var;
- }
- //else the variable was already inserted into the lazy list, so the above is not done.
- // Since above didn't return, the lazy list is not only present, but full because otherwise it
- // would have returned higher above.
- // Since the lazy list is now at its max capacity, merge it into the main list (if the
- // main list was at capacity, this section relies upon the fact that the above already
- // increased its capacity by an amount far larger than the number of items containined
- // in the lazy list).
- // LAZY LIST: Although it's not nearly as good as hashing (which might be implemented in the future,
- // though it would be no small undertaking since it affects so many design aspects, both load-time
- // and runtime for scripts), this method of accelerating insertions into a binary search array is
- // enormously beneficial because it improves the scalability of binary-search by two orders
- // of magnitude (from about 100,000 variables to at least 5M). Credit for the idea goes to Lazlo.
- // DETAILS:
- // The fact that this merge operation is so much faster than total work required
- // to insert each one into the main list is the whole reason for having the lazy
- // list. In other words, the large memmove() that would otherwise be required
- // to insert each new variable into the main list is completely avoided. Large memmove()s
- // become dramatically more costly than small ones because apparently they can't fit into
- // the CPU cache, so the operation would take hundreds or even thousands of times longer
- // depending on the speed difference between main memory and CPU cache. But above and
- // beyond the CPU cache issue, the lazy sorting method results in vastly less memory
- // being moved than would have been required without it, so even if the CPU doesn't have
- // a cache, the lazy list method vastly increases performance for scripts that have more
- // than 100,000 variables, allowing at least 5 million variables to be created without a
- // dramatic reduction in performance.
- char *target_name;
- Var **insert_pos, **insert_pos_prev;
- int i, left, right, mid;
- // Append any items from the lazy list to the main list that are alphabetically greater than
- // the last item in the main list. Above has already ensured that the main list is large enough
- // to accept all items in the lazy list.
- for (i = lazy_var_count - 1, target_name = var[var_count - 1]->mName
- ; i > -1 && stricmp(target_name, lazy_var[i]->mName) < 0
- ; --i);
- // Above is a self-contained loop.
- // Now do a separate loop to append (in the *correct* order) anything found above.
- for (int j = i + 1; j < lazy_var_count; ++j) // Might have zero iterations.
- var[var_count++] = lazy_var[j];
- lazy_var_count = i + 1; // The number of items that remain after moving out those that qualified.
- // This will have zero iterations if the above already moved them all:
- for (insert_pos = var + var_count, i = lazy_var_count - 1; i > -1; --i)
- {
- // Modified binary search that relies on the fact that caller has ensured a match will never
- // be found in the main list for each item in the lazy list:
- for (target_name = lazy_var[i]->mName, left = 0, right = (int)(insert_pos - var - 1); left <= right;)
- {
- mid = (left + right) / 2;
- if (stricmp(target_name, var[mid]->mName) > 0) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- left = mid + 1;
- else // it must be < 0 because caller has ensured it can't be equal (i.e. that there will be no match)
- right = mid - 1;
- }
- // Now "left" contains the insertion point is is known to be less than var_count due to a previous
- // set of loops above. Make a gap there large enough to hold all items because that allows a
- // smaller total amount of memory to be moved by shifting the gap to the left in the main list,
- // gradually filling it as we go:
- insert_pos_prev = insert_pos; // "prev" is the now the position of the beginning of the gap, but the gap is about to be shifted left by moving memory right.
- insert_pos = var + left; // This is where it *would* be inserted if we weren't doing the accelerated merge.
- memmove(insert_pos + i + 1, insert_pos, (insert_pos_prev - insert_pos) * sizeof(Var *));
- var[left + i] = lazy_var[i]; // Now insert this item at the far right side of the gap just created.
- }
- var_count += lazy_var_count;
- lazy_var_count = 0; // Indicate that the lazy var list is now empty.
- return the_new_var;
- }
- void *Script::GetVarType(char *aVarName)
- {
- // Convert to lowercase to help performance a little (it typically only helps loadtime performance because
- // this function is rarely called during script-runtime).
- char lowercase[MAX_VAR_NAME_LENGTH + 1];
- strlcpy(lowercase, aVarName, sizeof(lowercase)); // Caller should have ensured it fits, but call strlcpy() for maintainability.
- CharLower(lowercase);
- // Above: CharLower() is smaller in code size than strlwr(), but CharLower uses the OS locale and strlwr uses
- // the setlocal() locale (which is always the same if setlocal() is never called). However, locale
- // differences shouldn't affect the cases checked below; some evidence of this is at MSDN:
- // "CharLower always maps uppercase I to lowercase I, even when the current language is Turkish or Azeri."
- if (lowercase[0] != 'a' || lowercase[1] != '_') // This check helps average-case performance.
- {
- if ( !strcmp(lowercase, "true")
- || !strcmp(lowercase, "false")) return BIV_True_False;
- if (!strcmp(lowercase, "clipboard")) return (void *)VAR_CLIPBOARD;
- if (!strcmp(lowercase, "clipboardall")) return (void *)VAR_CLIPBOARDALL;
- if (!strcmp(lowercase, "comspec")) return BIV_ComSpec; // Lacks an "A_" prefix for backward compatibility with pre-NoEnv scripts and also it's easier to type & remember.
- if (!strcmp(lowercase, "programfiles")) return BIV_ProgramFiles; // v1.0.43.08: Added to ease the transition to #NoEnv.
- // Otherwise:
- return (void *)VAR_NORMAL;
- }
- // Otherwise, lowercase begins with "a_", so it's probably one of the built-in variables.
- char *lower = lowercase + 2;
- // Keeping the most common ones near the top helps performance a little.
- if (!strcmp(lower, "index")) return BIV_LoopIndex; // A short name since it's typed so often.
- if ( !strcmp(lower, "mmmm") // Long name of month.
- || !strcmp(lower, "mmm") // 3-char abbrev. month name.
- || !strcmp(lower, "dddd") // Name of weekday, e.g. Sunday
- || !strcmp(lower, "ddd") ) // Abbrev., e.g. Sun
- return BIV_MMM_DDD;
- if ( !strcmp(lower, "yyyy")
- || !strcmp(lower, "year") // Same as above.
- || !strcmp(lower, "mm") // 01 thru 12
- || !strcmp(lower, "mon") // Same
- || !strcmp(lower, "dd") // 01 thru 31
- || !strcmp(lower, "mday") // Same
- || !strcmp(lower, "wday")
- || !strcmp(lower, "yday")
- || !strcmp(lower, "yweek")
- || !strcmp(lower, "hour")
- || !strcmp(lower, "min")
- || !strcmp(lower, "sec")
- || !strcmp(lower, "msec") )
- return BIV_DateTime;
- if (!strcmp(lower, "tickcount")) return BIV_TickCount;
- if ( !strcmp(lower, "now")
- || !strcmp(lower, "nowutc")) return BIV_Now;
- if (!strcmp(lower, "workingdir")) return BIV_WorkingDir;
- if (!strcmp(lower, "scriptname")) return BIV_ScriptName;
- if (!strcmp(lower, "scriptdir")) return BIV_ScriptDir;
- if (!strcmp(lower, "scriptfullpath")) return BIV_ScriptFullPath;
- if (!strcmp(lower, "linenumber")) return BIV_LineNumber;
- if (!strcmp(lower, "linefile")) return BIV_LineFile;
- // A_IsCompiled is left blank/undefined in uncompiled scripts.
- #ifdef AUTOHOTKEYSC
- if (!strcmp(lower, "iscompiled")) return BIV_IsCompiled;
- #endif
- if ( !strcmp(lower, "batchlines")
- || !strcmp(lower, "numbatchlines")) return BIV_BatchLines;
- if (!strcmp(lower, "titlematchmode")) return BIV_TitleMatchMode;
- if (!strcmp(lower, "titlematchmodespeed")) return BIV_TitleMatchModeSpeed;
- if (!strcmp(lower, "detecthiddenwindows")) return BIV_DetectHiddenWindows;
- if (!strcmp(lower, "detecthiddentext")) return BIV_DetectHiddenText;
- if (!strcmp(lower, "autotrim")) return BIV_AutoTrim;
- if (!strcmp(lower, "stringcasesense")) return BIV_StringCaseSense;
- if (!strcmp(lower, "formatinteger")) return BIV_FormatInteger;
- if (!strcmp(lower, "formatfloat")) return BIV_FormatFloat;
- if (!strcmp(lower, "keydelay")) return BIV_KeyDelay;
- if (!strcmp(lower, "windelay")) return BIV_WinDelay;
- if (!strcmp(lower, "controldelay")) return BIV_ControlDelay;
- if (!strcmp(lower, "mousedelay")) return BIV_MouseDelay;
- if (!strcmp(lower, "defaultmousespeed")) return BIV_DefaultMouseSpeed;
- if (!strcmp(lower, "ispaused")) return BIV_IsPaused;
- if (!strcmp(lower, "iscritical")) return BIV_IsCritical;
- if (!strcmp(lower, "issuspended")) return BIV_IsSuspended;
- if (!strcmp(lower, "iconhidden")) return BIV_IconHidden;
- if (!strcmp(lower, "icontip")) return BIV_IconTip;
- if (!strcmp(lower, "iconfile")) return BIV_IconFile;
- if (!strcmp(lower, "iconnumber")) return BIV_IconNumber;
- if (!strcmp(lower, "exitreason")) return BIV_ExitReason;
- if (!strcmp(lower, "ostype")) return BIV_OSType;
- if (!strcmp(lower, "osversion")) return BIV_OSVersion;
- if (!strcmp(lower, "language")) return BIV_Language;
- if ( !strcmp(lower, "computername")
- || !strcmp(lower, "username")) return BIV_UserName_ComputerName;
- if (!strcmp(lower, "windir")) return BIV_WinDir;
- if (!strcmp(lower, "temp")) return BIV_Temp; // Debatably should be A_TempDir, but brevity seemed more popular with users, perhaps for heavy uses of the temp folder.
- if (!strcmp(lower, "programfiles")) return BIV_ProgramFiles;
- if (!strcmp(lower, "mydocuments")) return BIV_MyDocuments;
- if ( !strcmp(lower, "appdata")
- || !strcmp(lower, "appdatacommon")) return BIV_AppData;
- if ( !strcmp(lower, "desktop")
- || !strcmp(lower, "desktopcommon")) return BIV_Desktop;
- if ( !strcmp(lower, "startmenu")
- || !strcmp(lower, "startmenucommon")) return BIV_StartMenu;
- if ( !strcmp(lower, "programs")
- || !strcmp(lower, "programscommon")) return BIV_Programs;
- if ( !strcmp(lower, "startup")
- || !strcmp(lower, "startupcommon")) return BIV_Startup;
- if (!strcmp(lower, "isadmin")) return BIV_IsAdmin;
- if (!strcmp(lower, "cursor")) return BIV_Cursor;
- if ( !strcmp(lower, "caretx")
- || !strcmp(lower, "carety")) return BIV_Caret;
- if ( !strcmp(lower, "screenwidth")
- || !strcmp(lower, "screenheight")) return BIV_ScreenWidth_Height;
- if (!strncmp(lower, "ipaddress", 9))
- {
- lower += 9;
- return (*lower >= '1' && *lower <= '4'
- && !lower[1]) // Make sure has only one more character rather than none or several (e.g. A_IPAddress1abc should not be match).
- ? BIV_IPAddress
- : (void *)VAR_NORMAL; // Otherwise it can't be a match for any built-in variable.
- }
- if (!strncmp(lower, "loop", 4))
- {
- lower += 4;
- if (!strcmp(lower, "readline")) return BIV_LoopReadLine;
- if (!strcmp(lower, "field")) return BIV_LoopField;
- if (!strncmp(lower, "file", 4))
- {
- lower += 4;
- if (!strcmp(lower, "name")) return BIV_LoopFileName;
- if (!strcmp(lower, "shortname")) return BIV_LoopFileShortName;
- if (!strcmp(lower, "ext")) return BIV_LoopFileExt;
- if (!strcmp(lower, "dir")) return BIV_LoopFileDir;
- if (!strcmp(lower, "fullpath")) return BIV_LoopFileFullPath;
- if (!strcmp(lower, "longpath")) return BIV_LoopFileLongPath;
- if (!strcmp(lower, "shortpath")) return BIV_LoopFileShortPath;
- if (!strcmp(lower, "attrib")) return BIV_LoopFileAttrib;
- if ( !strcmp(lower, "timemodified")
- || !strcmp(lower, "timecreated")
- || !strcmp(lower, "timeaccessed")) return BIV_LoopFileTime;
- if ( !strcmp(lower, "size")
- || !strcmp(lower, "sizekb")
- || !strcmp(lower, "sizemb")) return BIV_LoopFileSize;
- // Otherwise, it can't be a match for any built-in variable:
- return (void *)VAR_NORMAL;
- }
- if (!strncmp(lower, "reg", 3))
- {
- lower += 3;
- if (!strcmp(lower, "type")) return BIV_LoopRegType;
- if (!strcmp(lower, "key")) return BIV_LoopRegKey;
- if (!strcmp(lower, "subkey")) return BIV_LoopRegSubKey;
- if (!strcmp(lower, "name")) return BIV_LoopRegName;
- if (!strcmp(lower, "timemodified")) return BIV_LoopRegTimeModified;
- // Otherwise, it can't be a match for any built-in variable:
- return (void *)VAR_NORMAL;
- }
- }
- if (!strcmp(lower, "thisfunc")) return BIV_ThisFunc;
- if (!strcmp(lower, "thislabel")) return BIV_ThisLabel;
- if (!strcmp(lower, "thismenuitem")) return BIV_ThisMenuItem;
- if (!strcmp(lower, "thismenuitempos")) return BIV_ThisMenuItemPos;
- if (!strcmp(lower, "thismenu")) return BIV_ThisMenu;
- if (!strcmp(lower, "thishotkey")) return BIV_ThisHotkey;
- if (!strcmp(lower, "priorhotkey")) return BIV_PriorHotkey;
- if (!strcmp(lower, "timesincethishotkey")) return BIV_TimeSinceThisHotkey;
- if (!strcmp(lower, "timesincepriorhotkey")) return BIV_TimeSincePriorHotkey;
- if (!strcmp(lower, "endchar")) return BIV_EndChar;
- if (!strcmp(lower, "lasterror")) return BIV_LastError;
- if (!strcmp(lower, "eventinfo")) return BIV_EventInfo; // It's called "EventInfo" vs. "GuiEventInfo" because it applies to non-Gui events such as OnClipboardChange.
- if (!strcmp(lower, "guicontrol")) return BIV_GuiControl;
- if ( !strcmp(lower, "guicontrolevent") // v1.0.36: A_GuiEvent was added as a synonym for A_GuiControlEvent because it seems unlikely that A_GuiEvent will ever be needed for anything:
- || !strcmp(lower, "guievent")) return BIV_GuiEvent;
- if ( !strcmp(lower, "gui")
- || !strcmp(lower, "guiwidth")
- || !strcmp(lower, "guiheight")
- || !strcmp(lower, "guix") // Naming: Brevity seems more a benefit than would A_GuiEventX's improved clarity.
- || !strcmp(lower, "guiy")) return BIV_Gui; // These can be overloaded if a GuiMove label or similar is ever needed.
- if (!strcmp(lower, "timeidle")) return BIV_TimeIdle;
- if (!strcmp(lower, "timeidlephysical")) return BIV_TimeIdlePhysical;
- if ( !strcmp(lower, "space")
- || !strcmp(lower, "tab")) return BIV_Space_Tab;
- if (!strcmp(lower, "ahkversion")) return BIV_AhkVersion;
- if (!strcmp(lower, "ahkpath")) return BIV_AhkPath;
- // Since above didn't return:
- return (void *)VAR_NORMAL;
- }
- WinGroup *Script::FindGroup(char *aGroupName, bool aCreateIfNotFound)
- // Caller must ensure that aGroupName isn't NULL. But if it's the empty string, NULL is returned.
- // Returns the Group whose name matches aGroupName. If it doesn't exist, it is created if aCreateIfNotFound==true.
- // Thread-safety: This function is thread-safe (except when when called with aCreateIfNotFound==true) even when
- // the main thread happens to be calling AddGroup() and changing the linked list while it's being traversed here
- // by the hook thread. However, any subsequent changes to this function or AddGroup() must be carefully reviewed.
- {
- if (!*aGroupName)
- return NULL;
- for (WinGroup *group = mFirstGroup; group != NULL; group = group->mNextGroup)
- if (!stricmp(group->mName, aGroupName)) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales; 3) performance.
- return group; // Match found.
- // Otherwise, no match found, so create a new group.
- if (!aCreateIfNotFound || AddGroup(aGroupName) != OK)
- return NULL;
- return mLastGroup;
- }
- ResultType Script::AddGroup(char *aGroupName)
- // Returns OK or FAIL.
- // The caller must already have verfied that this isn't a duplicate group.
- // This function is not thread-safe because it adds an entry to the quasi-global list of window groups.
- // In addition, if this function is being called by one thread while another thread is calling FindGroup(),
- // the thread-safety notes in FindGroup() apply.
- {
- size_t aGroupName_length = strlen(aGroupName);
- if (aGroupName_length > MAX_VAR_NAME_LENGTH)
- return ScriptError("Group name too long.", aGroupName);
- if (!Var::ValidateName(aGroupName, false, DISPLAY_NO_ERROR)) // Seems best to use same validation as var names.
- return ScriptError("Illegal group name.", aGroupName);
- char *new_name = SimpleHeap::Malloc(aGroupName, aGroupName_length);
- if (!new_name)
- return FAIL; // It already displayed the error for us.
- // The precise method by which the follows steps are done should be thread-safe even if
- // some other thread calls FindGroup() in the middle of the operation. But any changes
- // must be carefully reviewed:
- WinGroup *the_new_group = new WinGroup(new_name);
- if (the_new_group == NULL)
- return ScriptError(ERR_OUTOFMEM);
- if (mFirstGroup == NULL)
- mFirstGroup = the_new_group;
- else
- mLastGroup->mNextGroup = the_new_group;
- // This must be done after the above:
- mLastGroup = the_new_group;
- return OK;
- }
- Line *Script::PreparseBlocks(Line *aStartingLine, bool aFindBlockEnd, Line *aParentLine)
- // aFindBlockEnd should be true, only when this function is called
- // by itself. The end of this function relies upon this definition.
- // Will return NULL to the top-level caller if there's an error, or if
- // mLastLine is NULL (i.e. the script is empty).
- {
- // Not thread-safe, so this can only parse one script at a time.
- // Not a problem for the foreseeable future:
- static int nest_level; // Level zero is the outermost one: outside all blocks.
- static bool abort;
- if (!aParentLine)
- {
- // We were called from outside, not recursively, so init these. This is
- // very important if this function is ever to be called from outside
- // more than once, even though it isn't currently:
- nest_level = 0;
- abort = false;
- }
- int i, open_parens;
- bool in_quotes;
- DerefType *deref, *deref2;
- char *param_start, *param_end, *param_last_char, *cp, c;
- bool found;
- // Don't check aStartingLine here at top: only do it at the bottom
- // for its differing return values.
- for (Line *line = aStartingLine; line;)
- {
- // Check if any of each arg's derefs are function calls. If so, do some validation and
- // preprocessing to set things up for better runtime performance:
- for (i = 0; i < line->mArgc; ++i) // For each arg.
- {
- ArgStruct &this_arg = line->mArg[i]; // For performance and convenience.
- // Exclude the derefs of output and input vars from consideration, since they can't
- // be function calls:
- if (!this_arg.is_expression) // For now, only expressions are capable of calling functions. If ever change this, might want to add a check here for this_arg.type != ARG_TYPE_NORMAL (for performance).
- continue;
- if (this_arg.deref) // No function-calls are present because the expression contains neither variables nor function calls.
- {
- for (deref = this_arg.deref; deref->marker; ++deref) // For each deref.
- {
- if (!deref->is_function)
- continue;
- if ( !(deref->func = FindFunc(deref->marker, deref->length)) )
- {
- #ifndef AUTOHOTKEYSC
- bool error_was_shown;
- if ( !(deref->func = FindFuncInLibrary(deref->marker, deref->length, error_was_shown)) )
- {
- abort = true; // So that the caller doesn't also report an error.
- // When above already displayed the proximate cause of the error, it's usually
- // undesirable to show the cascade effects of that error in a second dialog:
- return error_was_shown ? NULL : line->PreparseError(ERR_NONEXISTENT_FUNCTION, deref->marker);
- }
- #else
- abort = true;
- return line->PreparseError(ERR_NONEXISTENT_FUNCTION, deref->marker);
- #endif
- }
- // An earlier stage has ensured that if the function exists, it's mJumpToLine is non-NULL.
- Func &func = *deref->func; // For performance and convenience.
- // Ealier stage has ensured that strchr() will always find an open-parenthesis:
- for (deref->param_count = 0, param_start = omit_leading_whitespace(strchr(deref->marker, '(') + 1);;)
- {
- // For each parameter of this function-call.
- if (*param_start == ')') // No more params.
- break;
- if (*param_start == ',')
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_BLANK_PARAM, deref->marker);
- }
- // Although problems such as blank/empty parameters and missing close-paren were already
- // checked by DefineFunc(), that was done only for the function's formal definition, not
- // the calls to it. And although parentheses were balanced in all expressions at an earlier
- // stage, it's done again here in case function calls are ever allowed to be occur in
- // a non-expression (or dynamic functions calls such as FnArray%i%() are ever supported):
- if (!*param_start)
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_MISSING_CLOSE_PAREN, deref->marker);
- }
- // Find the end of this function-param by taking into account nested parentheses, omitting
- // from consideration any parentheses inside of quoted/literal strings. When this loop is done,
- // param_end this param's final comma or this function-call's close-paren when this param
- // is the last one.
- for (in_quotes = false, open_parens = 0, param_end = param_start;; ++param_end)
- {
- // If nested function calls are encountered within the function call being examined
- // now, they are skipped over because they will be processed here only when the outer
- // loop gets to them.
- c = *param_end; // switch() is not used so that "break" can be used to exit the loop.
- if (c == ',')
- {
- if (!(in_quotes || open_parens)) // This comma belongs to our function, so it marks the end of this param.
- break;
- //else it's not a real comma since it's inside the parentheses of a subexpression or
- // sub-function, or inside a quoted/literal string. Ignore it.
- }
- else if (c == ')')
- {
- if (!in_quotes)
- {
- if (!open_parens) // This is our function's close-paren, and thus the end of this param.
- break;
- else
- --open_parens;
- }
- //else it's not a real paren since it's inside a quoted/literal string. Ignore it.
- }
- else if (c == '(')
- {
- if (!in_quotes) // Literal parentheses inside a quoted string should not be counted for this purpose.
- ++open_parens;
- }
- else if (c == '"')
- // The simple method below is sufficient for our purpose even if a quoted string contains
- // pairs of double-quotes to represent a single literal quote, e.g. "quoted ""word""".
- // In other words, it relies on the fact that there must be an even number of quotes
- // inside any mandatory-numeric arg that is an expression such as x=="red,blue"
- in_quotes = !in_quotes;
- else if (!c) // This function lacks a closing paren.
- {
- // Might happen if this is a syntax error not catchable by the earlier stage of syntax
- // checking (paren balancing, quote balancing, etc.)
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_MISSING_CLOSE_PAREN, deref->marker);
- }
- //else it's some other, non-special character, so ignore it.
- } // for() that finds the end of this param of this function.
- // Above would have returned unless *param_end is either a comma or close-paren (namely the
- // one that terminates this parameter of this function).
- if (deref->param_count >= func.mParamCount) // Check this every iteration to avoid going beyond MAX_FUNCTION_PARAMS.
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError("Too many parameters passed to function.", deref->marker);
- }
- // Below relies on the above check having been done first to avoid reading beyond the
- // end of the mParam array.
- // If this parameter is formally declared as ByRef, report a load-time error if
- // the actual-parameter is obviously not a variable (can't catch everything, such
- // as invalid double derefs, e.g. Array%VarContainingSpaces%):
- if (!func.mIsBuiltIn && func.mParam[deref->param_count].is_byref)
- {
- // First check if there are any EXPR_TELLTALES characters in this param, since the
- // presence of an expression for this parameter means it can't resolve to a variable
- // as required by ByRef:
- for (cp = param_start, param_last_char = omit_trailing_whitespace(param_start, param_end - 1)
- ; cp <= param_last_char; ++cp)
- {
- if (*cp == ':' && cp[1] == '=')
- // v1.0.46.05: This section fixes the inability to pass ByRef certain non-trivial
- // assignments like X := " ". Although this doesn't give 100% detection, something
- // more elaborate seems unjustified (in both code size and performance) given that
- // this is only a syntax check.
- break;
- if (strchr(EXPR_FORBIDDEN_BYREF, *cp)) // This character isn't allowed in something passed ByRef unless it's an assignment (which is checked below).
- {
- if (Line::StartsWithAssignmentOp(cp) || strstr(cp, " ? ")) // v1.0.46.09: Also allow a ternary unconditionally, because it can be an arbitrarily complex expression followed by two branches that yield variables.
- {
- // Skip over :=, +=, -=, *=, /=, ++, -- ... because they can be passed ByRef.
- // In fact, don't even continue the loop because any assignment can be followed
- // by an arbitrarily complex sub-expression that shouldn't disqualify ByRef.
- break;
- }
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_BYREF, param_start); // param_start seems more informative than func.mParam[deref->param_count].var->mName
- }
- }
- // Below relies on the above having been done because the above should prevent
- // any is_function derefs from being possible since their parentheses would have been caught
- // as an error:
- // For each deref after the function name itself, ensure that there is at least
- // one deref in between this param's param_start and param_end. This finds many
- // common syntax errors such as passing a literal number or string to a ByRef
- // parameter. Note that there can be more than one for something like Array%i%_%j%
- // or a ternary like true ? x : y.
- for (found = false, deref2 = deref + 1; deref2->marker; ++deref2)
- if (deref2->marker >= param_start && deref2->marker < param_end)
- {
- found = true;
- break;
- }
- if (!found)
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_BYREF, param_start); // param_start seems more informative than func.mParam[deref->param_count].var->mName
- }
- }
- ++deref->param_count;
- // Set up for the next iteration:
- param_start = param_end; // Must already be a comma or close-paren due to checking higher above.
- if (*param_start == ',')
- {
- param_start = omit_leading_whitespace(param_start + 1);
- if (*param_start == ')')
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_BLANK_PARAM, param_start); // Report param_start vs. aBuf to give an idea of where the blank parameter is in a possibly long list of params.
- }
- }
- //else it might be ')', in which case the next iteration will handle it.
- // Above has ensured that param_start now points to the next parameter, or ')' if none.
- } // for each parameter of this function call.
- if (deref->param_count < func.mMinParams)
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError("Too few parameters passed to function.", deref->marker);
- }
- } // for each deref of this arg
- } // if (this_arg.deref)
- if (!line->ExpressionToPostfix(this_arg)) // At this stage, this_arg.is_expression is known to be true. Doing this here, after the script has been loaded, might improve the compactness/adjacent-ness of the compiled expressions in memory, which might improve performance due to CPU caching.
- {
- abort = true; // So that the caller doesn't also report an error.
- return NULL; // The function above already displayed the error msg.
- }
- } // for each arg of this line
- // All lines in our recursion layer are assigned to the block that the caller specified:
- if (line->mParentLine == NULL) // i.e. don't do it if it's already "owned" by an IF or ELSE.
- line->mParentLine = aParentLine; // Can be NULL.
- if (ACT_IS_IF_OR_ELSE_OR_LOOP(line->mActionType) || line->mActionType == ACT_REPEAT)
- {
- // In this case, the loader should have already ensured that line->mNextLine is not NULL.
- if (line->mNextLine->mActionType == ACT_BLOCK_BEGIN && line->mNextLine->mAttribute == ATTR_TRUE)
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError("Improper line below this."); // Short message since so rare. A function must not be defined directly below an IF/ELSE/LOOP because runtime evaluation won't handle it properly.
- }
- // Make the line immediately following each ELSE, IF or LOOP be enclosed by that stmt.
- // This is done to make it illegal for a Goto or Gosub to jump into a deeper layer,
- // such as in this example:
- // #y::
- // ifwinexist, pad
- // {
- // goto, label1
- // ifwinexist, pad
- // label1:
- // ; With or without the enclosing block, the goto would still go to an illegal place
- // ; in the below, resulting in an "unexpected else" error:
- // {
- // msgbox, ifaction
- // } ; not necessary to make this line enclosed by the if because labels can't point to it?
- // else
- // msgbox, elseaction
- // }
- // return
- line->mNextLine->mParentLine = line;
- // Go onto the IF's or ELSE's action in case it too is an IF, rather than skipping over it:
- line = line->mNextLine;
- continue;
- }
- switch (line->mActionType)
- {
- case ACT_BLOCK_BEGIN:
- // Some insane limit too large to ever likely be exceeded, yet small enough not
- // to be a risk of stack overflow when recursing in ExecUntil(). Mostly, this is
- // here to reduce the chance of a program crash if a binary file, a corrupted file,
- // or something unexpected has been loaded as a script when it shouldn't have been.
- // Update: Increased the limit from 100 to 1000 so that large "else if" ladders
- // can be constructed. Going much larger than 1000 seems unwise since ExecUntil()
- // will have to recurse for each nest-level, possibly resulting in stack overflow
- // if things get too deep:
- if (nest_level > 1000)
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError("Nesting too deep."); // Short msg since so rare.
- }
- // Since the current convention is to store the line *after* the
- // BLOCK_END as the BLOCK_BEGIN's related line, that line can
- // be legitimately NULL if this block's BLOCK_END is the last
- // line in the script. So it's up to the called function
- // to report an error if it never finds a BLOCK_END for us.
- // UPDATE: The design requires that we do it here instead:
- ++nest_level;
- if (NULL == (line->mRelatedLine = PreparseBlocks(line->mNextLine, 1, line)))
- if (abort) // the above call already reported the error.
- return NULL;
- else
- {
- abort = true; // So that the caller doesn't also report an error.
- return line->PreparseError(ERR_MISSING_CLOSE_BRACE);
- }
- --nest_level;
- // The convention is to have the BLOCK_BEGIN's related_line
- // point to the line *after* the BLOCK_END.
- line->mRelatedLine = line->mRelatedLine->mNextLine; // Might be NULL now.
- // Otherwise, since any blocks contained inside this one would already
- // have been handled by the recursion in the above call, continue searching
- // from the end of this block:
- line = line->mRelatedLine; // If NULL, the loop-condition will catch it.
- break;
- case ACT_BLOCK_END:
- // Return NULL (failure) if the end was found but we weren't looking for one
- // (i.e. it's an orphan). Otherwise return the line after the block_end line,
- // which will become the caller's mRelatedLine. UPDATE: Return the
- // END_BLOCK line itself so that the caller can differentiate between
- // a NULL due to end-of-script and a NULL caused by an error:
- return aFindBlockEnd ? line // Doesn't seem necessary to set abort to true.
- : line->PreparseError(ERR_MISSING_OPEN_BRACE);
- default: // Continue line-by-line.
- line = line->mNextLine;
- } // switch()
- } // for each line
- // End of script has been reached. <line> is now NULL so don't attempt to dereference it.
- // If we were still looking for an EndBlock to match up with a begin, that's an error.
- // Don't report the error here because we don't know which begin-block is waiting
- // for an end (the caller knows and must report the error). UPDATE: Must report
- // the error here (see comments further above for explanation). UPDATE #2: Changed
- // it again: Now we let the caller handle it again:
- if (aFindBlockEnd)
- //return mLastLine->PreparseError("The script ends while a block is still open (missing }).");
- return NULL;
- // If no error, return something non-NULL to indicate success to the top-level caller.
- // We know we're returning to the top-level caller because aFindBlockEnd is only true
- // when we're recursed, and in that case the above would have returned. Thus,
- // we're not recursed upon reaching this line:
- return mLastLine;
- }
- Line *Script::PreparseIfElse(Line *aStartingLine, ExecUntilMode aMode, AttributeType aLoopTypeFile
- , AttributeType aLoopTypeReg, AttributeType aLoopTypeRead, AttributeType aLoopTypeParse)
- // Zero is the default for aMode, otherwise:
- // Will return NULL to the top-level caller if there's an error, or if
- // mLastLine is NULL (i.e. the script is empty).
- // Note: This function should be called with aMode == ONLY_ONE_LINE
- // only when aStartingLine's ActionType is something recursable such
- // as IF and BEGIN_BLOCK. Otherwise, it won't return after only one line.
- {
- static BOOL sInFunctionBody = FALSE; // Improves loadtime performance by allowing IsOutsideAnyFunctionBody() to be called only when necessary.
- // Don't check aStartingLine here at top: only do it at the bottom
- // for it's differing return values.
- Line *line_temp;
- // Although rare, a statement can be enclosed in more than one type of special loop,
- // e.g. both a file-loop and a reg-loop:
- AttributeType loop_type_file, loop_type_reg, loop_type_read, loop_type_parse;
- for (Line *line = aStartingLine; line != NULL;)
- {
- if ( ACT_IS_IF(line->mActionType)
- || line->mActionType == ACT_LOOP
- || line->mActionType == ACT_WHILE // Lexikos: Added check for ACT_WHILE.
- || line->mActionType == ACT_REPEAT )
- {
- // ActionType is an IF or a LOOP.
- line_temp = line->mNextLine; // line_temp is now this IF's or LOOP's action-line.
- // Update: Below is commented out because it's now impossible (since all scripts end in ACT_EXIT):
- //if (line_temp == NULL) // This is an orphan IF/LOOP (has no action-line) at the end of the script.
- // return line->PreparseError("Q"); // Placeholder. Formerly "This if-statement or loop has no action."
- // Other things rely on this check having been done, such as "if (line->mRelatedLine != NULL)":
- if (line_temp->mActionType == ACT_ELSE || line_temp->mActionType == ACT_BLOCK_END)
- return line->PreparseError("Inappropriate line beneath IF or LOOP.");
- // We're checking for ATTR_LOOP_FILEPATTERN here to detect whether qualified commands enclosed
- // in a true file loop are allowed to omit their filename parameter:
- loop_type_file = ATTR_NONE;
- if (aLoopTypeFile == ATTR_LOOP_FILEPATTERN || line->mAttribute == ATTR_LOOP_FILEPATTERN)
- // i.e. if either one is a file-loop, that's enough to establish
- // the fact that we're in a file loop.
- loop_type_file = ATTR_LOOP_FILEPATTERN;
- else if (aLoopTypeFile == ATTR_LOOP_UNKNOWN || line->mAttribute == ATTR_LOOP_UNKNOWN)
- // ATTR_LOOP_UNKNOWN takes precedence over ATTR_LOOP_NORMAL because
- // we can't be sure if we're in a file loop, but it's correct to
- // assume that we are (otherwise, unwarranted syntax errors may be reported
- // later on in here).
- loop_type_file = ATTR_LOOP_UNKNOWN;
- else if (aLoopTypeFile == ATTR_LOOP_NORMAL || line->mAttribute == ATTR_LOOP_NORMAL)
- loop_type_file = ATTR_LOOP_NORMAL;
- else if (aLoopTypeFile == ATTR_LOOP_WHILE || line->mAttribute == ATTR_LOOP_WHILE) // Lexikos: ACT_WHILE
- loop_type_file = ATTR_LOOP_WHILE;
- // The section is the same as above except for registry vs. file loops:
- loop_type_reg = ATTR_NONE;
- if (aLoopTypeReg == ATTR_LOOP_REG || line->mAttribute == ATTR_LOOP_REG)
- loop_type_reg = ATTR_LOOP_REG;
- else if (aLoopTypeReg == ATTR_LOOP_UNKNOWN || line->mAttribute == ATTR_LOOP_UNKNOWN)
- loop_type_reg = ATTR_LOOP_UNKNOWN;
- else if (aLoopTypeReg == ATTR_LOOP_NORMAL || line->mAttribute == ATTR_LOOP_NORMAL)
- loop_type_reg = ATTR_LOOP_NORMAL;
- else if (aLoopTypeReg == ATTR_LOOP_WHILE || line->mAttribute == ATTR_LOOP_WHILE) // Lexikos: ACT_WHILE
- loop_type_reg = ATTR_LOOP_WHILE;
- // Same as above except for READ-FILE loops:
- loop_type_read = ATTR_NONE;
- if (aLoopTypeRead == ATTR_LOOP_READ_FILE || line->mAttribute == ATTR_LOOP_READ_FILE)
- loop_type_read = ATTR_LOOP_READ_FILE;
- else if (aLoopTypeRead == ATTR_LOOP_UNKNOWN || line->mAttribute == ATTR_LOOP_UNKNOWN)
- loop_type_read = ATTR_LOOP_UNKNOWN;
- else if (aLoopTypeRead == ATTR_LOOP_NORMAL || line->mAttribute == ATTR_LOOP_NORMAL)
- loop_type_read = ATTR_LOOP_NORMAL;
- else if (aLoopTypeRead == ATTR_LOOP_WHILE || line->mAttribute == ATTR_LOOP_WHILE) // Lexikos: ACT_WHILE
- loop_type_read = ATTR_LOOP_WHILE;
- // Same as above except for PARSING loops:
- loop_type_parse = ATTR_NONE;
- if (aLoopTypeParse == ATTR_LOOP_PARSE || line->mAttribute == ATTR_LOOP_PARSE)
- loop_type_parse = ATTR_LOOP_PARSE;
- else if (aLoopTypeParse == ATTR_LOOP_UNKNOWN || line->mAttribute == ATTR_LOOP_UNKNOWN)
- loop_type_parse = ATTR_LOOP_UNKNOWN;
- else if (aLoopTypeParse == ATTR_LOOP_NORMAL || line->mAttribute == ATTR_LOOP_NORMAL)
- loop_type_parse = ATTR_LOOP_NORMAL;
- else if (aLoopTypeParse == ATTR_LOOP_WHILE || line->mAttribute == ATTR_LOOP_WHILE) // Lexikos: ACT_WHILE
- loop_type_parse = ATTR_LOOP_WHILE;
- // Check if the IF's action-line is something we want to recurse. UPDATE: Always
- // recurse because other line types, such as Goto and Gosub, need to be preparsed
- // by this function even if they are the single-line actions of an IF or an ELSE:
- // Recurse this line rather than the next because we want
- // the called function to recurse again if this line is a ACT_BLOCK_BEGIN
- // or is itself an IF:
- line_temp = PreparseIfElse(line_temp, ONLY_ONE_LINE, loop_type_file, loop_type_reg, loop_type_read
- , loop_type_parse);
- // If not end-of-script or error, line_temp is now either:
- // 1) If this if's/loop's action was a BEGIN_BLOCK: The line after the end of the block.
- // 2) If this if's/loop's action was another IF or LOOP:
- // a) the line after that if's else's action; or (if it doesn't have one):
- // b) the line after that if's/loop's action
- // 3) If this if's/loop's action was some single-line action: the line after that action.
- // In all of the above cases, line_temp is now the line where we
- // would expect to find an ELSE for this IF, if it has one.
- // Now the above has ensured that line_temp is this line's else, if it has one.
- // Note: line_temp will be NULL if the end of the script has been reached.
- // UPDATE: That can't happen now because all scripts end in ACT_EXIT:
- if (line_temp == NULL) // Error or end-of-script was reached.
- return NULL;
- // Seems best to keep this check for mainability because changes to other checks can impact
- // whether this check will ever be "true":
- if (line->mRelatedLine != NULL)
- return line->PreparseError("Q"); // Placeholder since it shouldn't happen. Formerly "This if-statement or LOOP unexpectedly already had an ELSE or end-point."
- // Set it to the else's action, rather than the else itself, since the else itself
- // is never needed during execution. UPDATE: No, instead set it to the ELSE itself
- // (if it has one) since we jump here at runtime when the IF is finished (whether
- // it's condition was true or false), thus skipping over any nested IF's that
- // aren't in blocks beneath it. If there's no ELSE, the below value serves as
- // the jumppoint we go to when the if-statement is finished. Example:
- // if x
- // if y
- // if z
- // action1
- // else
- // action2
- // action3
- // x's jumppoint should be action3 so that all the nested if's
- // under the first one can be skipped after the "if x" line is recursively
- // evaluated. Because of this behavior, all IFs will have a related line
- // with the possibly exception of the very last if-statement in the script
- // (which is possible only if the script doesn't end in a Return or Exit).
- line->mRelatedLine = line_temp; // Even if <line> is a LOOP and line_temp and else?
- // Even if aMode == ONLY_ONE_LINE, an IF and its ELSE count as a single
- // statement (one line) due to its very nature (at least for this purpose),
- // so always continue on to evaluate the IF's ELSE, if present:
- if (line_temp->mActionType == ACT_ELSE)
- {
- if (line->mActionType == ACT_LOOP || line->mActionType == ACT_WHILE || line->mActionType == ACT_REPEAT) // Lexikos: Added check for ACT_WHILE.
- {
- // this can't be our else, so let the caller handle it.
- if (aMode != ONLY_ONE_LINE)
- // This ELSE was encountered while sequentially scanning the contents
- // of a block or at the otuermost nesting layer. More thought is required
- // to verify this is correct. UPDATE: This check is very old and I haven't
- // found a case that can produce it yet, but until proven otherwise its safer
- // to assume it's possible.
- return line_temp->PreparseError(ERR_ELSE_WITH_NO_IF);
- // Let the caller handle this else, since it can't be ours:
- return line_temp;
- }
- // Now use line vs. line_temp to hold the new values, so that line_temp
- // stays as a marker to the ELSE line itself:
- line = line_temp->mNextLine; // Set it to the else's action line.
- // Update: The following is now impossible because all scripts end in ACT_EXIT.
- // Thus, it's commented out:
- //if (line == NULL) // An else with no action.
- // return line_temp->PreparseError("Q"); // Placeholder since impossible. Formerly "This ELSE has no action."
- if (line->mActionType == ACT_ELSE || line->mActionType == ACT_BLOCK_END)
- return line_temp->PreparseError("Inappropriate line beneath ELSE.");
- // Assign to line rather than line_temp:
- line = PreparseIfElse(line, ONLY_ONE_LINE, aLoopTypeFile, aLoopTypeReg, aLoopTypeRead
- , aLoopTypeParse);
- if (line == NULL)
- return NULL; // Error or end-of-script.
- // Set this ELSE's jumppoint. This is similar to the jumppoint set for
- // an ELSEless IF, so see related comments above:
- line_temp->mRelatedLine = line;
- }
- else // line doesn't have an else, so just continue processing from line_temp's position
- line = line_temp;
- // Both cases above have ensured that line is now the first line beyond the
- // scope of the if-statement and that of any ELSE it may have.
- if (aMode == ONLY_ONE_LINE) // Return the next unprocessed line to the caller.
- return line;
- // Otherwise, continue processing at line's new location:
- continue;
- } // ActionType is "IF".
- // Since above didn't continue, do the switch:
- char *line_raw_arg1 = LINE_RAW_ARG1; // Resolve only once to help reduce code size.
- char *line_raw_arg2 = LINE_RAW_ARG2; //
- switch (line->mActionType)
- {
- case ACT_BLOCK_BEGIN:
- if (line->mAttribute == ATTR_TRUE) // This is the opening brace of a function definition.
- sInFunctionBody = TRUE; // Must be set only for the above condition because functions can of course contain types of blocks other than the function's own block.
- line = PreparseIfElse(line->mNextLine, UNTIL_BLOCK_END, aLoopTypeFile, aLoopTypeReg, aLoopTypeRead
- , aLoopTypeParse);
- // "line" is now either NULL due to an error, or the location of the END_BLOCK itself.
- if (line == NULL)
- return NULL; // Error.
- break;
- case ACT_BLOCK_END:
- if (line->mAttribute == ATTR_TRUE) // This is the closing brace of a function definition.
- sInFunctionBody = FALSE; // Must be set only for the above condition because functions can of course contain types of blocks other than the function's own block.
- if (aMode == ONLY_ONE_LINE)
- // Syntax error. The caller would never expect this single-line to be an
- // end-block. UPDATE: I think this is impossible because callers only use
- // aMode == ONLY_ONE_LINE when aStartingLine's ActionType is already
- // known to be an IF or a BLOCK_BEGIN:
- return line->PreparseError("Q"); // Placeholder (see above). Formerly "Unexpected end-of-block (single)."
- if (UNTIL_BLOCK_END)
- // Return line rather than line->mNextLine because, if we're at the end of
- // the script, it's up to the caller to differentiate between that condition
- // and the condition where NULL is an error indicator.
- return line;
- // Otherwise, we found an end-block we weren't looking for. This should be
- // impossible since the block pre-parsing already balanced all the blocks?
- return line->PreparseError("Q"); // Placeholder (see above). Formerly "Unexpected end-of-block (multi)."
- case ACT_BREAK:
- case ACT_CONTINUE:
- if (!aLoopTypeFile && !aLoopTypeReg && !aLoopTypeRead && !aLoopTypeParse)
- return line->PreparseError("Break/Continue must be enclosed by a Loop.");
- break;
- case ACT_GOSUB: // These two must be done here (i.e. *after* all the script lines have been added),
- case ACT_GOTO: // so that labels both above and below each Gosub/Goto can be resolved.
- if (line->ArgHasDeref(1))
- // Since the jump-point contains a deref, it must be resolved at runtime:
- line->mRelatedLine = NULL;
- else
- {
- if (!line->GetJumpTarget(false))
- return NULL; // Error was already displayed by called function.
- if ( line->mActionType == ACT_GOSUB && sInFunctionBody
- && ((Label *)(line->mRelatedLine))->mJumpToLine->IsOutsideAnyFunctionBody() ) // Relies on above call to GetJumpTarget() having set line->mRelatedLine.
- // Since this Gosub and its target line are both inside a function, they must both
- // be in the same function because otherwise GetJumpTarget() would have reported
- // the target as invalid.
- line->mAttribute = ATTR_TRUE; // v1.0.48.02: To improve runtime performance, mark this Gosub as having a target that is outside of any function body.
- //else leave above at its line-constructor default of ATTR_NONE.
- }
- break;
- // These next 4 must also be done here (i.e. *after* all the script lines have been added),
- // so that labels both above and below this line can be resolved:
- case ACT_ONEXIT:
- if (*line_raw_arg1 && !line->ArgHasDeref(1))
- if ( !(line->mAttribute = FindLabel(line_raw_arg1)) )
- return line->PreparseError(ERR_NO_LABEL);
- break;
- case ACT_HOTKEY:
- if ( *line_raw_arg2 && !line->ArgHasDeref(2)
- && !line->ArgHasDeref(1) && strnicmp(line_raw_arg1, "IfWin", 5) ) // v1.0.42: Omit IfWinXX from validation.
- if ( !(line->mAttribute = FindLabel(line_raw_arg2)) )
- if (!Hotkey::ConvertAltTab(line_raw_arg2, true))
- return line->PreparseError(ERR_NO_LABEL);
- break;
- case ACT_SETTIMER:
- if (!line->ArgHasDeref(1))
- if ( !(line->mAttribute = FindLabel(line_raw_arg1)) )
- return line->PreparseError(ERR_NO_LABEL);
- if (*line_raw_arg2 && !line->ArgHasDeref(2))
- if (!Line::ConvertOnOff(line_raw_arg2) && !IsPureNumeric(line_raw_arg2, true) // v1.0.46.16: Allow negatives to support the new run-only-once mode.
- && !line->mArg[1].is_expression) // v1.0.46.10: Don't consider expressions THAT CONTAIN NO VARIABLES OR FUNCTION-CALLS like "% 2*500" to be a syntax error.
- return line->PreparseError(ERR_PARAM2_INVALID);
- break;
- case ACT_GROUPADD: // This must be done here because it relies on all other lines already having been added.
- if (*LINE_RAW_ARG4 && !line->ArgHasDeref(4))
- {
- // If the label name was contained in a variable, that label is now resolved and cannot
- // be changed. This is in contrast to something like "Gosub, %MyLabel%" where a change in
- // the value of MyLabel will change the behavior of the Gosub at runtime:
- Label *label = FindLabel(LINE_RAW_ARG4);
- if (!label)
- return line->PreparseError(ERR_NO_LABEL);
- line->mRelatedLine = (Line *)label; // The script loader has ensured that this can't be NULL.
- // Can't do this because the current line won't be the launching point for the
- // Gosub. Instead, the launching point will be the GroupActivate rather than the
- // GroupAdd, so it will be checked by the GroupActivate or not at all (since it's
- // not that important in the case of a Gosub -- it's mostly for Goto's):
- //return IsJumpValid(label->mJumpToLine);
- }
- break;
- case ACT_ELSE:
- // Should never happen because the part that handles the if's, above, should find
- // all the elses and handle them. UPDATE: This happens if there's
- // an extra ELSE in this scope level that has no IF:
- return line->PreparseError(ERR_ELSE_WITH_NO_IF);
- } // switch()
- line = line->mNextLine; // If NULL due to physical end-of-script, the for-loop's condition will catch it.
- if (aMode == ONLY_ONE_LINE) // Return the next unprocessed line to the caller.
- // In this case, line shouldn't be (and probably can't be?) NULL because the line after
- // a single-line action shouldn't be the physical end of the script. That's because
- // the loader has ensured that all scripts now end in ACT_EXIT. And that final
- // ACT_EXIT should never be parsed here in ONLY_ONE_LINE mode because the only time
- // that mode is used is for the action of an IF, an ELSE, or possibly a LOOP.
- // In all of those cases, the final ACT_EXIT line in the script (which is explicitly
- // insertted by the loader) cannot be the line that was just processed by the
- // switch(). Therefore, the above assignment should not have set line to NULL
- // (which is good because NULL would probably be construed as "failure" by our
- // caller in this case):
- return line;
- // else just continue the for-loop at the new value of line.
- } // for()
- // End of script has been reached. line is now NULL so don't dereference it.
- // If we were still looking for an EndBlock to match up with a begin, that's an error.
- // This indicates that the at least one BLOCK_BEGIN is missing a BLOCK_END.
- // However, since the blocks were already balanced by the block pre-parsing function,
- // this should be impossible unless the design of this function is flawed.
- if (aMode == UNTIL_BLOCK_END)
- #ifdef _DEBUG
- return mLastLine->PreparseError("DEBUG: The script ended while a block was still open."); // This is a bug because the preparser already verified all blocks are balanced.
- #else
- return NULL; // Shouldn't happen, so just return failure.
- #endif
- // If we were told to process a single line, we were recursed and it should have returned above,
- // so it's an error here (can happen if we were called with aStartingLine == NULL?):
- if (aMode == ONLY_ONE_LINE)
- return mLastLine->PreparseError("Q"); // Placeholder since probably impossible. Formerly "The script ended while an action was still expected."
- // Otherwise, return something non-NULL to indicate success to the top-level caller:
- return mLastLine;
- }
- ResultType Line::ExpressionToPostfix(ArgStruct &aArg)
- // Returns OK or FAIL.
- {
- // Having a precedence array is required at least for SYM_POWER (since the order of evaluation
- // of something like 2**1**2 does matter). It also helps performance by avoiding unnecessary pushing
- // and popping of operators to the stack. This array must be kept in sync with "enum SymbolType".
- // Also, dimensioning explicitly by SYM_COUNT helps enforce that at compile-time:
- static UCHAR sPrecedence[SYM_COUNT] = // Performance: UCHAR vs. INT benches a little faster, perhaps due to the slight reduction in code size it causes.
- {
- 0,0,0,0,0,0,0 // SYM_STRING, SYM_INTEGER, SYM_FLOAT, SYM_VAR, SYM_OPERAND, SYM_DYNAMIC, SYM_BEGIN (SYM_BEGIN must be lowest precedence).
- , 82, 82 // SYM_POST_INCREMENT, SYM_POST_DECREMENT: Highest precedence operator so that it will work even though it comes *after* a variable name (unlike other unaries, which come before).
- , 4, 4 // SYM_CPAREN, SYM_OPAREN (to simplify the code, parentheses must be lower than all operators in precedence).
- , 6 // SYM_COMMA -- Must be just above SYM_OPAREN so it doesn't pop OPARENs off the stack.
- , 7,7,7,7,7,7,7,7,7,7,7,7 // SYM_ASSIGN_*. THESE HAVE AN ODD NUMBER to indicate right-to-left evaluation order, which is necessary for cascading assignments such as x:=y:=1 to work.
- // , 8 // THIS VALUE MUST BE LEFT UNUSED so that the one above can be promoted to it by the infix-to-postfix routine.
- , 11, 11 // SYM_IFF_ELSE, SYM_IFF_THEN (ternary conditional). HAS AN ODD NUMBER to indicate right-to-left evaluation order, which is necessary for ternaries to perform traditionally when nested in each other without parentheses.
- // , 12 // THIS VALUE MUST BE LEFT UNUSED so that the one above can be promoted to it by the infix-to-postfix routine.
- , 16 // SYM_OR
- , 20 // SYM_AND
- , 25 // SYM_LOWNOT (the word "NOT": the low precedence version of logical-not). HAS AN ODD NUMBER to indicate right-to-left evaluation order so that things like "not not var" are supports (which can be used to convert a variable into a pure 1/0 boolean value).
- // , 26 // THIS VALUE MUST BE LEFT UNUSED so that the one above can be promoted to it by the infix-to-postfix routine.
- , 30, 30, 30 // SYM_EQUAL, SYM_EQUALCASE, SYM_NOTEQUAL (lower prec. than the below so that "x < 5 = var" means "result of comparison is the boolean value in var".
- , 34, 34, 34, 34 // SYM_GT, SYM_LT, SYM_GTOE, SYM_LTOE
- , 38 // SYM_CONCAT
- , 42 // SYM_BITOR -- Seems more intuitive to have these three higher in prec. than the above, unlike C and Perl, but like Python.
- , 46 // SYM_BITXOR
- , 50 // SYM_BITAND
- , 54, 54 // SYM_BITSHIFTLEFT, SYM_BITSHIFTRIGHT
- , 58, 58 // SYM_ADD, SYM_SUBTRACT
- , 62, 62, 62 // SYM_MULTIPLY, SYM_DIVIDE, SYM_FLOORDIVIDE
- , 67,67,67,67,67 // SYM_NEGATIVE (unary minus), SYM_HIGHNOT (the high precedence "!" operator), SYM_BITNOT, SYM_ADDRESS, SYM_DEREF
- // NOTE: THE ABOVE MUST BE AN ODD NUMBER to indicate right-to-left evaluation order, which was added in v1.0.46 to support consecutive unary operators such as !*var !!var (!!var can be used to convert a value into a pure 1/0 boolean).
- // , 68 // THIS VALUE MUST BE LEFT UNUSED so that the one above can be promoted to it by the infix-to-postfix routine.
- , 72 // SYM_POWER (see note below). Associativity kept as left-to-right for backward compatibility (e.g. 2**2**3 is 4**3=64 not 2**8=256).
- , 77, 77 // SYM_PRE_INCREMENT, SYM_PRE_DECREMENT (higher precedence than SYM_POWER because it doesn't make sense to evaluate power first because that would cause ++/-- to fail due to operating on a non-lvalue.
- // , 78 // THIS VALUE MUST BE LEFT UNUSED so that the one above can be promoted to it by the infix-to-postfix routine.
- // , 82, 82 // RESERVED FOR SYM_POST_INCREMENT, SYM_POST_DECREMENT (which are listed higher above for the performance of YIELDS_AN_OPERAND().
- , 86 // SYM_FUNC -- Must be of highest precedence so that it stays tightly bound together as though it's a single operand for use by other operators.
- };
- // Most programming languages give exponentiation a higher precedence than unary minus and logical-not.
- // For example, -2**2 is evaluated as -(2**2), not (-2)**2 (the latter is unsupported by qmathPow anyway).
- // However, this rule requires a small workaround in the postfix-builder to allow 2**-2 to be
- // evaluated as 2**(-2) rather than being seen as an error. v1.0.45: A similar thing is required
- // to allow the following to work: 2**!1, 2**not 0, 2**~0xFFFFFFFE, 2**&x.
- // On a related note, the right-to-left tradition of something like 2**3**4 is not implemented (maybe in v2).
- // Instead, the expression is evaluated from left-to-right (like other operators) to simplify the code.
- ExprTokenType infix[MAX_TOKENS], *postfix[MAX_TOKENS], *stack[MAX_TOKENS + 1]; // +1 for SYM_BEGIN on the stack.
- int infix_count = 0, postfix_count = 0, stack_count = 0;
- // Above dimensions the stack to be as large as the infix/postfix arrays to cover worst-case
- // scenarios and avoid having to check for overflow. For the infix-to-postfix conversion, the
- // stack must be large enough to hold a malformed expression consisting entirely of operators
- // (though other checks might prevent this). It must also be large enough for use by the final
- // expression evaluation phase, the worst case of which is unknown but certainly not larger
- // than MAX_TOKENS.
- ///////////////////////////////////////////////////////////////////////////////////////////////
- // TOKENIZE THE INFIX EXPRESSION INTO AN INFIX ARRAY: Avoids the performance overhead of having
- // to re-detect whether each symbol is an operand vs. operator at multiple stages.
- ///////////////////////////////////////////////////////////////////////////////////////////////
- // In v1.0.46.01, this section was simplified to avoid transcribing the entire expression into the
- // deref buffer. In addition to improving performance and reducing code size, this also solves
- // obscure timing bugs caused by functions that have side-effects, especially in comma-separated
- // sub-expressions. In these cases, one part of an expression could change a built-in variable
- // (indirectly or in the case of Clipboard, directly), an environment variable, or a double-def.
- // For example the dynamic components of a double-deref can be changed by other parts of an
- // expression, even one without commas. Another example is: fn(clipboard, func_that_changes_clip()).
- // So now, built-in & environment variables and double-derefs are resolve when they're actually
- // encountered during the final/evaluation phase.
- // Another benefit to deferring the resolution of these types of items is that they become eligible
- // for short-circuiting, which further helps performance (they're quite similar to built-in
- // functions in this respect).
- char *op_end, *cp;
- DerefType *deref, *this_deref, *deref_start, *deref_new;
- int derefs_in_this_double;
- int cp1; // int vs. char benchmarks slightly faster, and is slightly smaller in code size.
- for (cp = aArg.text, deref = aArg.deref // Start at the begining of this arg's text and look for the next deref.
- ;; ++deref, ++infix_count) // FOR EACH DEREF IN AN ARG:
- {
- this_deref = deref && deref->marker ? deref : NULL; // A deref with a NULL marker terminates the list (i.e. the final deref isn't a deref, merely a terminator of sorts.
- // BEFORE PROCESSING "this_deref" ITSELF, MUST FIRST PROCESS ANY LITERAL/RAW TEXT THAT LIES TO ITS LEFT.
- if (this_deref && cp < this_deref->marker // There's literal/raw text to the left of the next deref.
- || !this_deref && *cp) // ...or there's no next deref, but there's some literal raw text remaining to be processed.
- {
- for (;; ++infix_count) // FOR EACH TOKEN INSIDE THIS RAW/LITERAL TEXT SECTION.
- {
- // Because neither the postfix array nor the stack can ever wind up with more tokens than were
- // contained in the original infix array, only the infix array need be checked for overflow:
- if (infix_count > MAX_TOKENS - 1) // No room for this operator or operand to be added.
- return LineError(ERR_EXPR_TOO_LONG);
- // Only spaces and tabs are considered whitespace, leaving newlines and other whitespace characters
- // for possible future use:
- cp = omit_leading_whitespace(cp);
- if (!*cp // Very end of expression...
- || this_deref && cp >= this_deref->marker) // ...or no more literal/raw text left to process at the left side of this_deref.
- break; // Break out of inner loop so that bottom of the outer loop will process this_deref itself.
- ExprTokenType &this_infix_item = infix[infix_count]; // Might help reduce code size since it's referenced many places below.
- // CHECK IF THIS CHARACTER IS AN OPERATOR.
- cp1 = cp[1]; // Improves performance by nearly 5% and appreciably reduces code size (at the expense of being less maintainable).
- switch (*cp)
- {
- // The most common cases are kept up top to enhance performance if switch() is implemented as if-else ladder.
- case '+':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_ADD;
- }
- else
- {
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol))
- {
- if (cp1 == '+')
- {
- // For consistency, assume that since the previous item is an operand (even if it's
- // ')'), this is a post-op that applies to that operand. For example, the following
- // are all treated the same for consistency (implicit concatention where the '.'
- // is omitted is rare anyway).
- // x++ y
- // x ++ y
- // x ++y
- // The following implicit concat is deliberately unsupported:
- // "string" ++x
- // The ++ above is seen as applying to the string because it doesn't seem worth
- // the complexity to distinguish between expressions that can accept a post-op
- // and those that can't (operands other than variables can have a post-op;
- // e.g. (x:=y)++).
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_POST_INCREMENT;
- }
- else
- this_infix_item.symbol = SYM_ADD;
- }
- else if (cp1 == '+') // Pre-increment.
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_PRE_INCREMENT;
- }
- else // Remove unary pluses from consideration since they do not change the calculation.
- --infix_count; // Counteract the loop's increment.
- }
- break;
- case '-':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_SUBTRACT;
- break;
- }
- // Otherwise (since above didn't "break"):
- // Must allow consecutive unary minuses because otherwise, the following example
- // would not work correctly when y contains a negative value: var := 3 * -y
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol))
- {
- if (cp1 == '-')
- {
- // See comments at SYM_POST_INCREMENT about this section.
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_POST_DECREMENT;
- }
- else
- this_infix_item.symbol = SYM_SUBTRACT;
- }
- else if (cp1 == '-') // Pre-decrement.
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_PRE_DECREMENT;
- }
- else // Unary minus.
- {
- // Set default for cases where the processing below this line doesn't determine
- // it's a negative numeric literal:
- this_infix_item.symbol = SYM_NEGATIVE;
- // v1.0.40.06: The smallest signed 64-bit number (-0x8000000000000000) wasn't properly
- // supported in previous versions because its unary minus was being seen as an operator,
- // and thus the raw number was being passed as a positive to _atoi64() or _strtoi64(),
- // neither of which would recognize it as a valid value. To correct this, a unary
- // minus followed by a raw numeric literal is now treated as a single literal number
- // rather than unary minus operator followed by a positive number.
- //
- // To be a valid "literal negative number", the character immediately following
- // the unary minus must not be:
- // 1) Whitespace (atoi() and such don't support it, nor is it at all conventional).
- // 2) An open-parenthesis such as the one in -(x).
- // 3) Another unary minus or operator such as --x (which is the pre-decrement operator).
- // To cover the above and possibly other unforeseen things, insist that the first
- // character be a digit (even a hex literal must start with 0).
- if ((cp1 >= '0' && cp1 <= '9') || cp1 == '.') // v1.0.46.01: Recognize dot too, to support numbers like -.5.
- {
- for (op_end = cp + 2; !strchr(EXPR_OPERAND_TERMINATORS, *op_end); ++op_end); // Find the end of this number (can be '\0').
- // 1.0.46.11: Due to obscurity, no changes have been made here to support scientific
- // notation followed by the power operator; e.g. -1.0e+1**5.
- if (!this_deref || op_end < this_deref->marker) // Detect numeric double derefs such as one created via "12%i% = value".
- {
- // Because the power operator takes precedence over unary minus, don't collapse
- // unary minus into a literal numeric literal if the number is immediately
- // followed by the power operator. This is correct behavior even for
- // -0x8000000000000000 because -0x8000000000000000**2 would in fact be undefined
- // because ** is higher precedence than unary minus and +0x8000000000000000 is
- // beyond the signed 64-bit range. SEE ALSO the comments higher above.
- // Use a temp variable because numeric_literal requires that op_end be set properly:
- char *pow_temp = omit_leading_whitespace(op_end);
- if (!(pow_temp[0] == '*' && pow_temp[1] == '*'))
- goto numeric_literal; // Goto is used for performance and also as a patch to minimize the chance of breaking other things via redesign.
- //else it's followed by pow. Since pow is higher precedence than unary minus,
- // leave this unary minus as an operator so that it will take effect after the pow.
- }
- //else possible double deref, so leave this unary minus as an operator.
- }
- } // Unary minus.
- break;
- case ',':
- this_infix_item.symbol = SYM_COMMA; // Used to separate sub-statements and function parameters.
- break;
- case '/':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_DIVIDE;
- }
- else if (cp1 == '/')
- {
- if (cp[2] == '=')
- {
- cp += 2; // An additional increment to have loop skip over the operator's 2nd & 3rd symbols.
- this_infix_item.symbol = SYM_ASSIGN_FLOORDIVIDE;
- }
- else
- {
- ++cp; // An additional increment to have loop skip over the second '/' too.
- this_infix_item.symbol = SYM_FLOORDIVIDE;
- }
- }
- else
- this_infix_item.symbol = SYM_DIVIDE;
- break;
- case '*':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_MULTIPLY;
- }
- else
- {
- if (cp1 == '*') // Python, Perl, and other languages also use ** for power.
- {
- ++cp; // An additional increment to have loop skip over the second '*' too.
- this_infix_item.symbol = SYM_POWER;
- }
- else
- {
- // Differentiate between unary dereference (*) and the "multiply" operator:
- // See '-' above for more details:
- this_infix_item.symbol = (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol))
- ? SYM_MULTIPLY : SYM_DEREF;
- }
- }
- break;
- case '!':
- if (cp1 == '=') // i.e. != is synonymous with <>, which is also already supported by legacy.
- {
- ++cp; // An additional increment to have loop skip over the '=' too.
- this_infix_item.symbol = SYM_NOTEQUAL;
- }
- else
- // If what lies to its left is a CPARAN or OPERAND, SYM_CONCAT is not auto-inserted because:
- // 1) Allows ! and ~ to potentially be overloaded to become binary and unary operators in the future.
- // 2) Keeps the behavior consistent with unary minus, which could never auto-concat since it would
- // always be seen as the binary subtract operator in such cases.
- // 3) Simplifies the code.
- this_infix_item.symbol = SYM_HIGHNOT; // High-precedence counterpart of the word "not".
- break;
- case '(':
- // The below should not hurt any future type-casting feature because the type-cast can be checked
- // for prior to checking the below. For example, if what immediately follows the open-paren is
- // the string "int)", this symbol is not open-paren at all but instead the unary type-cast-to-int
- // operator.
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol)) // If it's an operand, at this stage it can only be SYM_OPERAND or SYM_STRING.
- {
- if (infix_count > MAX_TOKENS - 2) // -2 to ensure room for this operator and the operand further below.
- return LineError(ERR_EXPR_TOO_LONG);
- this_infix_item.symbol = SYM_CONCAT;
- ++infix_count;
- }
- infix[infix_count].symbol = SYM_OPAREN; // MUST NOT REFER TO this_infix_item IN CASE ABOVE DID ++infix_count.
- break;
- case ')':
- this_infix_item.symbol = SYM_CPAREN;
- break;
- case '=':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the other '=' too.
- this_infix_item.symbol = SYM_EQUALCASE;
- }
- else
- this_infix_item.symbol = SYM_EQUAL;
- break;
- case '>':
- switch (cp1)
- {
- case '=':
- ++cp; // An additional increment to have loop skip over the '=' too.
- this_infix_item.symbol = SYM_GTOE;
- break;
- case '>':
- if (cp[2] == '=')
- {
- cp += 2; // An additional increment to have loop skip over the operator's 2nd & 3rd symbols.
- this_infix_item.symbol = SYM_ASSIGN_BITSHIFTRIGHT;
- }
- else
- {
- ++cp; // An additional increment to have loop skip over the second '>' too.
- this_infix_item.symbol = SYM_BITSHIFTRIGHT;
- }
- break;
- default:
- this_infix_item.symbol = SYM_GT;
- }
- break;
- case '<':
- switch (cp1)
- {
- case '=':
- ++cp; // An additional increment to have loop skip over the '=' too.
- this_infix_item.symbol = SYM_LTOE;
- break;
- case '>':
- ++cp; // An additional increment to have loop skip over the '>' too.
- this_infix_item.symbol = SYM_NOTEQUAL;
- break;
- case '<':
- if (cp[2] == '=')
- {
- cp += 2; // An additional increment to have loop skip over the operator's 2nd & 3rd symbols.
- this_infix_item.symbol = SYM_ASSIGN_BITSHIFTLEFT;
- }
- else
- {
- ++cp; // An additional increment to have loop skip over the second '<' too.
- this_infix_item.symbol = SYM_BITSHIFTLEFT;
- }
- break;
- default:
- this_infix_item.symbol = SYM_LT;
- }
- break;
- case '&':
- if (cp1 == '&')
- {
- ++cp; // An additional increment to have loop skip over the second '&' too.
- this_infix_item.symbol = SYM_AND;
- }
- else if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_BITAND;
- }
- else
- {
- // Differentiate between unary "take the address of" and the "bitwise and" operator:
- // See '-' above for more details:
- this_infix_item.symbol = (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol))
- ? SYM_BITAND : SYM_ADDRESS;
- }
- break;
- case '|':
- if (cp1 == '|')
- {
- ++cp; // An additional increment to have loop skip over the second '|' too.
- this_infix_item.symbol = SYM_OR;
- }
- else if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_BITOR;
- }
- else
- this_infix_item.symbol = SYM_BITOR;
- break;
- case '^':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_BITXOR;
- }
- else
- this_infix_item.symbol = SYM_BITXOR;
- break;
- case '~':
- // If what lies to its left is a CPARAN or OPERAND, SYM_CONCAT is not auto-inserted because:
- // 1) Allows ! and ~ to potentially be overloaded to become binary and unary operators in the future.
- // 2) Keeps the behavior consistent with unary minus, which could never auto-concat since it would
- // always be seen as the binary subtract operator in such cases.
- // 3) Simplifies the code.
- this_infix_item.symbol = SYM_BITNOT;
- break;
- case '?':
- this_infix_item.symbol = SYM_IFF_THEN;
- break;
- case ':':
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the second '|' too.
- this_infix_item.symbol = SYM_ASSIGN;
- }
- else
- this_infix_item.symbol = SYM_IFF_ELSE;
- break;
- case '"': // QUOTED/LITERAL STRING.
- // Note that single and double-derefs are impossible inside string-literals
- // because the load-time deref parser would never detect anything inside
- // of quotes -- even non-escaped percent signs -- as derefs.
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol)) // If it's an operand, at this stage it can only be SYM_OPERAND or SYM_STRING.
- {
- if (infix_count > MAX_TOKENS - 2) // -2 to ensure room for this operator and the operand further below.
- return LineError(ERR_EXPR_TOO_LONG);
- this_infix_item.symbol = SYM_CONCAT;
- ++infix_count;
- }
- // The following section is nearly identical to one in DefineFunc().
- // Find the end of this string literal, noting that a pair of double quotes is
- // a literal double quote inside the string:
- for (op_end = ++cp;;) // Omit the starting-quote from consideration, and from the resulting/built string.
- {
- if (!*op_end) // No matching end-quote. Probably impossible due to load-time validation.
- return LineError(ERR_MISSING_CLOSE_QUOTE); // Since this error string is used in other places, compiler string pooling should result in little extra memory needed for this line.
- if (*op_end == '"') // And if it's not followed immediately by another, this is the end of it.
- {
- ++op_end;
- if (*op_end != '"') // String terminator or some non-quote character.
- break; // The previous char is the ending quote.
- //else a pair of quotes, which resolves to a single literal quote. So fall through
- // to the below, which will copy of quote character to the buffer. Then this pair
- // is skipped over and the loop continues until the real end-quote is found.
- }
- //else some character other than '\0' or '"'.
- ++op_end;
- }
- // Since above didn't "goto", op_end is now the character after the ending '"'.
- // MUST NOT REFER TO this_infix_item IN CASE HIGHER ABOVE DID ++infix_count:
- infix[infix_count].symbol = SYM_STRING; // Marked explicitly as string vs. SYM_OPERAND to prevent it from being seen as a number, e.g. if (var == "12.0") would be false if var contains "12" with no trailing ".0".
- if ( !(infix[infix_count].marker = SimpleHeap::Malloc(cp, op_end - cp - 1)) ) // -1 to omit the ending quote. cp was already adjusted to omit the starting quote.
- return LineError(ERR_OUTOFMEM);
- StrReplace(infix[infix_count].marker, "\"\"", "\"", SCS_SENSITIVE); // Resolve each "" into a single ". Consequently, a little bit of memory in "marker" might be wasted, but it doesn't seem worth the code size to compensate for this.
- cp = op_end; // Have the loop process whatever lies at op_end and beyond.
- continue; // Continue vs. break to avoid the ++cp at the bottom. Above has already set cp to be the character after this literal string's close-quote.
- default: // NUMERIC-LITERAL, DOUBLE-DEREF, RELATIONAL OPERATOR SUCH AS "NOT", OR UNRECOGNIZED SYMBOL.
- if (*cp == '.') // This one must be done here rather than as a "case". See comment below.
- {
- if (cp1 == '=')
- {
- ++cp; // An additional increment to have loop skip over the operator's second symbol.
- this_infix_item.symbol = SYM_ASSIGN_CONCAT;
- break;
- }
- if (IS_SPACE_OR_TAB(cp1))
- {
- this_infix_item.symbol = SYM_CONCAT;
- break;
- }
- //else this is a '.' that isn't followed by a space, tab, or '='. So it's probably
- // a number without a leading zero like .2, so continue on below to process it.
- // BACKWARD COMPATIBILITY: The above behavior creates ambiguity between a "pure"
- // concat operator (.) and numbers that begin with a decimal point. I think that
- // came about because the concat operator was added after numbers like .5 had been
- // supported a long time. In any case, it's documented that the '.' operator must
- // have a space on both sides to be valid, and maybe automatic/implicit concatenation
- // handles most such situations properly anyway (e.g. the expression "x .5").
- }
- // Find the end of this operand or keyword, even if that end extended into the next deref.
- // StrChrAny() is not used because if *op_end is '\0', the strchr() below will find it too:
- for (op_end = cp + 1; !strchr(EXPR_OPERAND_TERMINATORS, *op_end); ++op_end);
- // Now op_end marks the end of this operand or keyword. That end might be the zero terminator
- // or the next operator in the expression, or just a whitespace.
- if (this_deref && op_end >= this_deref->marker)
- goto double_deref; // This also serves to break out of the inner for(), equivalent to a break.
- // Otherwise, this operand is a normal raw numeric-literal or a word-operator (and/or/not).
- // The section below is very similar to the one used at load-time to recognize and/or/not,
- // so it should be maintained with that section. UPDATE for v1.0.45: The load-time parser
- // now resolves "OR" to || and "AND" to && to improve runtime performance and reduce code size here.
- // However, "NOT" but still be parsed here at runtime because it's not quite the same as the "!"
- // operator (different precedence), and it seemed too much trouble to invent some special
- // operator symbol for load-time to insert as a placeholder/substitute (especially since that
- // symbol would appear in ListLines).
- if (op_end-cp == 3
- && (cp[0] == 'n' || cp[0] == 'N')
- && ( cp1 == 'o' || cp1 == 'O')
- && (cp[2] == 't' || cp[2] == 'T')) // "NOT" was found.
- {
- this_infix_item.symbol = SYM_LOWNOT;
- cp = op_end; // Have the loop process whatever lies at op_end and beyond.
- continue; // Continue vs. break to avoid the ++cp at the bottom (though it might not matter in this case).
- }
- numeric_literal:
- // Since above didn't "continue", this item is probably a raw numeric literal (either SYM_FLOAT
- // or SYM_INTEGER, to be differentiated later) because just about every other possibility has
- // been ruled out above. For example, unrecognized symbols should be impossible at this stage
- // because load-time validation would have caught them. And any kind of unquoted alphanumeric
- // characters (other than "NOT", which was detected above) wouldn't have reached this point
- // because load-time pre-parsing would have marked it as a deref/var, not raw/literal text.
- if ( toupper(op_end[-1]) == 'E' // v1.0.46.11: It looks like scientific notation...
- && !(cp[0] == '0' && toupper(cp[1]) == 'X') // ...and it's not a hex number (this check avoids falsely detecting hex numbers that end in 'E' as exponents). This line fixed in v1.0.46.12.
- && !(cp[0] == '-' && cp[1] == '0' && toupper(cp[2]) == 'X') // ...and it's not a negative hex number (this check avoids falsely detecting hex numbers that end in 'E' as exponents). This line added as a fix in v1.0.47.03.
- )
- {
- // Since op_end[-1] is the 'E' or an exponent, the only valid things for op_end[0] to be
- // are + or - (it can't be a digit because the loop above would never have stopped op_end
- // at a digit). If it isn't + or -, it's some kind of syntax error, so doing the following
- // seems harmless in any case:
- do // Skip over the sign and its exponent; e.g. the "+1" in "1.0e+1". There must be a sign in this particular sci-notation number or we would never have arrived here.
- ++op_end;
- while (*op_end >= '0' && *op_end <= '9'); // Avoid isdigit() because it sometimes causes a debug assertion failure at: (unsigned)(c + 1) <= 256 (probably only in debug mode), and maybe only when bad data got in it due to some other bug.
- }
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol)) // If it's an operand, at this stage it can only be SYM_OPERAND or SYM_STRING.
- {
- if (infix_count > MAX_TOKENS - 2) // -2 to ensure room for this operator and the operand further below.
- return LineError(ERR_EXPR_TOO_LONG);
- this_infix_item.symbol = SYM_CONCAT;
- ++infix_count;
- }
- // MUST NOT REFER TO this_infix_item IN CASE ABOVE DID ++infix_count:
- infix[infix_count].symbol = SYM_OPERAND;
- if ( !(infix[infix_count].marker = SimpleHeap::Malloc(cp, op_end - cp)) )
- return LineError(ERR_OUTOFMEM);
- cp = op_end; // Have the loop process whatever lies at op_end and beyond.
- continue; // "Continue" to avoid the ++cp at the bottom.
- } // switch() for type of symbol/operand.
- ++cp; // i.e. increment only if a "continue" wasn't encountered somewhere above. Although maintainability is reduced to do this here, it avoids dozens of ++cp in other places.
- } // for each token in this section of raw/literal text.
- } // End of processing of raw/literal text (such as operators) that lie to the left of this_deref.
- if (!this_deref) // All done because the above just processed all the raw/literal text (if any) that
- break; // lay to the right of the last deref.
- // THE ABOVE HAS NOW PROCESSED ANY/ALL RAW/LITERAL TEXT THAT LIES TO THE LEFT OF this_deref.
- // SO NOW PROCESS THIS_DEREF ITSELF.
- if (infix_count > MAX_TOKENS - 1) // No room for the deref item below to be added.
- return LineError(ERR_EXPR_TOO_LONG);
- DerefType &this_deref_ref = *this_deref; // Boosts performance slightly.
- if (this_deref_ref.is_function) // Above has ensured that at this stage, this_deref!=NULL.
- {
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol)) // If it's an operand, at this stage it can only be SYM_OPERAND or SYM_STRING.
- {
- if (infix_count > MAX_TOKENS - 2) // -2 to ensure room for this operator and the operand further below.
- return LineError(ERR_EXPR_TOO_LONG);
- infix[infix_count++].symbol = SYM_CONCAT;
- }
- infix[infix_count].symbol = SYM_FUNC;
- infix[infix_count].deref = this_deref;
- }
- else // this_deref is a variable.
- {
- if (*this_deref_ref.marker == g_DerefChar) // A double-deref because normal derefs don't start with '%'.
- {
- // Find the end of this operand, even if that end extended into the next deref.
- // StrChrAny() is not used because if *op_end is '\0', the strchr() below will find it too:
- for (op_end = this_deref_ref.marker + this_deref_ref.length; !strchr(EXPR_OPERAND_TERMINATORS, *op_end); ++op_end);
- goto double_deref;
- }
- else
- {
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol)) // If it's an operand, at this stage it can only be SYM_OPERAND or SYM_STRING.
- {
- if (infix_count > MAX_TOKENS - 2) // -2 to ensure room for this operator and the operand further below.
- return LineError(ERR_EXPR_TOO_LONG);
- infix[infix_count++].symbol = SYM_CONCAT;
- }
- if (this_deref_ref.var->Type() == VAR_NORMAL // VAR_ALIAS is taken into account (and resolved) by Type().
- && g_NoEnv) // v1.0.43.08: Added g_NoEnv. Relies on short-circuit boolean order.
- // "!this_deref_ref.var->Get()" isn't checked here. See comments in SYM_DYNAMIC evaluation.
- {
- // DllCall() and possibly others rely on this having been done to support changing the
- // value of a parameter (similar to by-ref).
- infix[infix_count].symbol = SYM_VAR; // Type() is VAR_NORMAL as verified above; but SYM_VAR can be the clipboard in the case of expression lvalues. Search for VAR_CLIPBOARD further below for details.
- }
- else // It's either a built-in variable (including clipboard) OR a possible environment variable.
- {
- infix[infix_count].symbol = SYM_DYNAMIC;
- infix[infix_count].buf = NULL; // SYM_DYNAMIC requires that buf be set to NULL for non-double-deref vars (since there are two different types of SYM_DYNAMIC).
- }
- infix[infix_count].var = this_deref_ref.var;
- }
- } // Handling of the var or function in this_deref.
- // Finally, jump over the dereference text. Note that in the case of an expression, there might not
- // be any percent signs within the text of the dereference, e.g. x + y, not %x% + %y% (unless they're
- // deliberately double-derefs).
- cp += this_deref_ref.length;
- // The outer loop will now do ++infix for us.
- continue; // To avoid falling into the label below. The label below is only reached by explicit goto.
- double_deref: // Caller has set cp to be start and op_end to be the character after the last one of the double deref.
- if (infix_count && YIELDS_AN_OPERAND(infix[infix_count - 1].symbol)) // If it's an operand, at this stage it can only be SYM_OPERAND or SYM_STRING.
- {
- if (infix_count > MAX_TOKENS - 2) // -2 to ensure room for this operator and the operand further below.
- return LineError(ERR_EXPR_TOO_LONG);
- infix[infix_count++].symbol = SYM_CONCAT;
- }
- infix[infix_count].symbol = SYM_DYNAMIC;
- if ( !(infix[infix_count].buf = SimpleHeap::Malloc(cp, op_end - cp)) ) // Example string: "Array%i%"
- return LineError(ERR_OUTOFMEM);
- // Set "deref" properly for the loop to resume processing at the item after this double deref.
- // Callers of double_deref have ensured that deref!=NULL and deref->marker!=NULL (because it
- // doesn't make sense to have a double-deref unless caller discovered the first deref that
- // belongs to this double deref, such as the "i" in Array%i%).
- for (deref_start = deref, ++deref; deref->marker && deref->marker < op_end; ++deref);
- derefs_in_this_double = (int)(deref - deref_start);
- --deref; // Compensate for the outer loop's ++deref.
- // There's insufficient room to shoehorn all the necessary data into the token (since circuit_token probably
- // can't be safely overloaded at this stage), so allocate a little bit of stack memory, just enough for the
- // number of derefs (variables) whose contents comprise the name of this double-deref variable (typically
- // there's only one; e.g. the "i" in Array%i%).
- if ( !(deref_new = (DerefType *)SimpleHeap::Malloc((derefs_in_this_double + 1) * sizeof(DerefType))) ) // Provides one extra at the end as a terminator.
- return LineError(ERR_OUTOFMEM);
- memcpy(deref_new, deref_start, derefs_in_this_double * sizeof(DerefType));
- deref_new[derefs_in_this_double].marker = NULL; // Put a NULL in the last item, which terminates the array.
- for (deref_start = deref_new; deref_start->marker; ++deref_start)
- deref_start->marker = infix[infix_count].buf + (deref_start->marker - cp); // Point each to its position in the *new* buf.
- infix[infix_count].var = (Var *)deref_new; // Postfix evaluation uses this to build the variable's name dynamically.
- if (*op_end == '(') // i.e. dynamic function call (v1.0.47.06)
- {
- if (infix_count > MAX_TOKENS - 2) // No room for the following symbol to be added (plus the ++infix done that will be done by the outer loop).
- return LineError(ERR_EXPR_TOO_LONG);
- deref_start->is_function = true; // As a result of the loop above, deref_start is the null-marker deref which terminates the deref list.
- deref_start->param_count = deref_new->param_count; // param_count was set when the derefs were parsed.
- ++infix_count; // THIS CREATES ANOTHER TOKEN for the function call itself. Order in infix is SYM_DYNAMIC + SYM_FUNC + (parameter tokens/operators).
- infix[infix_count].symbol = SYM_FUNC;
- infix[infix_count].deref = deref_start; // See comment below.
- // The trick here is that this SYM_FUNC now points to one of the same deref items that the
- // corresponding SYM_DYNAMIC does. During postfix evaluation, that allows SYM_DYNAMIC to update
- // the attributes of that deref so that when the SYM_FUNC is executed, it will know which function
- // to call.
- }
- else
- deref_start->is_function = false;
- cp = op_end; // Must be done only after above is done using cp: Set things up for the next iteration.
- // The outer loop will now do ++infix for us.
- } // For each deref in this expression, and also for the final literal/raw text to the right of the last deref.
- // Terminate the array with a special item. This allows infix-to-postfix conversion to do a faster
- // traversal of the infix array.
- if (infix_count > MAX_TOKENS - 1) // No room for the following symbol to be added.
- return LineError(ERR_EXPR_TOO_LONG);
- infix[infix_count].symbol = SYM_INVALID;
- ////////////////////////////
- // CONVERT INFIX TO POSTFIX.
- ////////////////////////////
- // SYM_BEGIN is the first item to go on the stack. It's a flag to indicate that conversion to postfix has begun:
- ExprTokenType token_begin;
- token_begin.symbol = SYM_BEGIN;
- STACK_PUSH(&token_begin);
- SymbolType stack_symbol, infix_symbol, sym_prev;
- ExprTokenType *fwd_infix, *this_infix = infix;
- int functions_on_stack = 0;
- for (;;) // While SYM_BEGIN is still on the stack, continue iterating.
- {
- ExprTokenType *&this_postfix = postfix[postfix_count]; // Resolve early, especially for use by "goto". Reduces code size a bit, though it doesn't measurably help performance.
- infix_symbol = this_infix->symbol; //
- stack_symbol = stack[stack_count - 1]->symbol; // Frequently used, so resolve only once to help performance.
- // Put operands into the postfix array immediately, then move on to the next infix item:
- if (IS_OPERAND(infix_symbol)) // At this stage, operands consist of only SYM_OPERAND and SYM_STRING.
- {
- if (infix_symbol == SYM_DYNAMIC && SYM_DYNAMIC_IS_VAR_NORMAL_OR_CLIP(this_infix)) // Ordered for short-circuit performance.
- {
- // v1.0.46.01: If an environment variable is being used as an lvalue -- regardless
- // of whether that variable is blank in the environment -- treat it as a normal
- // variable instead. This is because most people would want that, and also because
- // it's tranditional not to directly support assignments to environment variables
- // (only EnvSet can do that, mostly for code simplicity). In addition, things like
- // EnvVar.="string" and EnvVar+=2 aren't supported due to obscurity/rarity (instead,
- // such expressions treat EnvVar as blank). In light of all this, convert environment
- // variables that are targets of ANY assignments into normal variables so that they
- // can be seen as a valid lvalues when the time comes to do the assignment.
- // IMPORTANT: VAR_CLIPBOARD is made into SYM_VAR here, but only for assignments.
- // This allows built-in functions and other places in the code to treat SYM_VAR
- // as though it's always VAR_NORMAL, which reduces code size and improves maintainability.
- sym_prev = this_infix[1].symbol; // Resolve to help macro's code size and performance.
- if (IS_ASSIGNMENT_OR_POST_OP(sym_prev) // Post-op must be checked for VAR_CLIPBOARD (by contrast, it seems unnecessary to check it for others; see comments below).
- || stack_symbol == SYM_PRE_INCREMENT || stack_symbol == SYM_PRE_DECREMENT) // Stack *not* infix.
- this_infix->symbol = SYM_VAR; // Convert clipboard or environment variable into SYM_VAR.
- // POST-INC/DEC: It seems unnecessary to check for these except for VAR_CLIPBOARD because
- // those assignments (and indeed any assignment other than .= and :=) will have no effect
- // on a ON A SYM_DYNAMIC environment variable. This is because by definition, such
- // variables have an empty Var::Contents(), and AutoHotkey v1 does not allow
- // math operations on blank variables. Thus, the result of doing a math-assignment
- // operation on a blank lvalue is almost the same as doing it on an invalid lvalue.
- // The main difference is that with the exception of post-inc/dec, assignments
- // wouldn't produce an lvalue unless we explicitly check for them all above.
- // An lvalue should be produced so that the following features are consistent
- // even for variables whose names happen to match those of environment variables:
- // - Pass an assignment byref or takes its address; e.g. &(++x).
- // - Cascading assigments; e.g. (++var) += 4 (rare to be sure).
- // - Possibly other lvalue behaviors that rely on SYM_VAR being present.
- // Above logic might not be perfect because it doesn't check for parens such as (var):=x,
- // and possibly other obscure types of assignments. However, it seems adequate given
- // the rarity of such things and also because env vars are being phased out (scripts can
- // use #NoEnv to avoid all such issues).
- }
- this_postfix = this_infix++;
- this_postfix->circuit_token = NULL; // Set default. It's only ever overridden after it's in the postfix array.
- ++postfix_count;
- continue; // Doing a goto to a hypothetical "standard_postfix_circuit_token" (in lieu of these last 3 lines) reduced performance and didn't help code size.
- }
- // Since above didn't "continue", the current infix symbol is not an operand, but an operator or other symbol.
- switch(infix_symbol)
- {
- case SYM_CPAREN: // Listed first for performance. It occurs frequently while emptying the stack to search for the matching open-parenthesis.
- if (stack_symbol == SYM_OPAREN) // See comments near the bottom of this case. The first open-paren on the stack must be the one that goes with this close-paren.
- {
- --stack_count; // Remove this open-paren from the stack, since it is now complete.
- ++this_infix; // Since this pair of parentheses is done, move on to the next token in the infix expression.
- // There should be no danger of stack underflow in the following because SYM_BEGIN always
- // exists at the bottom of the stack:
- if (stack[stack_count - 1]->symbol == SYM_FUNC) // i.e. topmost item on stack is SYM_FUNC.
- {
- --functions_on_stack;
- goto standard_pop_into_postfix; // Within the postfix list, a function-call should always immediately follow its params.
- }
- }
- else if (stack_symbol == SYM_BEGIN) // This can happen with bad expressions like "Var := 1 ? (:) :" even though theoretically it should mean that paren is closed without having been opened (currently impossible due to load-time balancing).
- return LineError(ERR_MISSING_OPEN_PAREN); // Since this error string is used in other places, compiler string pooling should result in little extra memory needed for this line.
- else // This stack item is an operator.
- {
- goto standard_pop_into_postfix;
- // By not incrementing i, the loop will continue to encounter SYM_CPAREN and thus
- // continue to pop things off the stack until the corresponding OPAREN is reached.
- }
- break;
- case SYM_FUNC:
- ++functions_on_stack; // This technique performs well but prevents multi-statements from being nested inside function calls (seems too obscure to worry about); e.g. fn((x:=5, y+=3), 2)
- STACK_PUSH(this_infix++);
- // NOW FALL INTO THE OPEN-PAREN BELOW because load-time validation has ensured that each SYM_FUNC
- // is followed by a '('.
- // ABOVE CASE FALLS INTO BELOW.
- case SYM_OPAREN:
- // Open-parentheses always go on the stack to await their matching close-parentheses.
- STACK_PUSH(this_infix++);
- break;
- case SYM_IFF_ELSE: // i.e. this infix symbol is ':'.
- if (stack_symbol == SYM_BEGIN) // An ELSE with no matching IF/THEN.
- return L