/***********************************************************************************************************
*   Justina interpreter library                                                                            *
*                                                                                                          *
*   Copyright 2024, 2025 Herwig Taveirne                                                                   *
*                                                                                                          *
*   This file is part of the Justina Interpreter library.                                                  *
*   The Justina interpreter library 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 3 of the License, or (at your option) any later version.                                       *
*                                                                                                          *
*   This library 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.                                                   *
*                                                                                                          *
*   You should have received a copy of the GNU General Public License along with this program. If not,     *
*   see https://www.gnu.org/licenses.                                                                      *
*                                                                                                          *
*   The library is intended to work with 32 bit boards using the SAMD architecture ,                       *
*   the Arduino nano RP2040 and Arduino nano ESP32 boards.                                                 *
*                                                                                                          *
*   See GitHub for more information and documentation: https://github.com/Herwig9820/Justina_interpreter   *
*                                                                                                          *
***********************************************************************************************************/


#include "Justina.h"

#define PRINT_HEAP_OBJ_CREA_DEL 0
#define PRINT_DEBUG_INFO 0
#define PRINT_OBJECT_COUNT_ERRORS 0


// *****************************************************
// ***        class Justina - implementation         ***
// *****************************************************

// ----------------------------------------------
// *   initialization of static class members   *
// ----------------------------------------------

// internal commands: keywords with attributes
// -------------------------------------------

// maximum number of allowed arguments should be no more than defined in constant 'c_internalFncOrCmdMaxArgs'

const Justina::internCmdDef Justina::_internCommands[]{
    //  name            id code                 where allowed                                          #arg     param key       control info
    //  ----            -------                 -------------                                          ----     ---------       ------------   

    // declare and delete variables
    // ----------------------------
    {"var",             cmdcod_var,             cmd_noRestrictions | cmd_skipDuringExec,                1,15,   cmdArgSeq_102,  cmdBlockNone},
    {"const",           cmdcod_constVar,        cmd_noRestrictions | cmd_skipDuringExec,                1,15,   cmdArgSeq_102,  cmdBlockNone},
    {"static",          cmdcod_static,          cmd_onlyInFunctionBlock | cmd_skipDuringExec,           1,15,   cmdArgSeq_102,  cmdBlockNone},

    {"delete",          cmdcod_deleteVar,       cmd_onlyCommandLineStart | cmd_skipDuringExec
                                                                                | cmd_notInDebugMode,   1,15,   cmdArgSeq_110,  cmdBlockNone},      // can only delete user variables (command line only)

    {"clearMem",        cmdcod_clearAll ,       cmd_onlyCommandLine | cmd_skipDuringExec
                                                                                | cmd_notInDebugMode,   0,0,    cmdArgSeq_100,  cmdBlockNone},      // executed AFTER execution phase ends
    {"clearProg",       cmdcod_clearProg,       cmd_onlyImmediate | cmd_skipDuringExec
                                                                                | cmd_notInDebugMode,   0,0,    cmdArgSeq_100,  cmdBlockNone},      // executed AFTER execution phase ends

    // program and flow control commands
    // ---------------------------------
    {"loadProg",        cmdcod_loadProg,        cmd_onlyImmediate | cmd_notInDebugMode,                 0,1,    cmdArgSeq_101,  cmdBlockNone},

    {"program",         cmdcod_program,         cmd_onlyProgramTop | cmd_skipDuringExec,                1,1,    cmdArgSeq_110,  cmdBlockNone},
    {"function",        cmdcod_function,        cmd_onlyInProgram | cmd_skipDuringExec,                 1,1,    cmdArgSeq_109,  cmdBlockJustinaFunction},
    {"procedure",       cmdcod_voidFunction,    cmd_onlyInProgram | cmd_skipDuringExec,                 1,1,    cmdArgSeq_109,  cmdBlockJustinaFunction},

    {"for",             cmdcod_for,             cmd_onlyImmOrInsideFuncBlock,                           2,3,    cmdArgSeq_103,  cmdBlockFor},
    {"while",           cmdcod_while,           cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockWhile},
    {"if",              cmdcod_if,              cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockIf},
    {"elseif",          cmdcod_elseif,          cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockIf_elseIf},
    {"else",            cmdcod_else,            cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockIf_else},
    {"end",             cmdcod_end,             cmd_noRestrictions,                                     0,0,    cmdArgSeq_100,  cmdBlockGenEnd},    // closes inner open command block

    {"break",           cmdcod_break,           cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockOpenBlock_loop},        // allowed if at least one open loop block (any level) 
    {"continue",        cmdcod_continue,        cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockOpenBlock_loop },       // allowed if at least one open loop block (any level) 
    {"return",          cmdcod_return,          cmd_onlyImmOrInsideFuncBlock,                           0,1,    cmdArgSeq_101,  cmdBlockOpenBlock_function},    // allowed if currently an open function definition block 

    {"pause",           cmdcod_pause,           cmd_onlyImmOrInsideFuncBlock,                           0,1,    cmdArgSeq_101,  cmdBlockNone},
    {"halt",            cmdcod_halt,            cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},

    // batch file commands
    // -------------------
    {"exec",            cmdcod_execBatchFile,   cmd_onlyImmediate,                                      1,10,   cmdArgSeq_101,  cmdBlockNone},      // execute a batch file (not a program) and pass optional arguments, return to calling batch file or console
    {"ditch",           cmdcod_ditchBatchFile,  cmd_onlyInBatchFile,                                    0,0,    cmdArgSeq_100,  cmdBlockNone},      // stop executing a batch file (not a program)
    {"gotoLabel",       cmdcod_gotoLabel,       cmd_onlyInBatchFile,                                    1,1,    cmdArgSeq_101,  cmdBlockNone},      // jump to a label in a batch file     
    {"silent",          cmdcod_silent,          cmd_onlyInBatchFile,                                    1,1,    cmdArgSeq_101,  cmdBlockNone},      // batch files only: no prompt, no last results     

    // debugging commands
    // ------------------
    {"stop",            cmdcod_stop,            cmd_onlyInFunctionBlock,                                0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"nop",             cmdcod_nop,             cmd_onlyInFunctionBlock | cmd_skipDuringExec,           0,0,    cmdArgSeq_100,  cmdBlockNone},      // insert two bytes in program, do nothing

    {"go",              cmdcod_go,              cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},      // not allowed in a batch file
    {"step",            cmdcod_step,            cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"stepOut",         cmdcod_stepOut,         cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"stepOver",        cmdcod_stepOver,        cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"bStepOut",        cmdcod_stepOutOfBlock,  cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"loop",            cmdcod_stepToBlockEnd,  cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"setNextLine",     cmdcod_setNextLine,     cmd_onlyCommandLine | cmd_onlyInDebugMode,              1,1,    cmdArgSeq_101,  cmdBlockNone},

    {"abort",           cmdcod_abort,           cmd_onlyCommandLine | cmd_onlyInDebugMode,              0,0,    cmdArgSeq_100,  cmdBlockNone},

    {"debug",           cmdcod_debug,           cmd_onlyImmediate,                                      0,0,    cmdArgSeq_100,  cmdBlockNone},

    {"watch",           cmdcod_watch,           cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"watchExprOn",     cmdcod_watchExprOn,     cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"watchExprOff",    cmdcod_watchExprOff,    cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},

    {"BPon",            cmdcod_BPon,            cmd_onlyImmediate,                                      0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"BPoff",           cmdcod_BPoff,           cmd_onlyImmediate,                                      0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"BPactivate",      cmdcod_BPactivate,      cmd_onlyImmediate,                                      0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"setBP",           cmdcod_setBP,           cmd_onlyImmediate,                                      1,9,    cmdArgSeq_101,  cmdBlockNone},
    {"clearBP",         cmdcod_clearBP,         cmd_onlyImmediate,                                      0,9,    cmdArgSeq_101,  cmdBlockNone},
    {"enableBP",        cmdcod_enableBP,        cmd_onlyImmediate,                                      1,9,    cmdArgSeq_101,  cmdBlockNone},
    {"disableBP",       cmdcod_disableBP,       cmd_onlyImmediate,                                      1,9,    cmdArgSeq_101,  cmdBlockNone},
    {"moveBP",          cmdcod_moveBP,          cmd_onlyImmediate,                                      2,2,    cmdArgSeq_101,  cmdBlockNone},

    {"raiseError",      cmdcod_raiseError,      cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"trapErrors",      cmdcod_trapErrors,      cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"clearError",      cmdcod_clearError,      cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"quit",            cmdcod_quit,            cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},

    // settings
    // --------
    {"dispWidth",       cmdcod_dispwidth,       cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"floatFmt",        cmdcod_floatfmt,        cmd_onlyImmOrInsideFuncBlock,                           1,3,    cmdArgSeq_101,  cmdBlockNone},
    {"intFmt",          cmdcod_intfmt,          cmd_onlyImmOrInsideFuncBlock,                           1,3,    cmdArgSeq_101,  cmdBlockNone},
    {"dispMode",        cmdcod_dispmod,         cmd_onlyImmOrInsideFuncBlock,                           2,2,    cmdArgSeq_101,  cmdBlockNone},
    {"tabSize",         cmdcod_tabSize,         cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"angleMode",       cmdcod_angle,           cmd_onlyImmOrInsideFuncBlock,                           1,1,    cmdArgSeq_101,  cmdBlockNone},

    // input and output commands
    // -------------------------

    {"setConsole",      cmdcod_setConsole,      cmd_onlyImmediate,                                      1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"setConsoleIn",    cmdcod_setConsIn,       cmd_onlyImmediate,                                      1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"setConsoleOut",   cmdcod_setConsOut,      cmd_onlyImmediate,                                      1,1,    cmdArgSeq_101,  cmdBlockNone},
    {"setDebugOut",     cmdcod_setDebugOut,     cmd_onlyImmediate,                                      1,1,    cmdArgSeq_101,  cmdBlockNone},

    {"info",            cmdcod_info,            cmd_onlyImmOrInsideFuncBlock,                           1,2,    cmdArgSeq_104,  cmdBlockNone},
    {"input",           cmdcod_input,           cmd_onlyImmOrInsideFuncBlock,                           3,3,    cmdArgSeq_104,  cmdBlockNone},

    {"startSD",         cmdcod_startSD,         cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},
    {"stopSD",          cmdcod_stopSD,          cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},

    {"receiveFile",     cmdcod_receiveFile,     cmd_onlyImmOrInsideFuncBlock,                           1,3,    cmdArgSeq_101,  cmdBlockNone},
    {"sendFile",        cmdcod_sendFile,        cmd_onlyImmOrInsideFuncBlock,                           1,3,    cmdArgSeq_101,  cmdBlockNone},
    {"copyFile",        cmdcod_copyFile,        cmd_onlyImmOrInsideFuncBlock,                           2,3,    cmdArgSeq_101,  cmdBlockNone},

    {"dbout",           cmdcod_dbout,           cmd_onlyImmOrInsideFuncBlock,                           1,15,   cmdArgSeq_101,  cmdBlockNone},      // values (expressions) to print to debug out
    {"dboutLine",       cmdcod_dboutLine,       cmd_onlyImmOrInsideFuncBlock,                           0,15,   cmdArgSeq_101,  cmdBlockNone},

    {"cout",            cmdcod_cout,            cmd_onlyImmOrInsideFuncBlock,                           1,15,   cmdArgSeq_101,  cmdBlockNone},      // values (expressions) to print to console
    {"coutLine",        cmdcod_coutLine,        cmd_onlyImmOrInsideFuncBlock,                           0,15,   cmdArgSeq_101,  cmdBlockNone},
    {"coutList",        cmdcod_coutList,        cmd_onlyImmOrInsideFuncBlock,                           1,15,   cmdArgSeq_101,  cmdBlockNone},

    {"print",           cmdcod_print,           cmd_onlyImmOrInsideFuncBlock,                           2,16,   cmdArgSeq_101,  cmdBlockNone},      // stream, values (expressions) to print to stream
    {"printLine",       cmdcod_printLine,       cmd_onlyImmOrInsideFuncBlock,                           1,16,   cmdArgSeq_101,  cmdBlockNone},
    {"printList",       cmdcod_printList,       cmd_onlyImmOrInsideFuncBlock,                           2,16,   cmdArgSeq_101,  cmdBlockNone},

    {"vprint",          cmdcod_printToVar,      cmd_onlyImmOrInsideFuncBlock,                           2,16,   cmdArgSeq_101,  cmdBlockNone},      // variable, values (expressions) to print to variable
    {"vprintLine",      cmdcod_printLineToVar,  cmd_onlyImmOrInsideFuncBlock,                           1,16,   cmdArgSeq_101,  cmdBlockNone},
    {"vprintList",      cmdcod_printListToVar,  cmd_onlyImmOrInsideFuncBlock,                           2,16,   cmdArgSeq_101,  cmdBlockNone},

    {"listCallStack",   cmdcod_printCallSt,     cmd_onlyImmOrInsideFuncBlock,                           0,1,    cmdArgSeq_101,  cmdBlockNone},      // print call stack to stream (default is console)
    {"listBP",          cmdcod_printBP,         cmd_onlyImmOrInsideFuncBlock,                           0,1,    cmdArgSeq_101,  cmdBlockNone},      // list breakpoints
    {"listVars",        cmdcod_printVars,       cmd_onlyImmOrInsideFuncBlock,                           0,1,    cmdArgSeq_101,  cmdBlockNone},      // list variables "         "         "         "
    {"listFiles",       cmdcod_listFiles,       cmd_onlyImmOrInsideFuncBlock,                           0,1,    cmdArgSeq_101,  cmdBlockNone},      // list files     "         "         "         "
    {"listFilesToSerial",cmdcod_listFilesToSer, cmd_onlyImmOrInsideFuncBlock,                           0,0,    cmdArgSeq_100,  cmdBlockNone},      // list files to Serial with modification dates (SD library fixed)
};


// internal cpp Justina functions: returning a result
// --------------------------------------------------

// the 8 array pattern bits indicate the order of arrays and scalars; bit b0 to bit b7 refer to parameter 1 to 8, if a bit is set, an array is expected as argument
// if more than 8 arguments are supplied, only arguments 1 to 8 can be set as array arguments

// maximum number of allowed arguments should be no more than defined in constant 'c_internalFncOrCmdMaxArgs'

const Justina::InternCppFuncDef Justina::_internCppFunctions[]{
    //  name                    id code                         #arg    array pattern
    //  ----                    -------                         ----    -------------   

    // math functions
    {"sqrt",                    fnccod_sqrt,                    1,1,    0b0},
    {"sin",                     fnccod_sin,                     1,1,    0b0},
    {"cos",                     fnccod_cos,                     1,1,    0b0},
    {"tan",                     fnccod_tan,                     1,1,    0b0},
    {"asin",                    fnccod_asin,                    1,1,    0b0},
    {"acos",                    fnccod_acos,                    1,1,    0b0},
    {"atan",                    fnccod_atan,                    1,1,    0b0},
    {"ln",                      fnccod_ln,                      1,1,    0b0},
    {"lnp1",                    fnccod_lnp1,                    1,1,    0b0},
    {"log10",                   fnccod_log10,                   1,1,    0b0},
    {"exp",                     fnccod_exp,                     1,1,    0b0},
    {"expm1",                   fnccod_expm1,                   1,1,    0b0},

    {"round",                   fnccod_round,                   1,1,    0b0},
    {"ceil",                    fnccod_ceil,                    1,1,    0b0},
    {"floor",                   fnccod_floor,                   1,1,    0b0},
    {"trunc",                   fnccod_trunc,                   1,1,    0b0},

    {"min",                     fnccod_min,                     2,2,    0b0},
    {"max",                     fnccod_max,                     2,2,    0b0},
    {"abs",                     fnccod_abs,                     1,1,    0b0},
    {"signBit",                 fnccod_sign,                    1,1,    0b0},
    {"fmod",                    fnccod_fmod,                    2,2,    0b0},

    // lookup functions
    {"ifte",                    fnccod_ifte,                    3,15,   0b0},
    {"switch",                  fnccod_switch,                  3,15,   0b0},
    {"index",                   fnccod_index,                   3,15,   0b0},
    {"choose",                  fnccod_choose,                  3,15,   0b0},

    // conversion functions
    {"cInt",                    fnccod_cint,                    1,1,    0b0},
    {"cFloat",                  fnccod_cfloat,                  1,1,    0b0},
    {"cStr",                    fnccod_cstr,                    1,1,    0b0},

    // Arduino digital I/O, timing and other functions
    {"millis",                  fnccod_millis,                  0,0,    0b0},
    {"micros",                  fnccod_micros,                  0,0,    0b0},
    {"wait",                    fnccod_delay,                   1,1,    0b0},               // delay microseconds: doesn't make sense, because execution is not fast enough (interpreter)
    {"pinMode",                 fnccod_pinMode,                 2,2,    0b0},
    {"digitalRead",             fnccod_digitalRead,             1,1,    0b0},
    {"digitalWrite",            fnccod_digitalWrite,            2,2,    0b0},
    {"analogRead",              fnccod_analogRead,              1,1,    0b0},
    {"analogReference",         fnccod_analogReference,         1,1,    0b0},
    {"analogWrite",             fnccod_analogWrite,             2,2,    0b0},
    {"analogReadResolution",    fnccod_analogReadResolution,    1,1,    0b0},
    {"analogWriteResolution",   fnccod_analogWriteResolution,   1,1,    0b0},
    {"noTone",                  fnccod_noTone,                  1,1,    0b0},
    {"tone",                    fnccod_tone,                    2,3,    0b0},
    {"pulseIn",                 fnccod_pulseIn,                 2,3,    0b0},
    {"shiftIn",                 fnccod_shiftIn,                 3,3,    0b0},
    {"shiftOut",                fnccod_shiftOut,                4,4,    0b0},
    {"random",                  fnccod_random,                  1,2,    0b0},
    {"randomSeed",              fnccod_randomSeed,              1,1,    0b0},

    // Arduino bit and byte manipulation functions
    {"bit",                     fnccod_bit,                     1,1,    0b0},
    {"bitRead",                 fnccod_bitRead,                 2,2,    0b0},
    {"bitClear",                fnccod_bitClear,                2,2,    0b0},
    {"bitSet",                  fnccod_bitSet,                  2,2,    0b0},
    {"bitWrite",                fnccod_bitWrite,                3,3,    0b0},
    {"byteRead",                fnccod_byteRead,                2,2,    0b0},
    {"byteWrite",               fnccod_byteWrite,               3,3,    0b0},
    {"maskedWordRead",          fnccod_wordMaskedRead,          2,2,    0b0},
    {"maskedWordClear",         fnccod_wordMaskedClear,         2,2,    0b0},
    {"maskedWordSet",           fnccod_wordMaskedSet,           2,2,    0b0},
    {"maskedWordWrite",         fnccod_wordMaskedWrite,         3,3,    0b0},

    {"mem32Read",               fnccod_mem32Read,               1,1,    0b0},
    {"mem32Write",              fnccod_mem32Write,              2,2,    0b0},
    {"mem8Read",                fnccod_mem8Read,                2,2,    0b0},
    {"mem8Write",               fnccod_mem8Write,               3,3,    0b0},

    // string and 'character' functions
    {"char",                    fnccod_char,                    1,1,    0b0},
    {"len",                     fnccod_len,                     1,1,    0b0},
    {"line",                    fnccod_nl,                      0,0,    0b0},
    {"asc",                     fnccod_asc,                     1,2,    0b0},
    {"rtrim",                   fnccod_rtrim,                   1,1,    0b0},
    {"ltrim",                   fnccod_ltrim,                   1,1,    0b0},
    {"trim",                    fnccod_trim,                    1,1,    0b0},
    {"left",                    fnccod_left,                    2,2,    0b0},
    {"mid",                     fnccod_mid,                     3,3,    0b0},
    {"right",                   fnccod_right,                   2,2,    0b0},
    {"toUpper",                 fnccod_toupper,                 1,3,    0b0},
    {"toLower",                 fnccod_tolower,                 1,3,    0b0},
    {"space",                   fnccod_space,                   1,1,    0b0},
    {"repeatChar",              fnccod_repeatchar,              2,2,    0b0},
    {"replaceChar",             fnccod_replaceChar,             2,3,    0b0},
    {"findStr",                 fnccod_findsubstr,              2,3,    0b0},
    {"replaceStr",              fnccod_replacesubstr,           3,4,    0b0},
    {"strCmp",                  fnccod_strcmp,                  2,2,    0b0},
    {"strCaseCmp",              fnccod_strcasecmp,              2,2,    0b0},
    {"ascToHexStr",             fnccod_ascToHexString,          1,1,    0b0},
    {"hexStrToAsc",             fnccod_hexStringToAsc,          1,2,    0b0},
    {"quote",                   fnccod_quote,                   1,1,    0b0},

    {"isAlpha",                 fnccod_isAlpha,                 1,2,    0b0},
    {"isAlphaNumeric",          fnccod_isAlphaNumeric,          1,2,    0b0},
    {"isDigit",                 fnccod_isDigit,                 1,2,    0b0},
    {"isHexDigit",              fnccod_isHexadecimalDigit,      1,2,    0b0},
    {"isControl",               fnccod_isControl,               1,2,    0b0},
    {"isGraph",                 fnccod_isGraph,                 1,2,    0b0},
    {"isPrintable",             fnccod_isPrintable,             1,2,    0b0},
    {"isPunct",                 fnccod_isPunct,                 1,2,    0b0},
    {"isWhitespace",            fnccod_isWhitespace,            1,2,    0b0},
    {"isAscii",                 fnccod_isAscii,                 1,2,    0b0},
    {"isLowerCase",             fnccod_isLowerCase,             1,2,    0b0},
    {"isUpperCase",             fnccod_isUpperCase,             1,2,    0b0},

    // other functions
    {"eval",                    fnccod_eval,                    1,1,    0b0},
    {"ubound",                  fnccod_ubound,                  2,2,    0b00000001},        // first parameter is array (LSB)
    {"dims",                    fnccod_dims,                    1,1,    0b00000001},
    {"type",                    fnccod_valueType,               1,1,    0b0},
    {"r",                       fnccod_last,                    0,1,    0b0 },              // function: retrieve last result
    {"err",                     fnccod_getTrappedErr,           0,1,    0b0 },
    {"isColdStart",             fnccod_isColdStart,             0,0,    0b0 },
    {"sysVal",                  fnccod_sysVal,                  1,1,    0b0 },
    {"p",                       fnccod_batchFilePar,            1,1,    0b0 },              // function: retrieve batch file parameter

    // input and output functions
    {"cin",                     fnccod_cin,                    0,2,    0b0 },
    {"cinLine",                 fnccod_cinLine,                0,0,    0b0 },
    {"cinList",                 fnccod_cinParseList,           1,15,   0b0 },
    {"read",                    fnccod_read,                   1,3,    0b0 },
    {"readLine",                fnccod_readLine,               1,1,    0b0 },
    {"readList",                fnccod_parseList,              2,16,   0b0 },

    {"vreadList",               fnccod_parseListFromVar,       2,16,   0b0 },

    {"find",                    fnccod_find,                   2,2,    0b0 },
    {"findUntil",               fnccod_findUntil,              3,3,    0b0 },
    {"peek",                    fnccod_peek,                   0,1,    0b0 },
    {"available",               fnccod_available,              0,1,    0b0 },
    {"flush",                   fnccod_flush,                  1,1,    0b0 },
    {"setTimeout",              fnccod_setTimeout,             2,2,    0b0 },
    {"getTimeout",              fnccod_getTimeout,             1,1,    0b0 },
    {"availableForWrite",       fnccod_availableForWrite,      1,1,    0b0 },
    {"getWriteError",           fnccod_getWriteError,          1,1,    0b0 },
    {"clearWriteError",         fnccod_clearWriteError,        1,1,    0b0 },

    {"fmt",                     fnccod_format,                 1,6,    0b0},                // short label for 'system value'
    {"tab",                     fnccod_tab,                    0,1,    0b0},
    {"col",                     fnccod_gotoColumn,             1,1,    0b0},
    {"pos",                     fnccod_getColumnPos,           0,0,    0b0},

    // SD card only (based upon Arduino SD card library functions)
    {"open",                    fnccod_open,                   1,2,    0b0 },
    {"close",                   fnccod_close,                  1,1,    0b0 },
    {"position",                fnccod_position,               1,1,    0b0 },
    {"size",                    fnccod_size,                   1,1,    0b0 },
    {"seek",                    fnccod_seek,                   2,2,    0b0 },
    {"name",                    fnccod_name,                   1,1,    0b0 },
    {"fullName",                fnccod_fullName,               1,1,    0b0 },
    {"isDirectory",             fnccod_isDirectory,            1,1,    0b0 },
    {"rewindDirectory",         fnccod_rewindDirectory,        1,1,    0b0 },
    {"openNext",                fnccod_openNextFile,           1,2,    0b0 },
    {"exists",                  fnccod_exists,                 1,1,    0b0 },
    {"createDirectory",         fnccod_mkdir,                  1,1,    0b0 },
    {"removeDirectory",         fnccod_rmdir,                  1,1,    0b0 },
    {"remove",                  fnccod_remove,                 1,1,    0b0 },
    {"fileNum",                 fnccod_fileNumber,             1,1,    0b0 },
    {"isInUse",                 fnccod_slotHasOpenFile,        1,1,    0b0 },
    {"closeAll",                fnccod_closeAll,               0,0,    0b0 },
};


// terminal tokens 
// ---------------

// priority: bits b43210 define priority if used as prefix, infix, postfix operator, respectively (0x1 = lowest, 0x1F = highest) 
// priority 0 means operator not available for use as use as postfix, prefix, infix operator
// bit b7 defines associativity for infix operators (bit set indicates 'right-to-left').
// prefix operators: always right-to-left. postfix operators: always left-to-right
// NOTE: !!!!! table entries with names starting with same characters: shortest entries should come BEFORE longest (e.g. '!' before '!=', '&' before '&&') !!!!!
// postfix operator names can only be shared with prefix operator names

const Justina::TerminalDef Justina::_terminals[]{

    //  name                id code                 prefix prio          infix prio                 postfix prio         
    //  ----                -------                 -----------          ----------                 ------------   

    // non-operator terminals: ONE character only, character should NOT appear in operator names

    // THREE internal codes for semicolon symbol: next statements has a BP set, next statement allows a BP (but it's currently not set), next statement does not allow a BP
    {term_semicolon,        termcod_semicolon_BPset,    0x00,               0x00,                       0x00},
    {term_semicolon,        termcod_semicolon_BPallowed,0x00,               0x00,                       0x00},
    {term_semicolon,        termcod_semicolon,          0x00,               0x00,                       0x00},      // MUST directly follow two previous 'semicolon operator' entries

    {term_comma,            termcod_comma,              0x00,               0x00,                       0x00},
    {term_leftPar,          termcod_leftPar,            0x00,               0x10,                       0x00},
    {term_rightPar,         termcod_rightPar,           0x00,               0x00,                       0x00},

    // operators (0x00 -> operator not available, 0x01 -> pure or compound assignment)
    // op_long: operands must be long, a long is returned (e.g. 'bitand' operator)
    // res_long: operands can be float or long, a long is returned (e.g. 'and' operator)
    // op_RtoL: operator has right-to-left associativity
    // prefix operators: always right-to-left associativity; not added to the operator definition table below

    // assignment operator: ONE character only, character should NOT appear in any other operator name, except compound operator names (but NOT as first character)
    {term_assign,           termcod_assign,             0x00,               0x01 | op_RtoL,             0x00},

    {term_bitAnd,           termcod_bitAnd,             0x00,               0x06 | op_long,             0x00},
    {term_bitXor,           termcod_bitXor,             0x00,               0x05 | op_long,             0x00},
    {term_bitOr,            termcod_bitOr,              0x00,               0x04 | op_long,             0x00},

    {term_and,              termcod_and,                0x00,               0x03 | res_long,            0x00},
    {term_or,               termcod_or,                 0x00,               0x02 | res_long,            0x00},
    {term_not,              termcod_not,                0x0C | res_long,    0x00,                       0x00},
    {term_bitCompl,         termcod_bitCompl,           0x0C | op_long,     0x00,                       0x00},

    {term_eq,               termcod_eq,                 0x00,               0x07 | res_long,            0x00},
    {term_neq,              termcod_ne,                 0x00,               0x07 | res_long,            0x00},
    {term_lt,               termcod_lt,                 0x00,               0x08 | res_long,            0x00},
    {term_gt,               termcod_gt,                 0x00,               0x08 | res_long,            0x00},
    {term_ltoe,             termcod_ltoe,               0x00,               0x08 | res_long,            0x00},
    {term_gtoe,             termcod_gtoe,               0x00,               0x08 | res_long,            0x00},

    {term_bitShLeft,        termcod_bitShLeft,          0x00,               0x09 | op_long,             0x00},
    {term_bitShRight,       termcod_bitShRight,         0x00,               0x09 | op_long,             0x00},

    {term_plus,             termcod_plus,               0x0C,               0x0A,                       0x00},      // note: for strings, means 'concatenate'
    {term_minus,            termcod_minus,              0x0C,               0x0A,                       0x00},
    {term_mult,             termcod_mult,               0x00,               0x0B,                       0x00},
    {term_div,              termcod_div,                0x00,               0x0B,                       0x00},
    {term_mod,              termcod_mod,                0x00,               0x0B | op_long,             0x00},
    {term_pow,              termcod_pow,                0x00,               0x0D | op_RtoL,             0x00},

    {term_incr,             termcod_incr,               0x0E,               0x00,                       0x0F},
    {term_decr,             termcod_decr,               0x0E,               0x00,                       0x0F},

    {term_plusAssign,       termcod_plusAssign,         0x00,               0x01 | op_RtoL,             0x00},
    {term_minusAssign,      termcod_minusAssign,        0x00,               0x01 | op_RtoL,             0x00},
    {term_multAssign,       termcod_multAssign,         0x00,               0x01 | op_RtoL,             0x00},
    {term_divAssign,        termcod_divAssign,          0x00,               0x01 | op_RtoL,             0x00},
    {term_modAssign,        termcod_modAssign,          0x00,               0x01 | op_RtoL,             0x00},

    {term_bitAndAssign,     termcod_bitAndAssign,       0x00,               0x01 | op_RtoL | op_long,   0x00},
    {term_bitOrAssign,      termcod_bitOrAssign,        0x00,               0x01 | op_RtoL | op_long,   0x00},
    {term_bitXorAssign,     termcod_bitXorAssign,       0x00,               0x01 | op_RtoL | op_long,   0x00},

    {term_bitShLeftAssign,  termcod_bitShLeftAssign,    0x00,               0x01 | op_RtoL | op_long,   0x00},
    {term_bitShRightAssign, termcod_bitShRightAssign,   0x00,               0x01 | op_RtoL | op_long,   0x00}
};


// predefined constants
// --------------------

// these symbolic names can be used in Justina programs instead of the values themselves
// symbolic names should not contain '\' or '#' characters and should have a valid (identifier name) length

const Justina::SymbNumConsts Justina::_symbNumConsts[]{

    // symbol name          value                       symbol group code   symbol_code             value type
    //                                                  <curr. not used >         
    // -----------          -----                       -----------------   -----------             ----------

    // boolean values                                                                    
    {"FALSE",               "0",                        symb_bool,          valcod_false,           value_isLong},          // value for boolean 'false'
    {"TRUE",                "1",                        symb_bool,          valcod_true,            value_isLong},          // value for boolean 'true'

    {"OFF",                 "0",                        symb_bool,          valcod_off,             value_isLong},          // value for boolean 'false'
    {"ON",                  "1",                        symb_bool,          valcod_on,              value_isLong},          // value for boolean 'true'

    {"LOW",                 "0",                        symb_bool,          valcod_low,             value_isLong},          // standard ARduino constants for digital I/O
    {"HIGH",                "1",                        symb_bool,          valcod_high,            value_isLong},

    // math: floating point constants (with more precision than what will actually be used)
    {"e",                   "2.7182818284590452354",    symb_number,        valcod_e,               value_isFloat},         // base of natural logarithm (more digits then actually needed for float)
    {"PI",                  "3.14159265358979323846",   symb_number,        valcod_pi,              value_isFloat},         // PI (more digits then actually needed for float)
    {"HALF_PI",             "1.57079632679489661923",   symb_number,        valcod_half_pi,         value_isFloat},         // PI / 2
    {"QUART_PI",            "0.78539816339744830962",   symb_number,        valcod_quart_pi,        value_isFloat},         // PI / 4
    {"TWO_PI",              "6.2831853071795864769",    symb_number,        valcod_two_pi,          value_isFloat},         // 2 * PI 

    {"DEG_TO_RAD",          "0.01745329251994329577",   symb_number,        valcod_deg_to_rad,      value_isFloat},         // conversion factor: degrees to radians
    {"RAD_TO_DEG",          "57.2957795130823208768",   symb_number,        valcod_rad_to_deg,      value_isFloat},         // radians to degrees

    {"EOF",                 "-1",                       symb_number,        valcod_eof,             value_isLong},          // seek(n, EOF) is same as seek(n, size(n))

   // angle mode                                                                        
    {"RADIANS",             "0",                        symb_angle,         valcod_radians,         value_isLong},
    {"DEGREES",             "1",                        symb_angle,         valcod_degrees,         value_isLong},

    // data types                                                                        
    {"INTEGER",             "1",                        symb_numType,       valcod_integer,         value_isLong},          // value type of an integer value
    {"FLOAT",               "2",                        symb_numType,       valcod_float,           value_isLong},          // value type of a float value
    {"STRING",              "3",                        symb_numType,       valcod_string,          value_isLong},          // value type of a string value

    // digital I/O                                                                       
    {"INPUT",               "0x1",                      symb_digitalIO,     valcod_input,           value_isLong},          // standard ARduino constants for digital I/O
    {"OUTPUT",              "0x3",                      symb_digitalIO,     valcod_output,          value_isLong},
    {"INPUT_PULLUP",        "0x5",                      symb_digitalIO,     valcod_input_pl_up,     value_isLong},
    {"INPUT_PULLDOWN",      "0x9",                      symb_digitalIO,     valcod_input_pl_down,   value_isLong},
    {"LED_BUILTIN",         "13",                       symb_digitalIO,     valcod_led_blt_in,      value_isLong},
#if (defined ARDUINO_ARCH_ESP32)                                                         
    {"LED_RED",             "14",                       symb_digitalIO,     valcod_led_red,         value_isLong},
    {"LED_GREEN",           "15",                       symb_digitalIO,     valcod_led_green,       value_isLong},
    {"LED_BLUE",            "16",                       symb_digitalIO,     valcod_led_blue,        value_isLong},
#endif                                                                                   
    {"LSBFIRST",            "0x0",                      symb_digitalIO,     valcod_lsb_first,       value_isLong},          // standard ARduino constants for digital I/O
    {"MSBFIRST",            "0x1",                      symb_digitalIO,     valcod_msb_first,       value_isLong},

    // display mode command first argument: prompt and echo display                      
    {"NO_PROMPT",           "0",                        symb_dispMode,      valcod_no_prompt,       value_isLong},          // do not print prompt and do not echo user input
    {"PROMPT",              "1",                        symb_dispMode,      valcod_prompt,          value_isLong},          // print prompt but no not echo user input
    {"ECHO",                "2",                        symb_dispMode,      valcod_echo,            value_isLong},          // print prompt and echo user input

    // display mode command second argument: last result format                      
    {"NO_RESULTS",          "0",                        symb_dispResults,   valcod_no_results,      value_isLong},          // do not print last result
    {"RESULTS",             "1",                        symb_dispResults,   valcod_disp_results,    value_isLong},          // print last result
    {"QUOTE_RES",           "2",                        symb_dispResults,   valcod_quote_results,   value_isLong},          // print last result, quote string results 

    // info command: type of confirmation required ("request answer yes/no, ...")
    {"ENTER",               "0",                        symb_infoCmd,       valcod_enter,           value_isLong},          // confirmation required by pressing ENTER (any preceding characters are skipped)
    {"ENTER_CANCEL",        "1",                        symb_infoCmd,       valcod_enter_cancel,    value_isLong},          // idem, but if '\c' encountered in input stream the operation is canceled by user 
    {"YES_NO",              "2",                        symb_infoCmd,       valcod_yes_no,          value_isLong},          // only yes or no answer allowed, by pressing 'y' or 'n' followed by ENTER   
    {"YN_CANCEL",           "3",                        symb_infoCmd,       valcod_yn_cancel,       value_isLong},          // idem, but if '\c' encountered in input stream the operation is canceled by user 

    // input command: default allowed                                                    
    {"NO_DEFAULT",          "0",                        symb_inputCmd,      valcod_no_default,      value_isLong},          // '\d' sequences ('default') in the input stream are ignored
    {"ALLOW_DEFAULT",       "1",                        symb_inputCmd,      valcod_allow_default,   value_isLong},          // if '\d' sequence is encountered in the input stream, default value is returned

    // input and info command: flag 'user canceled' (input argument 3 / info argument 2 return value - argument must be a variable)
    {"CANCELED",            "0",                        symb_success,       valcod_canceled,        value_isLong},          // operation was canceled by user (\c sequence encountered)
    {"OK",                  "1",                        symb_success,       valcod_ok,              value_isLong},          // OK 
    {"NOK",                 "-1",                       symb_success,       valcod_nok,             value_isLong},          // NOT OK 

    // input / output streams                                                            
    {"CONSOLE",             "0",                        symb_IOstream,      valcod_console,         value_isLong},          // IO: read from / print to console
    {"IO1",                 "-1",                       symb_IOstream,      valcod_IO1,             value_isLong},          // IO: read from / print to alternative I/O port 1 (if defined)
    {"IO2",                 "-2",                       symb_IOstream,      valcod_IO2,             value_isLong},          // IO: read from / print to alternative I/O port 2 (if defined)
    {"IO3",                 "-3",                       symb_IOstream,      valcod_IO3,             value_isLong},          // IO: read from / print to alternative I/O port 3 (if defined)
    {"IO4",                 "-4",                       symb_IOstream,      valcod_IO4,             value_isLong},          // IO: read from / print to alternative I/O port 4 (if defined)
    {"FILE1",               "1",                        symb_IOstream,      valcod_file1,           value_isLong},          // IO: read from / print to open SD file 1
    {"FILE2",               "2",                        symb_IOstream,      valcod_file2,           value_isLong},          // IO: read from / print to open SD file 2 
    {"FILE3",               "3",                        symb_IOstream,      valcod_file3,           value_isLong},          // IO: read from / print to open SD file 3 
    {"FILE4",               "4",                        symb_IOstream,      valcod_file4,           value_isLong},          // IO: read from / print to open SD file 4 
    {"FILE5",               "5",                        symb_IOstream,      valcod_file5,           value_isLong},          // IO: read from / print to open SD file 5 
    {"DISCARD",             "99",                       symb_IOstream,      valcod_discard,         value_isLong},          // debug print only: discard output 

    // file access type on open: constants can be bitwise 'or'ed                     
    // READ can be combined with WRITE or APPEND; APPEND automatically includes WRITE (but only possible to append)
    {"READ",                "0x1",                      symb_access,        valcod_read,            value_isLong},          // open SD file for read access
    {"WRITE",               "0x2",                      symb_access,        valcod_write,           value_isLong},          // open SD file for write access
    {"APPEND",              "0x6",                      symb_access,        valcod_append,          value_isLong},          // open SD file for write access, writes will occur at end of file
    // NOTE: next 4 file access constants HAVE NO FUNCTION with nano ESP32 boards - they don't do anything
    {"SYNC",                "0x8",                      symb_access,        valcod_sync,            value_isLong},          // synchronous writes: send data physically to the card after each write 
    {"NEW_OK",              "0x10",                     symb_access,        valcod_new_ok,          value_isLong},          // creating new files if non-existent is allowed, open existing files
    {"NEW_ONLY",            "0x30",                     symb_access,        valcod_new_only,        value_isLong},          // create new file only - do not open an existing file
    {"TRUNC",               "0x40",                     symb_access,        valcod_trunc,           value_isLong},          // truncate file to zero bytes on open (NOT if file is opened for read access only)

    // formatting: specifiers for floating point numbers                             
    {"FIXED",               "f",                        symb_fmtSpec,       valcod_fixed,           value_isStringPointer}, // fixed point notation
    {"EXP_U",               "E",                        symb_fmtSpec,       valcod_exp_upper,       value_isStringPointer}, // scientific notation, exponent: 'E'
    {"EXP",                 "e",                        symb_fmtSpec,       valcod_exp,             value_isStringPointer}, // scientific notation, exponent: 'e' 
    {"SHORT_U",             "G",                        symb_fmtSpec,       valcod_short_upper,     value_isStringPointer}, // shortest notation possible; if exponent: 'E' 
    {"SHORT",               "g",                        symb_fmtSpec,       valcod_short,           value_isStringPointer}, // shortest notation possible; if exponent: 'e'   

    // formatting: specifiers for integers              symb_fmtSpec,                    
    {"DEC",                 "d",                        symb_fmtSpec,       valcod_dec,             value_isStringPointer}, // base 10 (decimal)
    {"HEX_U",               "X",                        symb_fmtSpec,       valcod_hex_upper,       value_isStringPointer}, // base 16 (hex), digits A..F
    {"HEX",                 "x",                        symb_fmtSpec,       valcod_hex,             value_isStringPointer}, // base 16 (hex), digits a..f

    // formatting: specifier for character strings                                       
    {"CHARS",               "s",                        symb_fmtSpec,       valcod_chars,           value_isStringPointer },// format character string   

    // formatting: flags                                                                 
    {"FMT_LEFT",            "0x01",                     symb_fmtFlag,       valcod_fmt_left,        value_isLong},          // align output left within the print field 
    {"FMT_SIGN",            "0x02",                     symb_fmtFlag,       valcod_fmt_sign,        value_isLong},          // always add a sign (- or +) preceding the value
    {"FMT_SPACE",           "0x04",                     symb_fmtFlag,       valcod_fmt_space,       value_isLong},          // precede the value with a space if no sign is written 
    {"FMT_POINT",           "0x08",                     symb_fmtFlag,       valcod_fmt_pnt,         value_isLong},          // if used with 'F', 'E', 'G' specifiers: add decimal point, even if no digits after decimal point  
    {"FMT_0X",              "0x08",                     symb_fmtFlag,       valcod_fmt_0x,          value_isLong},          // if used with hex output 'X' specifier: precede non-zero values with 0x  
    {"FMT_000",             "0x10",                     symb_fmtFlag,       valcod_fmt_000,         value_isLong},          // if used with 'F', 'E', 'G' specifiers: pad with zeros 
    {"FMT_NONE",            "0x00",                     symb_fmtFlag,       valcod_fmt_none,        value_isLong},          // no flags 

    {"BOARD_OTHER",         "0",                        symb_board,         valcod_board_other,     value_isLong },         // board architecture is undefined
    {"BOARD_SAMD",          "1",                        symb_board,         valcod_board_samd,      value_isLong },         // board architecture is SAMD (nano 33 IoT)
    {"BOARD_RP2040",        "2",                        symb_board,         valcod_board_rp2040,    value_isLong },         // board architecture is RP2040 
    {"BOARD_ESP32",         "3",                        symb_board,         valcod_board_esp32,     value_isLong },         // board architecture is ESP32 
    {"BOARD_NRF52840",      "4",                        symb_board,         valcod_board_nrf52840,  value_isLong }          // board architecture is NRF52840 (nano 33 BLE) 
};


// ---------------------------
// *   Justina constructor   *
// ---------------------------

Justina::Justina(int JustinaStartupOptions, int SDcardChipSelectPin) : _justinaStartupOptions(JustinaStartupOptions), _SDcardChipSelectPin(SDcardChipSelectPin), _externIOstreamCount(1) {
    _ppExternInputStreams = _pDefaultExternalInput;
    _ppExternOutputStreams = _pDefaultExternalOutput;

    constructorCommonPart();
}


Justina::Justina(Stream** const ppAltInputStreams, Print** const ppAltOutputStreams, int altIOstreamCount, int JustinaStartupOptions, int SDcardChipSelectPin) :
    _ppExternInputStreams(ppAltInputStreams), _ppExternOutputStreams(ppAltOutputStreams), _externIOstreamCount(altIOstreamCount),
    _justinaStartupOptions(JustinaStartupOptions), _SDcardChipSelectPin(SDcardChipSelectPin) {

    constructorCommonPart();
}


void Justina::constructorCommonPart() {

    // settings to be initialized when cold starting interpreter only
    // --------------------------------------------------------------

    // NOTE: count of objects created / deleted in constructors / destructors is not maintained

    _constructorInvoked = true;

    _housekeepingCallback = nullptr;

    _lastPrintedIsPrompt = false;

    _programMode = false;
    _currenttime = millis();
    _previousTime = _currenttime;
    _lastCallBackTime = _currenttime;

    parsingStack.setListName("parsing ");
    evalStack.setListName("eval    ");
    flowCtrlStack.setListName("flowCtrl");
    parsedStatementLineStack.setListName("cmd line");

    // create objects
    // --------------
    // current print column is maintained for each stream separately: init
    _pExternPrintColumns = new int[_externIOstreamCount];                                           // maximum 4 external streams
    for (int i = 0; i < _externIOstreamCount; i++) {
        // NOTE: will only have effect for currently established connections (e.g. TCP)
        if (_ppExternInputStreams[i] != nullptr) { _ppExternInputStreams[i]->setTimeout(DEFAULT_READ_TIMEOUT); }
        _pExternPrintColumns[i] = 0;
    }

    // create a 'breakpoints' object, containing the breakpoints table, and responsible for handling breakpoints 
    _pBreakpoints = new Breakpoints(this, (_PROGRAM_MEMORY_SIZE * BP_LINE_RANGE_PROGMEM_STOR_RATIO) / 100, MAX_BP_COUNT);


    // by default, console in/out and debug out are first element in _ppExternInputStreams[], _ppExternOutputStreams[]
    _consoleIn_sourceStreamNumber = _consoleOut_sourceStreamNumber = _debug_sourceStreamNumber = -1;
    _pConsoleIn = _ppExternInputStreams[0];
    _pConsoleOut = _pDebugOut = _ppExternOutputStreams[0];
    _pConsolePrintColumn = _pDebugPrintColumn = _pLastPrintColumn = _pExternPrintColumns;           //  point to its current print column (IO1)

    // find and store long associated with 'DISCARD' symbolic constant name
    for (int index = 0; index < _symbvalueCount; index++) {
        if (_symbNumConsts[index].symbolCode == valcod_discard) { _discardOut_streamNumber = strtol(_symbNumConsts[index].symbolValue, nullptr, 0); break; } // valueType MUST be long value_isLong
    }

    // set linked list debug printing. Pointer to debug out stream pointer: will follow if debug stream is changed
    parsingStack.setDebugOutStream(&_pDebugOut);                                                    // for debug printing within linked list object

    initInterpreterVariables(true, false);                                                          // init internal variables 
};


// --------------------------
// *   Justina destructor   *
// --------------------------

Justina::~Justina() {
    // delete all objects created on the heap, including user variables (+ FiFo stack), with watch expression and breakpoints
    resetMachine(true, true);

    // NOTE: object count of objects created / deleted in constructors / destructors is not maintained
    delete _pBreakpoints;                                                                           // not an array: use 'delete'
    delete[] _pExternPrintColumns;
};


// ----------------------------------
// *   user commands: constructor   *
// ----------------------------------

// allows to leave out usage restrictions and argument type restrictions when registering external (user) commands in a user's sketch 

// register external command, providing alias, function pointer, min. and max. allowed arguments
Justina::CppCommand::CppCommand(const char* a_cppCommandName, void (*a_func)(void** const pdata, const char* const valueType, const int argCount, int& execError), char a_minArgCount, char a_maxArgCount) :
    cppCommandName(a_cppCommandName), func(a_func), minArgCount(a_minArgCount), maxArgCount(a_maxArgCount) {
}

// register external command, providing alias, function pointer, min. and max. allowed arguments, usage restriction)
Justina::CppCommand::CppCommand(const char* a_cppCommandName, void (*a_func)(void** const pdata, const char* const valueType, const int argCount, int& execError), char a_minArgCount, char a_maxArgCount,
    char a_usageRestrictions) :
    cppCommandName(a_cppCommandName), func(a_func), minArgCount(a_minArgCount), maxArgCount(a_maxArgCount), usageRestrictions(a_usageRestrictions) {
}

// register external command, providing alias, function pointer, min. and max. allowed arguments, usage restriction, argument type restriction
Justina::CppCommand::CppCommand(const char* a_cppCommandName, void (*a_func)(void** const pdata, const char* const valueType, const int argCount, int& execError), char a_minArgCount, char a_maxArgCount,
    char a_usageRestrictions, char a_argTypeRestrictions) :
    cppCommandName(a_cppCommandName), func(a_func), minArgCount(a_minArgCount), maxArgCount(a_maxArgCount), usageRestrictions(a_usageRestrictions), argTypeRestrictions(a_argTypeRestrictions) {
}


// -------------------------
// *   interpreter start   *
// -------------------------

void Justina::begin() {
    // variables for maintaining lines allowing breakpoints
    bool parsedStatementStartsOnNewLine{ false };
    bool parsedStatementStartLinesAreAdjacent{ false };
    long statementStartsAtLine{ 0 };
    long parsedStatementAllowingBPstartsAtLine{ 0 };
    long BPstartLine{ 0 }, BPendLine{ 0 };

    static long BPpreviousEndLine{ 0 };

    _appFlags = 0x0000L;                                                                    // init application flags (for communication with Justina caller, using callbacks)
    printlnTo(0);
    for (int i = 0; i < 13; i++) { printTo(0, "*"); } printTo(0, "____");
    for (int i = 0; i < 4; i++) { printTo(0, "*"); } printTo(0, "__");
    for (int i = 0; i < 14; i++) { printTo(0, "*"); } printTo(0, "_");
    for (int i = 0; i < 10; i++) { printTo(0, "*"); }printlnTo(0);

    printTo(0, "    "); printlnTo(0, J_productName);
    printTo(0, "    "); printlnTo(0, J_legalCopyright);
    printTo(0, "    Version: "); printlnTo(0, J_version);
    for (int i = 0; i < 48; i++) { printTo(0, "*"); } printlnTo(0);

#if PRINT_HEAP_OBJ_CREA_DEL
    int col{};
    _pDebugOut->println();
    _pDebugOut->print("+++++ (Justina object) at 0x"); col = 10 - _pDebugOut->print((uint32_t)this, HEX); _pDebugOut->print(", size "); _pDebugOut->println(sizeof(*this));
    _pDebugOut->print("+++++ (program memory) at 0x"); col = 10 - _pDebugOut->print((uint32_t)_programStorage, HEX); _pDebugOut->print(", size "); _pDebugOut->println(_PROGRAM_MEMORY_SIZE + IMM_MEM_SIZE);
#endif

    // find token index for terminal tokens 'termcod_semicolon_BPset' and  'semicolon with breakpoint allowed' 
    int index{}, semicolonBPallowed_index{}, semicolonBPset_index{}, matches{};

    for (index = _termTokenCount - 1, matches = 0; index >= 0; index--) {                   // for all defined terminals
        if (_terminals[index].terminalCode == termcod_semicolon_BPallowed) { semicolonBPallowed_index = index; matches++; }
        if (_terminals[index].terminalCode == termcod_semicolon_BPset) { semicolonBPset_index = index; matches++; }
        if (matches == 2) { break; }                                                        // both entries found
    }
    _semicolonBPallowed_token = (semicolonBPallowed_index <= 0x0F) ? tok_isTerminalGroup1 : (semicolonBPallowed_index <= 0x1F) ? tok_isTerminalGroup2 : tok_isTerminalGroup3;
    _semicolonBPallowed_token |= ((semicolonBPallowed_index & 0x0F) << 4);
    _semicolonBPset_token = (semicolonBPset_index <= 0x0F) ? tok_isTerminalGroup1 : (semicolonBPset_index <= 0x1F) ? tok_isTerminalGroup2 : tok_isTerminalGroup3;
    _semicolonBPset_token |= ((semicolonBPset_index & 0x0F) << 4);

    _programMode = false;
    _programCounter = _programStorage + _PROGRAM_MEMORY_SIZE;
    *(_programStorage + _PROGRAM_MEMORY_SIZE) = tok_no_token;                               //  current end of program (FIRST byte of immediate mode command line)
    _lastPrintedIsPrompt = false;

    _coldStart = _constructorInvoked;
    _constructorInvoked = false;                                                            // reset

    int streamNumber{ 0 };
    setActiveStreamTo(0);                                                                   // perform checks and set input stream (to console)

    bool kill{ false };

    resetMachine(false);                                                                    // if 'warm' start, previous program (with its variables) may still exist

    bool doAutoStart{ false };

   // initialize SD card library now ?
    // 0 = no card reader, 1 = card reader present, do not yet initialize, 2 = initialize card now, 3 = init card & run startup file function start() now
    if ((_justinaStartupOptions & SD_mask) >= SD_init) {
        printlnTo(0, "\r\nLooking for an SD card...");
        execResult_type execResult = startSD();
        printlnTo(0, _SDinitOK ? "SD card found" : "SD card error: SD card NOT found");
    }

    if ((_justinaStartupOptions & SD_mask) == SD_runAutoStart) {
        // open startup file and retrieve file number (which would be one, normally)
        doAutoStart = _SDinitOK;
        if (doAutoStart) {
            printTo(0, "Looking for Justina batch file \""); printTo(0, AUTOSTART_FILE_PATH); printlnTo(0, "\"");

            if (SD.exists(AUTOSTART_FILE_PATH)) {
                printTo(0, "Executing Justina batch file \""); printTo(0, AUTOSTART_FILE_PATH); printlnTo(0, "\"...");

                // look up the 'exec' command keyword and fill source statement input buffer with 'exec batchFileName' statement
                int index{ -1 }; do {} while (_internCommands[++index].commandCode != cmdcod_execBatchFile);
                sprintf(_sourceStatement, "%s \"%s\";\0", _internCommands[index]._commandName, AUTOSTART_FILE_PATH);
                _silent = true;
            }
            else { doAutoStart = false; printTo(0, "Justina batch file \""); printTo(0, AUTOSTART_FILE_PATH); printlnTo(0, "\" not found"); }
        }
    }

    if (!doAutoStart) {
        printTo(0, "Justina> ");                                                                    // and stay on that line
        _lastPrintedIsPrompt = true;                                                                // signal that a prompt is printed now
        *_pConsolePrintColumn = 9;                                                                  // prompt character length 
    }

    JustinaMainLoop(doAutoStart, parsedStatementStartsOnNewLine, parsedStatementStartLinesAreAdjacent, statementStartsAtLine, parsedStatementAllowingBPstartsAtLine, BPstartLine, BPendLine, BPpreviousEndLine, kill);


    // returning control to Justina caller
    _appFlags = 0x0000L;                                                                            // clear all application flags
    if (_housekeepingCallback != nullptr) { _housekeepingCallback(_appFlags); }                     // pass application flags to caller immediately

    if (kill) { printlnTo(0, "\r\n\r\nProcessing kill request from calling program"); }

    SD_closeAllFiles(true);                                                                         // safety (in case an SD card is present): close all files (including system files) 
    _SDinitOK = false;
    SD.end();                                                                                       // stop SD card

    // !!! if code is ever changed to clear memory when quitting: include next line
    /* resetMachine(true, true); */

    while (_pConsoleIn->available() > 0) { readFrom(0); }                                           //  empty console buffer before quitting
    printlnTo(0, "\r\nJustina: bye\r\n");
    for (int i = 0; i < 48; i++) { printTo(0, "="); } printlnTo(0, "\r\n");

    // If data is NOT kept in memory, objects that will be deleted are: variable and function names; parsed, intermediate and variable string objects,...
    // ...array objects, stack entries, last values FiFo, open function data,  and watch strings, ...
    // Objects that are not deleted now, will be deleted when the Justina object is deleted (destructor).  
    // (program and variable memory itself is only freed when the Justina object itself is deleted).
    return;                                                                                         // return to calling program
}


// -------------------------
// *   Justina main loop   *
// -------------------------

void Justina::JustinaMainLoop(bool& doAutoStart, bool& parsedStatementStartsOnNewLine, bool& parsedStatementStartLinesAreAdjacent,
    long& statementStartsAtLine, long& parsedStatementAllowingBPstartsAtLine, long& BPstartLine, long& BPendLine, long& BPpreviousEndLine, bool& kill) {

    static bool flushAllUntilEOF{ false };

    bool isSilentOnOffStatement{ false };
    bool withinStringEscSequence{ false }, lastCharWasSemiColon{ false }, within1LineComment{ false }, withinString{ false }, redundantSemiColon = false;
    int clearCmdIndicator{ 0 };
    long lineCount{ -1 }, statementCharCount{ 0 }, parsedStatementCount{ 0 };                       // 1 = clear program cmd, 2 = clear all cmd
    char c{};
    char* pErrorPos{};
    parsingResult_type result{ result_parsing_OK };                                                 // init

    do {
        // when loading a program, as soon as first printable character of a PROGRAM is read, each subsequent character needs to follow after the previous one within a fixed time delay, 
        // this is handled by getCharacter(). Program reading ends when no character is read within this time window.
        // when processing immediate mode statements (single line), reading ends when a New Line terminating character is received
        bool programCharsReceived = _programMode && !_initiateProgramLoad;                          // _initiateProgramLoad is set during execution of the command to read a program source file from the console
        bool waitForFirstProgramCharacter = _initiateProgramLoad && (_loadProgFromStreamNo <= 0);

        // get a character if available and perform a regular housekeeping callback as well
        bool quitNow{ false }, forcedAbort{ false }, stdConsole{ false };      // kill is true: request from caller, kill is false: quit command executed
        bool bufferOverrun{ false };                                                                // buffer where statement characters are assembled for parsing
        bool noCharAdded{ false };
        bool allCharsReceived{ false };

        long currentSourceLine{ 0 };

        _initiateProgramLoad = false;

        if (doAutoStart) {
            statementCharCount = strlen(_sourceStatement);
            allCharsReceived = true;                                                                 // ready for parsing
            doAutoStart = false;                                                                     // nothing to prepare any more
        }

        else {     // note: while waiting for first program character, allow a longer time out              
            bool charFetched{ false };
            c = getCharacter(charFetched, kill, forcedAbort, stdConsole, true, waitForFirstProgramCharacter);    // forced stop has no effect here
            forcedAbort = forcedAbort && (_openDebugLevels > 0);                                  // anything to abort ? if no, reset flag

            if (charFetched) {
                if (!_silent && waitForFirstProgramCharacter) { printlnTo(0, "Receiving and parsing program... please wait"); }
                _appFlags &= ~appFlag_errorConditionBit;                                            // clear error condition flag 
                _appFlags = (_appFlags & ~appFlag_statusMask) | appFlag_parsing;                    // status 'parsing'
            }

            if (kill) { break; }
            // start processing input buffer when (1) in program mode: time out occurs and at least one character received, or (2) in immediate mode: when a new line character is detected
            allCharsReceived = _programMode ? (!charFetched && programCharsReceived) : (c == '\n'); // programCharsReceived: at least one program character received
            if (!charFetched && !allCharsReceived && !forcedAbort && !stdConsole) { continue; }     // no character: keep waiting for input (except when program or imm. mode line is read)

            // if no character added: nothing to do, wait for next
            noCharAdded = !addCharacterToInput(lastCharWasSemiColon, withinString, withinStringEscSequence, within1LineComment, _withinMultiLineComment, redundantSemiColon, allCharsReceived,
                bufferOverrun, flushAllUntilEOF, lineCount, statementCharCount, c);
            currentSourceLine = lineCount + 1;      // adjustment only
        }

        do {        // one loop only
            if (bufferOverrun) { result = result_statementTooLong; }
            if (kill) { quitNow = true;  result = result_parse_kill; break; }
            if (forcedAbort) { result = result_parse_abort; }
            if (stdConsole && !_programMode) { result = result_parse_setStdConsole; }


            // if a statement is complete (terminated by a semicolon or end of input), maintain breakpoint line ranges and parse statement
            // ---------------------------------------------------------------------------------------------------------------------------
            bool isStatementSeparator = (!withinString) && (!within1LineComment) && (!_withinMultiLineComment) && (c == term_semicolon[0]) && !redundantSemiColon;
            isStatementSeparator = isStatementSeparator || (withinString && (c == '\n'));  // a new line character within a string is sent to parser as well

            bool statementReadyForParsing = !bufferOverrun && !forcedAbort && !stdConsole && !kill && (isStatementSeparator || (allCharsReceived && (statementCharCount > 0)));
            char* pNextStatement{};

            if (_programMode && (statementCharCount == 1) && !noCharAdded) { statementStartsAtLine = currentSourceLine; }      // first character of new statement

            bool isBatchFile = !_programMode && (_activeFunctionData.statementInputStream > 0);
            if (statementReadyForParsing) {                                                         // if quitting anyway, just skip                                               
                _sourceStatement[statementCharCount] = '\0';                                        // add string terminator

                char* pStatement = _sourceStatement;                                                // because passed by reference 
                _parsingExecutingWatchString = false;                                               // init
                _parsingExecutingConditionString = false;
                _parsingEvalString = false;

                // The user can set breakpoints for source lines having at least one statement starting on that line (given that the statement is not 'parsing only').
                // Procedure 'collectSourceLineRangePairs' stores necessary data to enable this functionality.
                result = _pBreakpoints->collectSourceLineRangePairs(_semicolonBPallowed_token, parsedStatementStartsOnNewLine, parsedStatementStartLinesAreAdjacent,
                    statementStartsAtLine, parsedStatementAllowingBPstartsAtLine, BPstartLine, BPendLine, BPpreviousEndLine);

                // if no error, parse ONE statement
                if (result == result_parsing_OK) { result = parseStatement(pStatement, pNextStatement, clearCmdIndicator, isSilentOnOffStatement); }

                if (!_silent && ((++parsedStatementCount & 0x0f) == 0)) {
                    printTo(0, '.');                                                                // print a dot each 64 parsed lines
                    if ((parsedStatementCount & 0x0fff) == 0) { printlnTo(0); }                     // print a crlf each 64 dots
                }
                pErrorPos = pStatement;                                                             // in case of error

                if (result != result_parsing_OK) { flushAllUntilEOF = true; }

                // reset after each statement read 
                statementCharCount = 0;
                withinString = false; withinStringEscSequence = false; within1LineComment = false;

                // as long as statement input is from a batch file, do not reset _withinMultiLineComment after a statement is parsed 
                if (!isBatchFile) { _withinMultiLineComment = false; }

                lastCharWasSemiColon = false;
                parsedStatementStartsOnNewLine = false;                                             // reset flag (prepare for next statement)
            }

            // last 'gap' source line range and 'adjacent' source line "start of statement" range of source file
            if (_programMode && allCharsReceived && (result == result_parsing_OK)) {
                result = _pBreakpoints->addOneSourceLineRangePair(BPstartLine - BPpreviousEndLine - 1, BPendLine - BPstartLine + 1);
            }

            // program mode: complete program now read and parsed   /  imm. mode: all statements in command line read and parsed OR parsing error ?
            if (allCharsReceived || (result != result_parsing_OK)) {                                // note: if all statements have been read, they also have been parsed
                if (kill) { quitNow = true; }
                else {
                    quitNow = finaliseParsing(result, kill, lineCount, pErrorPos, allCharsReceived, isSilentOnOffStatement);     // return value: quit Justina now

                    // if not in program mode and no parsing error: execute
                    execResult_type execResult{ result_exec_OK };
                    if (!_programMode && (result == result_parsing_OK)) {

                        // revoke app flag stop request if it was stored while idle
                        if (!isBatchFile) { _appFlagStopRequestIsStored = false; }
                        execResult = exec(_programStorage + _PROGRAM_MEMORY_SIZE);                  // execute parsed user statements (and call programs from there)
                        if (execResult == EVENT_kill) { kill = true; }
                        if (kill || (execResult == EVENT_quit)) { printlnTo(0); quitNow = true; }   // make sure Justina prompt will be printed on a new line
                    }

                    quitNow = quitNow || prepareForIdleMode(result, execResult, kill, clearCmdIndicator, isSilentOnOffStatement);       // return value: quit Justina now
                }

                if (result != result_parsing_OK) {
                    statementCharCount = 0;
                    withinString = false; withinStringEscSequence = false; within1LineComment = false; _withinMultiLineComment = false;
                    lastCharWasSemiColon = false;
                }

                // reset after program (or imm. mode line) is read and processed
                lineCount = -1;                                                                     // flag: reset 'addCharacterToInput' static variables
                parsedStatementCount = 0;
                flushAllUntilEOF = false;
                _sourceStatement[statementCharCount] = '\0';                                        // add string terminator

                parsedStatementStartsOnNewLine = false;
                parsedStatementStartLinesAreAdjacent = false;
                statementStartsAtLine = 0;
                parsedStatementAllowingBPstartsAtLine = 0;
                BPstartLine = 0;
                BPendLine = 0;
                BPpreviousEndLine = 0;

                clearCmdIndicator = 0;          // reset
                result = result_parsing_OK;
            }
        } while (false);

        if (quitNow) { break; }                                                                     // user gave quit command

    } while (true);


}


// ----------------------------------------------------------------------------------
// *   add a character received from the input stream to the parsing input buffer   *
// ----------------------------------------------------------------------------------

bool Justina::addCharacterToInput(bool& lastCharWasSemiColon, bool& withinString, bool& withinStringEscSequence, bool& within1LineComment, bool& withinMultiLineComment,
    bool& redundantSemiColon, bool ImmModeLineOrProgramRead, bool& bufferOverrun, bool flushAllUntilEOF, long& lineCount, long& statementCharCount, char c) {

    const char commentOuterDelim = '/'; // twice: single line comment; followed by inner delimiter: start of multi-line comment; preceded by inner delimiter: end of multi-line comment 
    const char commentInnerDelim = '*';

    bool redundantSpaces = false;

    static bool lastCharWasWhiteSpace{ false };
    static char lastCommentChar{ '\0' };

    // init static vars at first character to be evaluated (flag: lineCount == -1)
    if (lineCount == -1) { lineCount++; lastCharWasWhiteSpace = false; lastCommentChar = '\0'; }

    bufferOverrun = false;
    if (c == '\t') { c = ' '; };                                                                    // replace TAB characters by space characters
    if ((c < ' ') && (c != '\n')) { return false; }                                                 // skip all other control-chars except new line and EOF character

    // when an imm. mode line or program is completely read and the last character (part of the last statement) received from input stream is not a semicolon, add it
    if (ImmModeLineOrProgramRead) {
        if (statementCharCount > 0) {
            if (_sourceStatement[statementCharCount - 1] != term_semicolon[0]) {
                if (statementCharCount == MAX_STATEMENT_LEN) { bufferOverrun = true; return false; }
                _sourceStatement[statementCharCount] = term_semicolon[0];                           // still room: add character
                statementCharCount++;
            }
        }

        within1LineComment = false;

        bool isBatchFile = !_programMode && (_activeFunctionData.statementInputStream > 0);
        if (!isBatchFile) { withinMultiLineComment = false; }
    }

    // not at end of program or imm. mode line: process character   
    else {
        if (flushAllUntilEOF) { return false; }                                                     // discard characters (after parsing error)

        if (c == '\n') { lineCount++; }                                                             // line number used when while reading program in input file

        // currently within a string or within a comment ? check for ending delimiter, check for in-string backslash sequences
        if (withinString) {
            if (c == '\\') { withinStringEscSequence = !withinStringEscSequence; }
            else if (c == '\"') { withinString = withinStringEscSequence; withinStringEscSequence = false; }
            else { withinStringEscSequence = false; }                                               // any other character within string
            lastCharWasWhiteSpace = false;
            lastCharWasSemiColon = false;
        }

        // within a single-line comment ? check for end of comment 
        else if (within1LineComment) {
            if (c == '\n') { within1LineComment = false; return false; }                            // comment stops at end of line
        }

        // within a multi-line comment ? check for end of comment 
        else if (withinMultiLineComment) {
            if ((c == commentOuterDelim) && (lastCommentChar == commentInnerDelim)) { withinMultiLineComment = false; return false; }
            lastCommentChar = c;                // a discarded character within a comment
        }

        // NOT within a string or (single-or multi-) line comment ?
        else {
            bool leadingWhiteSpace = (((c == ' ') || (c == '\n')) && (statementCharCount == 0));
            if (leadingWhiteSpace) { return false; };

            // start of string ?
            if (c == '\"') { withinString = true; }

            // start of (single-or multi-) line comment ?
            else if ((c == commentOuterDelim) || (c == commentInnerDelim)) {  // if previous character = same, then remove it from input buffer. It's the start of a single line comment
                if (statementCharCount > 0) {
                    if (_sourceStatement[statementCharCount - 1] == commentOuterDelim) {
                        lastCommentChar = '\0';         // reset
                        --statementCharCount;
                        _sourceStatement[statementCharCount] = '\0';                                // add string terminator

                        ((c == commentOuterDelim) ? within1LineComment : withinMultiLineComment) = true; return false;
                    }
                }
            }

            // white space in multi-line statements: replace a new line with a space (program only)
            else if (c == '\n') { c = ' '; }

            // check last character 
            redundantSpaces = (statementCharCount > 0) && (c == ' ') && lastCharWasWhiteSpace;
            redundantSemiColon = (c == term_semicolon[0]) && lastCharWasSemiColon;
            lastCharWasWhiteSpace = (c == ' ');                     // remember
            lastCharWasSemiColon = (c == term_semicolon[0]);
        }

        // do NOT add character to parsing input buffer if specific conditions are met
        if (redundantSpaces || redundantSemiColon || within1LineComment || withinMultiLineComment) { return false; }    // no character added
        if (statementCharCount == MAX_STATEMENT_LEN) { bufferOverrun = true; return false; }

        // add character  
        _sourceStatement[statementCharCount] = c;                                                   // still room: add character
        ++statementCharCount;
    }

    return true;
}


// ------------------------
// *   finalize parsing   *  
// ------------------------

bool Justina::finaliseParsing(parsingResult_type& result, bool& kill, long lineCount, char* pErrorPos, bool allCharsReceived, bool isSilentOnOffStatement) {

    bool quitJustina{ false };
    int funcNotDefIndex;
    if (result == result_parsing_OK) {
        // checks at the end of parsing: any undefined functions (program mode only) ?  any open blocks ?
        if (_programMode && (!checkAllJustinaFunctionsDefined(funcNotDefIndex))) { result = result_function_undefinedFunctionOrArray; }
        if (_blockLevel > 0) { result = result_block_noBlockEnd; }
    }

    (_programMode ? _lastProgramStep : _lastUserCmdLineStep) = _programCounter;

    if (result == result_parsing_OK) {
        if (_programMode) {
            // parsing OK message (program mode only - no message if not in program mode)  
            if (!_silent) { printParsingResult(result, funcNotDefIndex, _sourceStatement, lineCount, pErrorPos); }

            if (*_programStorage != '\0') {                      // a program was loaded: try to activate breakpoints (if defined)
                execResult_type execError = _pBreakpoints->tryBPactivation();
                if (_pBreakpoints->_breakpointsUsed > 0) {
                    printlnTo(0, (_pBreakpoints->_breakpointsStatusDraft) ?        // even if _silent mode
                        "WARNING: defined breakpoints could not be activated; breakpoint status remains draft\r\n" : "Breakpoints are now active\r\n");
                }
            }
        }

        // not in program mode; no parsing errors and not silent
        else {
            if (!_silent && !isSilentOnOffStatement) {
                // echo enabled ? pretty print input line (echo input), then advance a line if input did not come from batch file OR the last thing printed was not a prompt 
                if (_promptAndEcho == 2) { prettyPrintStatements(0, 0); }
                if (_promptAndEcho >= 1) {
                    if (!_lastPrintedIsPrompt) {
                        printlnTo(0); *_pConsolePrintColumn = 0;
                    }
                }
            }
        }
    }
    else {                                                                                          // parsing error, abort or kill during parsing
        if (_programMode && (_loadProgFromStreamNo <= 0)) {
            if (result == result_parse_abort) { printTo(0, "\r\nAbort: "); }                        // not for other parsing errors
            else { printTo(0, "\r\nParsing error: "); }
            printlnTo(0, "processing remainder of input file... please wait");
        }

        if (result == result_parse_abort) {
            printlnTo(0, "\r\n+++ Abort: execution terminated +++\r\n");                            // abort: do not distinguish between paring and execution
        }
        else if (result == result_parse_setStdConsole) {
            printlnTo(0, "\r\n+++ console reset +++");
            _consoleIn_sourceStreamNumber = _consoleOut_sourceStreamNumber = -1;
            _pConsoleIn = _ppExternInputStreams[0];                                                 // set console to stream -1 (NOT debug out)
            _pConsoleOut = _ppExternOutputStreams[0];                                               // set console to stream -1 (NOT debug out)
            _pConsolePrintColumn = &_pExternPrintColumns[0];
            *_pConsolePrintColumn = 0;

        }
        else if (result == result_parse_kill) { quitJustina = true; }
        else {
            printParsingResult(result, funcNotDefIndex, _sourceStatement, lineCount, pErrorPos);    // parsing error occurred: print error message
        }
    }
    return quitJustina;
}


// ---------------------------------------------
// *   finalize execution: prepare for idle mode
// ---------------------------------------------

bool Justina::prepareForIdleMode(parsingResult_type result, execResult_type execResult, bool& kill, int& clearIndicator, bool isSilentOnOffStatement) {
    bool isResetNow{ false };

    // ---------------------------------------------------------------------
    // if in debug mode, watch expressions (if defined) and print debug info 
    // ---------------------------------------------------------------------


    if ((_openDebugLevels > 0) && (execResult != EVENT_kill) && (execResult != EVENT_quit) && (execResult != EVENT_initiateProgramLoad)) { printDebugInfo(execResult); }


    // --------------------------------------------------------------------------------------------
    // parsing error ? (program parsing error or immediate mode (command line or batch file) error)
    // --------------------------------------------------------------------------------------------
    if (result != result_parsing_OK) {

        // reset machine (but keep user variables and breakpoints) - this clears any newly created program variables 
        // This will also close all open batch files when the corresponding flow control stack levels will be deleted.
        // The program file itself will be closed a little bit further down in this procedure.
        // -----------------------------------------------------------------------------------------------------------
        if (_programMode) { resetMachine(false); isResetNow = true; }


        // parsing error in command line (or in a batch file line) - no program is running 
        // -------------------------------------------------------------------------------
        else {
            // if parsing error occurred in a batch file, this will close it as well as any calling batch files and reset input stream to console
            int deleteImmModeCmdStackLevels{ 0 };

            // close any open batch files for the current (debug or 'root') command line
            int parsedCmdLevelsToClear{ 0 };
            clearFlowCtrlStack(parsedCmdLevelsToClear, true, false, false);
            clearParsedCommandLineStack(parsedCmdLevelsToClear);

            int streamNumber = _activeFunctionData.statementInputStream;            // this is now the console input stream again
            bool stop{ false }, abort{ false };                                     // dummy, as we are entering idle mode anyway
            setActiveStreamTo(streamNumber);                                        // perform checks and set input stream to current statement input (console or batch file) 
            flushInputCharacters(abort);                                            // flush any remaining input characters (e.g. after a program load) 
        }
    }

#if PRINT_DEBUG_INFO  
    // NOTE !!! Also set PRINT_DEBUG_INFO to 1 in file Breakpoints !!!
    if (_programMode) {
        _pDebugOut->println();
        _pBreakpoints->printLineRangesToDebugOut(static_cast<Stream*>(_pDebugOut));
    }
#endif


    // --------------------------------------
    // 'clear memory' / 'clear all' command ? 
    // --------------------------------------
    // Memory will be cleared AFTER the execution phase, if no parsing or execution errors 
    bool quitJustina{ false };
    // note: clearing a program is only allowed if no stopped programs (see 'clear program' command)
    if ((result == result_parsing_OK) && (execResult == result_exec_OK) && (clearIndicator != 0)) { clearMemory(clearIndicator, kill, quitJustina); }


    // --------------------------------------------------------------------------
    // if a reset has not been performed, reset a couple of parser variables here
    // --------------------------------------------------------------------------
    if (!isResetNow) {
        parsingStack.deleteList();
        _blockLevel = 0;                                                        // current number of open block commands (during parsing) - block level + parenthesis level = parsing stack depth
        _parenthesisLevel = 0;                                                  // current number of open parentheses (during parsing)

        _justinaFunctionBlockOpen = false;
        _JustinaVoidFunctionBlockOpen = false;
    }


    // ----------------------------------------------------------------------
    // not stopping in debug mode ? delete parsed strings in imm mode command 
    // ----------------------------------------------------------------------
    // note: Identifiers must stay available.
    // -> if stopping a program for debug, do not delete parsed strings (in imm. mode command), because the parsed command line (or batch file line) defining these strings...
     // ...is now pushed on the parsed statements stack.
    if ((execResult != EVENT_stopForDebug) && (execResult != EVENT_stopForBreakpoint)) { deleteConstStringObjects(_programStorage + _PROGRAM_MEMORY_SIZE); } // always


    // ------------------------------------------------------------
    // init function flags, program counter, statement input stream 
    // ------------------------------------------------------------
    resetFunctionFlags();

    _programCounter = _programStorage + _PROGRAM_MEMORY_SIZE;                   // start of 'immediate mode' program area
    *(_programStorage + _PROGRAM_MEMORY_SIZE) = tok_no_token;                   //  current end of program (immediate mode)

    static int statementInputStreamNumber = 0;                                  // console input stream number (default)
    static Stream* pStatementInputStream = _pConsoleIn;                         // console input stream (default)


    // ---------------------------
    // INITIATING a program load ? 
    // ---------------------------
    if (execResult == EVENT_initiateProgramLoad) {
        // Before loading a program, clear memory except user variables and breakpoints; do NOT close the currently active batch file
        // note: initiating a program load is only allowed if no stopped programs (see 'load program' command)
        resetMachine(false, false, true); isResetNow = true;                    // but keep debug level batch file (if command launched from a batch file)
        _initiateProgramLoad = true;
        _programMode = true;
        _programCounter = _programStorage;

        if (_lastPrintedIsPrompt) { printlnTo(0); }                             // print new line if last printed was a prompt
        if (_loadProgFromStreamNo > 0) { if (!_silent) { printTo(0, "Loading program "); printTo(0, openFiles[_loadProgFromStreamNo - 1].filePath); printTo(0, "...\r\n"); } }
        else { printTo(0, "Waiting for program...\r\n"); }                      // even if silent, print (hint for the user)
        _lastPrintedIsPrompt = false;

        // set stream to PROGRAM input stream (is a valid stream, already checked)
        statementInputStreamNumber = _loadProgFromStreamNo;
        setActiveStreamTo(_loadProgFromStreamNo, pStatementInputStream);        // perform checks and set input stream to program input stream (external or file)

        // flush any characters currently in stream input buffer, waiting to be read (useful for remote terminals)
        if (statementInputStreamNumber <= 0) { while (pStatementInputStream->available()) { readFrom(statementInputStreamNumber); } }
    }

    // -----------------------------
    // NOT initiating a program load  
    // ----------------------------- 
    else {

        // end of a program load ? 
        // -----------------------
        if (_programMode) {                                                     // At this time, the stream is still the stream from where the program was loaded 

            _programMode = false;

            // program load (with or without success) from external IO channel ? Flush any remaining characters in the input buffer
            // note: statementInputStreamNumber (= _loadProgFromStreamNo) and pStatementInputStream are still correct, but setActiveStreamTo needed before flushing characters 
            setActiveStreamTo(_loadProgFromStreamNo, pStatementInputStream);
            if (_loadProgFromStreamNo <= 0) {
                if (pStatementInputStream->available() > 0) {                   // skip if initially buffer is empty
                    bool stop{ false }, abort{ false };                         // dummy, as we are entering idle mode anyway
                    flushInputCharacters(abort);                                // flush any remaining input characters (e.g. after a program parsing error)
                }
            }
            // program load (with or without success) from a file ? Close it now
            else { SD_closeFile(_loadProgFromStreamNo); }                       // may close program file now 

            _loadProgFromStreamNo = 0;                                          // back to the default stream (console) for program load    
        }

        // set the input stream again to console (or batch file) 
        statementInputStreamNumber = _activeFunctionData.statementInputStream;  // static !
        setActiveStreamTo(statementInputStreamNumber, pStatementInputStream, false, true);

    }


     // ------------------------------------------------
     // finalize: set application flags and print prompt
     // ------------------------------------------------
    bool isError = (result != result_parsing_OK) || ((execResult != result_exec_OK) && (execResult < EVENT_startOfEvents));
    isError ? (_appFlags |= appFlag_errorConditionBit) : (_appFlags &= ~appFlag_errorConditionBit);     // set or clear error condition flag 
    // status 'idle in debug mode' or 'idle' 
    (_appFlags &= ~appFlag_statusMask);
    (_openDebugLevels > 0) ? (_appFlags |= appFlag_stoppedInDebug) : (_appFlags |= appFlag_idle);

#if PRINT_DEBUG_INFO
    if (_lastPrintedIsPrompt) { printlnTo(0); }
    _pDebugOut->print("==== flow ctrl stack elements = "); _pDebugOut->println(flowCtrlStack.getElementCount());
    _pDebugOut->print("     parsed cmd line stack el = "); _pDebugOut->println(parsedStatementLineStack.getElementCount());
    _pDebugOut->print("     open debug levels        = "); _pDebugOut->println(_openDebugLevels);
    _pDebugOut->print("     call stack depth         = "); _pDebugOut->println(_callStackDepth);
    _pDebugOut->print("     eval stack elements      = "); _pDebugOut->println(evalStack.getElementCount());

    _pDebugOut->print("  == input stream number      = "); _pDebugOut->println(statementInputStreamNumber);
    if (statementInputStreamNumber > 0) {
        if (openFiles[statementInputStreamNumber - 1].fileNumberInUse) {
            _pDebugOut->print("         stream position      = ");  _pDebugOut->println(openFiles[statementInputStreamNumber - 1].file.position());
            _pDebugOut->print("                available     = ");  _pDebugOut->println(openFiles[statementInputStreamNumber - 1].file.available());
        }
    }
#endif

    setActiveStreamTo(0, true);

    // statement input from file: batch file input (it can not be a program here)
    int streamNumber = _activeFunctionData.statementInputStream;                                    // caller stream (batch file or command line)
    bool isBatchFile = (streamNumber > 0);

    if (!isBatchFile) { _silent = false; }                                                          // is true after autostart                    

    // if batch file, then only print prompt if not in silent mode and the last thing printed was not a prompt (this avoids printing a lot of Justina prompts followed by empty lines)
    bool promptPrinting = (_promptAndEcho != 0) && (execResult != EVENT_initiateProgramLoad);   // only print prompts if enabled, and you're not waiting for an external program file
    promptPrinting = promptPrinting && (!isBatchFile || (!_silent && !_lastPrintedIsPrompt));   // batch file: do not print prompt for every empty, comment... line 

    if (promptPrinting) {
        if (_lastPrintedIsPrompt) { printlnTo(0); }                                                 // still positioned after the previous prompt: avoid two prompts on the same line
        printTo(0, "Justina> ");                                                                    // and stay on that line
        _lastPrintedIsPrompt = true;                                                                // signal that a prompt is printed now
        *_pConsolePrintColumn = 9;                                                                  // prompt character length 
    }

    execResult = result_exec_OK;
    return quitJustina;
}

// -----------------------------------------
// *   clear program memory / all memory   *
// -----------------------------------------

// executed AFTER all statements in the command line containing the 'clearProg' or 'clearMem' statement have been EXECUTED, just before returning to idle

void Justina::clearMemory(int& clearIndicator, bool& kill, bool& quitJustina) {                     // 1 = clear program cmd, 2 = clear all cmd
    // if only clearing a program, keep batch files open (this is only allowed if no stopped programs - see 'clear program' command)
    if (_silent) { resetMachine(clearIndicator == 2, clearIndicator == 2, clearIndicator == 1); }
    else {
        while (_pConsoleIn->available() > 0) { readFrom(0); }                                       // empty console buffer first (to allow the user to start with an empty line)
        do {
            char s[60];
            sprintf(s, "===== Clear %s ? (please answer Y or N) =====", ((clearIndicator == 2) ? "memory" : "program"));
            printlnTo(0, s);

            // read characters and store in 'input' variable. Return on '\n' (length is stored in 'length').
            // return flags doAbort, doCancel, doDefault if user included corresponding escape sequences in input string.
            bool doCancel{ false }, doAbort{ false }, doDefault{ false };          // not used but mandatory
            int length{ 2 };                                                                        // detects input > 1 character
            char input[2 + 1] = "";                                                                 // init: empty string. Provide room for 1 character + terminating '\0'
            // NOTE: stop, cancel and default arguments have no function here (execution has ended already), but abort and kill do
            if (getConsoleCharacters(doAbort, doCancel, doDefault, input, length, '\n')) { kill = true; quitJustina = true; break; }  // kill request from caller ?

            if (doAbort) { break; }        // avoid a next loop (getConsoleCharacters exits immediately when abort request received, not waiting for any characters)
            bool validAnswer = (strlen(input) == 1) && ((tolower(input[0]) == 'n') || (tolower(input[0]) == 'y'));
            if (validAnswer) {
                // 1 = clear program, 2 = clear all (including user variables)
                if (tolower(input[0]) == 'y') {
                    printlnTo(0, (clearIndicator == 2) ? "clearing memory" : "clearing program");
                    // if only clearing a program, keep batch files open (this is only allowed if no stopped programs - see 'clear program' command)
                    resetMachine(clearIndicator == 2, clearIndicator == 2, clearIndicator == 1);
                }
                break;
            }
        } while (true);
    }
}


// -------------------------------------------------------------------------
// *   watch expressions as defined in watch statement, print debug info   *
// -------------------------------------------------------------------------

void Justina::printDebugInfo(execResult_type execResult) {
    // count of programs in debug:
    // - if an error occurred in a RUNNING program, the program is terminated and the number of STOPPED programs ('in debug mode') does not change.
    // - if an error occurred while executing a command line, then this count is not changed either
    // flow control stack:
    // - at this point, structure '_activeFunctionData' always contains flow control data for the main program level (command line or open batch file - in debug mode if the count of open programs is not zero)
    // - the flow control stack maintains data about open block commands, open batch file, open functions and eval() strings in execution (call stack)
    // => skip stack elements for any command line open block commands or eval() strings in execution, and fetch the data for the function where control will resume when started again

    char* nextStatementPointer = _programCounter;
    OpenFunctionData* pDeepestOpenFunction = &_activeFunctionData;

    void* pFlowCtrlStackLvl = _pFlowCtrlStackTop;
    int blockType = block_none;
    do {                                                                                            // there is at least one open function in the call stack
        // if within a batch file, skip the debug command line to reach the deepest open function
        blockType = ((OpenBlockGeneric*)pFlowCtrlStackLvl)->blockType;
        if (blockType == block_JustinaFunction) { if (((OpenFunctionData*)pFlowCtrlStackLvl)->pNextStep < (_programStorage + _PROGRAM_MEMORY_SIZE)) { break; } }
        pFlowCtrlStackLvl = flowCtrlStack.getPrevListElement(pFlowCtrlStackLvl);
    } while (true);

    pDeepestOpenFunction = (OpenFunctionData*)pFlowCtrlStackLvl;                                    // deepest level of nested functions
    nextStatementPointer = pDeepestOpenFunction->pNextStep;

    bool isBreakpointStop = (execResult == EVENT_stopForBreakpoint);

    // print debug header ('STOP'or 'BREAK') line
    // ------------------------------------------
    int stopLineLength = max(_dispWidth, 30 + (int)strlen(JustinaFunctionNames[pDeepestOpenFunction->functionIndex])) + 1;   // includes a few spare character positions      
    char msg[stopLineLength];

    int length = sprintf(msg, "%s[%s] ", (isBreakpointStop ? "\r\n-- BREAK IN " : "\r\n-- STOP in "), JustinaFunctionNames[pDeepestOpenFunction->functionIndex]);
    if (_openDebugLevels > 1) { length += sprintf(msg + length, "-- [%ld] ", _openDebugLevels); }
    int i{};
    for (i = length; i < (stopLineLength - 2 - 1); i++) { msg[i] = '-'; }
    strcpy(msg + i, "\r\n");                                                                        // this adds terminating \0 as well
    printTo(_debug_sourceStreamNumber, msg);

    // print watch and breakpoint watch string, if any
    // -----------------------------------------------
    // if this is a breakpoint stop, parse and evaluate expression (if any) stored in BP watch string
    // for all stops: parse and evaluate expression (if any) stored in overall watch string
    Breakpoints::BreakpointData* pBreakpointDataRow{ nullptr };
    int BPdataRow{};
    bool lineHasBPtableEntry = (*(nextStatementPointer - 1) == _semicolonBPset_token);              // (note that BP can be disabled, hit count not yet reached or condition result = false)
    if (lineHasBPtableEntry) {                                                                      // check attributes in breakpoints table
        pBreakpointDataRow = _pBreakpoints->findBPtableRow(nextStatementPointer, BPdataRow);        // find table entry
    }
    if (isBreakpointStop) { parseAndExecWatchOrBPwatchString(BPdataRow); }                          // BP watch string: may not contain keywords, Justina functions, generic names
    parseAndExecWatchOrBPwatchString();                                                             // watch string: may not contain keywords, Justina functions, generic names

    // print the source line, function and statement 
    // ---------------------------------------------
    // if source line has an entry in breakpoint table: retrieve source line from there. If not,...
    // ...then calculate the source line number by counting parsed statements with a preceding 'breakpoint set' or 'breakpoint allowed' token.
    // note that the second method can be slow(-er) if program consists of a large number of statements
    long sourceLine = (lineHasBPtableEntry) ? pBreakpointDataRow->sourceLine : _pBreakpoints->findLineNumberForBPstatement(nextStatementPointer);
    sprintf(msg, "next [%.4ld] ", sourceLine);                                                      // minimum 4 printed digits (including leading zeros)
    printTo(_debug_sourceStreamNumber, msg);
    prettyPrintStatements(_debug_sourceStreamNumber, 1, nextStatementPointer);                      // print statement
    printTo(_debug_sourceStreamNumber, "\r\n");

    return;
}


// -----------------------------------------------
// *   parse and exec watch string expressions   *
// -----------------------------------------------

// watch string may not contain keywords, user functions, generic names

void Justina::parseAndExecWatchOrBPwatchString(int BPindex) {
    char* pNextParseStatement{};

    char* pwatchParsingInput = ((BPindex == -1) ? _pwatchString : _pBreakpoints->_pBreakpointData[BPindex].pWatch);     // copy pointer to start of watch string
    if (pwatchParsingInput == nullptr) { return; }                                                                      // no watch string: nothing to watch

    // watch string expressions will be parsed and executed from immediate mode program storage: 
    // before overwriting user statements that were just parsed and executed, delete parsed strings
    deleteConstStringObjects(_programStorage + _PROGRAM_MEMORY_SIZE);

    bool valuePrinted{ false };

    _parsingExecutingWatchString = true;

    printTo(_debug_sourceStreamNumber, (BPindex == -1) ? "<watch> " : "<BP wa> ");

    // in each loop, parse and execute ONE expression 
    do {
        // init
        *(_programStorage + _PROGRAM_MEMORY_SIZE) = tok_no_token;                                                   // in case no valid tokens will be stored
        _programCounter = _programStorage + _PROGRAM_MEMORY_SIZE;                                                   // start of 'immediate mode' program area

        // skip any spaces and semi-colons in the input stream
        while ((pwatchParsingInput[0] == ' ') || (pwatchParsingInput[0] == term_semicolon[0])) { pwatchParsingInput++; }
        if (*pwatchParsingInput == '\0') { break; }                                                                 // could occur if semicolons skipped

        // parse multiple watch string expressions ? print a comma in between
        if (valuePrinted) { printTo(_debug_sourceStreamNumber, ", "); }                                             // separate values (if more than one)

        // note: application flags are not adapted (would not be passed to caller immediately)
        int dummyInt{}; bool dummyBool{};
        parsingResult_type result = parseStatement(pwatchParsingInput, pNextParseStatement, dummyInt, dummyBool);   // parse ONE statement
        if (result == result_parsing_OK) {
            if (!_printWatchValueOnly) {
                // do NOT pretty print if parsing error, to avoid bad-looking partially printed statements (even if there will be an execution error later)
                prettyPrintStatements(_debug_sourceStreamNumber, 0);
                printTo(_debug_sourceStreamNumber, ": ");                                                           // resulting value will follow
                pwatchParsingInput = pNextParseStatement;
            }
        }
        else {
            char  errStr[12];                                                                                       // includes place for terminating '\0'
            // if parsing error, print error instead of value AND CONTINUE with next watch expression (if any)
            sprintf(errStr, "<ErrP%d>", (int)result);
            printTo(_debug_sourceStreamNumber, errStr);
            // pNextParseStatement not yet correctly positioned: set to next statement
            while ((pwatchParsingInput[0] != term_semicolon[0]) && (pwatchParsingInput[0] != '\0')) { ++pwatchParsingInput; }
            if (pwatchParsingInput[0] == term_semicolon[0]) { ++pwatchParsingInput; }
        }

        // if parsing went OK: execute ONE parsed expression (just parsed now)
        execResult_type execResult{ result_exec_OK };
        if (result == result_parsing_OK) {
            execResult = exec(_programStorage + _PROGRAM_MEMORY_SIZE);                                              // note: value or exec. error is printed from inside exec()
        }

        valuePrinted = true;

        // execution finished: delete parsed strings in imm mode command (they are on the heap and not needed any more)
        deleteConstStringObjects(_programStorage + _PROGRAM_MEMORY_SIZE);                                           // always
        *(_programStorage + _PROGRAM_MEMORY_SIZE) = tok_no_token;                                                   // current end of program (immediate mode)

    } while (*pwatchParsingInput != '\0');                                                                          // exit loop if all expressions handled

    _parsingExecutingWatchString = false;
    printlnTo(_debug_sourceStreamNumber);       // go to next output line

    return;
}


// -------------------------------------------------------------
// *   check if all Justina functions referenced are defined   *
// -------------------------------------------------------------

bool Justina::checkAllJustinaFunctionsDefined(int& index) {
    for (index = 0; index < _justinaFunctionCount; index++) {                                                       // points to variable in use
        if (justinaFunctionData[index].pJustinaFunctionStartToken == nullptr) { return false; }
    }
    return true;
}


// ---------------------------------------------------------------------------------------------------
// *   reset flags indicating that Justina function calls are found that are part of an expression   *
// ---------------------------------------------------------------------------------------------------

// this flag is maintained in the master data for each Justina function / procedure

bool Justina::resetFunctionFlags() {
    for (int index = 0; index < _justinaFunctionCount; index++) {                                                   // points to variable in use
        justinaFunctionData[index].callsAsPartOfExpression = 0;
    }
    return true;
}


// ----------------------------------------------------
// *   set system (housekeeping) call back function   *
// ----------------------------------------------------

void Justina::setSystemCallbackFunction(void (*func)(long& appFlags)) {

    // this function is directly called from the main Arduino program, before the Justina begin() method is called
    // it stores the address of an optional 'system callback' function
    // Justina will call this user routine at specific time intervals, allowing  the user...
    // ...to execute a specific routine regularly (e.g. to maintain a TCP connection, to implement a heartbeat, ...)

    _housekeepingCallback = func;
}


// ----------------------------------
// *   set RTC call back function   *
// ----------------------------------

void Justina::setRTCCallbackFunction(void (*func)(uint16_t* date, uint16_t* time)) {

    // this function is directly called from the main Arduino program, before the Justina begin() method is called
    // it stores the address of an optional 'RTC callback' function
    // Justina can than call this function when it needs the date or time 

    _dateTime = func;
}


//----------------------------------------------
// *   execute regular housekeeping callback   *
// ---------------------------------------------

// do a housekeeping callback at regular intervals (if callback function defined), but only
// - while waiting for input
// - while executing the Justina delay() function
// - when a statement has been executed 
// the callback function relays specific flags to the caller and upon return, reads certain flags set by the caller 

void Justina::execPeriodicHousekeeping(bool* pKillNow, bool* pForcedAbort, bool* pSetStdConsole) {
    if (pKillNow != nullptr) { *pKillNow = false; }; if (pForcedAbort != nullptr) { *pForcedAbort = false; }    // init
    if (_housekeepingCallback != nullptr) {
        _currenttime = millis();
        _previousTime = _currenttime;
        // note: also handles millis() overflow after about 47 days
        if ((_lastCallBackTime + CALLBACK_INTERVAL < _currenttime) || (_currenttime < _previousTime)) {         // while executing, limit calls to housekeeping callback routine 
            _lastCallBackTime = _currenttime;
            _housekeepingCallback(_appFlags);                                                                   // execute housekeeping callback
            if ((_appFlags & appFlag_consoleRequestBit) && (pSetStdConsole != nullptr)) { *pSetStdConsole = true; }
            if ((_appFlags & appFlag_killRequestBit) && (pKillNow != nullptr)) { *pKillNow = true; }
            if (_appFlags & appFlag_stopRequestBit) { _appFlagStopRequestIsStored = true; }
            if ((_appFlags & appFlag_abortRequestBit) && (pForcedAbort != nullptr)) { *pForcedAbort = true; }

            _appFlags &= ~appFlag_dataInOut;                                                                    // reset flag
        }
    }

    yield();
}


// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------
// *   sets pointers to the locations where the Arduino program stored information about user-defined (external) cpp functions and commands (user callback functions)   *
// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------

    // the 'register...' functions are called from the main Arduino program, before the Justina begin() method is called
    // each function stores the starting address of an array with information about external (user callback) functions with a specific CPP return type 
    // for instance, _pExtCppFunctions[0] stores the address of the array containing information about cpp functions returning a boolean value
    // a null pointer indicates there are no functions of a specific type
    // cpp function return types are: 0 = bool, 1 = char, 2 = int, 3 = long, 4 = float, 5 = char*, 6 = void (but returns zero to Justina)


void Justina::registerBoolUserCppFunctions(const CppBoolFunction* const  pCppBoolFunctions, const int cppBoolFunctionCount) {
    _pExtCppFunctions[0] = (CppBoolFunction*)pCppBoolFunctions;
    _ExtCppFunctionCounts[0] = cppBoolFunctionCount;
};

void Justina::registerCharUserCppFunctions(const CppCharFunction* const  pCppCharFunctions, const int cppCharFunctionCount) {
    _pExtCppFunctions[1] = (CppCharFunction*)pCppCharFunctions;
    _ExtCppFunctionCounts[1] = cppCharFunctionCount;
};

void Justina::registerIntUserCppFunctions(const CppIntFunction* const  pCppIntFunctions, const int cppIntFunctionCount) {
    _pExtCppFunctions[2] = (CppIntFunction*)pCppIntFunctions;
    _ExtCppFunctionCounts[2] = cppIntFunctionCount;
};

void Justina::registerLongUserCppFunctions(const CppLongFunction* const pCppLongFunctions, const int cppLongFunctionCount) {
    _pExtCppFunctions[3] = (CppLongFunction*)pCppLongFunctions;
    _ExtCppFunctionCounts[3] = cppLongFunctionCount;
};

void Justina::registerFloatUserCppFunctions(const CppFloatFunction* const pCppFloatFunctions, const int cppFloatFunctionCount) {
    _pExtCppFunctions[4] = (CppFloatFunction*)pCppFloatFunctions;
    _ExtCppFunctionCounts[4] = cppFloatFunctionCount;
};

void Justina::register_pCharUserCppFunctions(const Cpp_pCharFunction* const pCpp_pCharFunctions, const int cpp_pCharFunctionCount) {
    _pExtCppFunctions[5] = (Cpp_pCharFunction*)pCpp_pCharFunctions;
    _ExtCppFunctionCounts[5] = cpp_pCharFunctionCount;
};

void Justina::registerVoidUserCppFunctions(const CppVoidFunction* const pCppVoidFunctions, const int cppVoidFunctionCount) {
    _pExtCppFunctions[6] = (CppVoidFunction*)pCppVoidFunctions;
    _ExtCppFunctionCounts[6] = cppVoidFunctionCount;
};

// user commands
void Justina::registerUserCommands(const CppCommand* const pCppCommands, const int cppCommandCount) {
    _pExternCommands = (CppCommand*)pCppCommands;
    _externCommandCount = cppCommandCount;
}


// -------------------------
// *   reset interpreter   *
// -------------------------

void Justina::resetMachine(bool withUserVariables, bool withBreakpoints, bool keepDebugLevelBatchFile) {

    // delete all objects created on the heap
    // --------------------------------------

    // note: objects living during execution only, do not need to be deleted: they are all always deleted when the execution phase ends (even if with execution errors)
    // more in particular: evaluation stack, intermediate alphanumeric constants, local storage areas, local variable strings, local array objects

    // delete identifier name objects on the heap (variable names, Justina function names) 
    deleteIdentifierNameObjects(programVarNames, _programVarNameCount);
    deleteIdentifierNameObjects(JustinaFunctionNames, _justinaFunctionCount);
    if (withUserVariables) { deleteIdentifierNameObjects(userVarNames, _userVarCount, true); }

    // delete variable heap objects: array variable element string objects
    deleteStringArrayVarsStringObjects(globalVarValues, globalVarType, _programVarNameCount, 0, true);
    deleteStringArrayVarsStringObjects(staticVarValues, staticVarType, _staticVarCount, 0, false);
    if (withUserVariables) {
        deleteStringArrayVarsStringObjects(userVarValues, userVarType, _userVarCount, 0, false, true);
        deleteLastValueFiFoStringObjects();
    }

    // delete variable heap objects: scalar variable strings and array variable array storage 
    deleteVariableValueObjects(globalVarValues, globalVarType, _programVarNameCount, 0, true);
    deleteVariableValueObjects(staticVarValues, staticVarType, _staticVarCount, 0, false);
    if (withUserVariables) {
        deleteVariableValueObjects(userVarValues, userVarType, _userVarCount, 0, false, true);
    }

    // delete all elements of the flow control stack 
    // in the process, delete all local variable areas referenced in elements of the flow control stack referring to functions, including local variable string and array values
    // if 'keepDebugLevelBatchFile' is false, then an active batch file and any calling batch files will be closed as well
    int deleteImmModeCmdStackLevels{};
    clearFlowCtrlStack(deleteImmModeCmdStackLevels, keepDebugLevelBatchFile, false, keepDebugLevelBatchFile);

    // delete elements of the immediate mode parsed statements stack 
    // (parsed immediate mode statements can be temporarily pushed on the immediate mode stack to be replaced either by parsed debug command lines or parsed eval() strings) 
    // also delete all parsed alphanumeric constants: (1) in the currently parsed program, (2) in parsed immediate mode statements (including those on the imm.mode parsed statements stack)) 

    clearParsedCommandLineStack(deleteImmModeCmdStackLevels);                                                   // including parsed string constants
    deleteConstStringObjects(_programStorage);
    deleteConstStringObjects(_programStorage + _PROGRAM_MEMORY_SIZE);

    // clear expression evaluation stack
    clearEvalStack();

    // delete parsing stack (keeps track of open parentheses and open command blocks during parsing)
    parsingStack.deleteList();

    // delete watch string and delete breakpoint watch and condition strings ?
    if (withBreakpoints) {
        if (_pwatchString != nullptr) {        // internal watch 'variable'
        #if PRINT_HEAP_OBJ_CREA_DEL
            _pDebugOut->print("\r\n----- (system exp str) "); _pDebugOut->println((uint32_t)_pwatchString, HEX);
            _pDebugOut->print("reset mach.: watch str "); _pDebugOut->println(_pwatchString);
        #endif
            _systemStringObjectCount--;
            delete[] _pwatchString;
            _pwatchString = nullptr;                                                                            // old watch string
        }

        _pBreakpoints->resetBreakpointsState();                                                                 // '_breakpointsStatusDraft' is set false (breakpoint table empty) 
    }

    // if machine reset without clearing breakpoints, set breakpoints status to DRAFT then 
    else {
        bool wasDraft = _pBreakpoints->_breakpointsStatusDraft;
        _pBreakpoints->_breakpointsStatusDraft = (_pBreakpoints->_breakpointsUsed > 0);                         // '_breakpointsStatusDraft' set according to existence of entries in breakpoint table
        _pBreakpoints->_BPlineRangeStorageUsed = 0;
    }

    // check that all heap objects are deleted (in fact only the count is checked)
    // ---------------------------------------------------------------------------
    // With open batch files OR when stopped programs exist, the flow control stack and associated objects (batch file names and parameters)... 
    //       ...are NOT completely cleared: do not check dangling pointers then
    if ((_activeFunctionData.statementInputStream == 0) && (_openDebugLevels == 0)) { danglingPointerCheckAndCount(withUserVariables); }        // check and count


    // initialize interpreter object variables
    // ---------------------------------------
    initInterpreterVariables(withUserVariables, keepDebugLevelBatchFile);


    if (!_silent) { printlnTo(0); }
}


// ---------------------------------------------------------------------------------------
// *   perform consistency checks: verify that all objects created are destroyed again   *
// ---------------------------------------------------------------------------------------

void Justina::danglingPointerCheckAndCount(bool withUserVariables) {

    // note: the evaluation stack, intermediate string objects, function local storage, and function local variable strings and arrays exist solely during execution
    //       relevant checks are performed each time execution terminates 

    // parsing stack: no need to check if any elements were left (the list has just been deleted)
    // note: this stack does not contain any pointers to heap objects

    // string and array heap objects: any objects left ?
    if (_identifierNameStringObjectCount != 0) {
    #if PRINT_OBJECT_COUNT_ERRORS
        _pDebugOut->print("**** Variable / function name objects cleanup error. Remaining: "); _pDebugOut->println(_identifierNameStringObjectCount);
    #endif
        _identifierNameStringObjectErrors += abs(_identifierNameStringObjectCount);
    }

    if (_parsedStringConstObjectCount != 0) {
    #if PRINT_OBJECT_COUNT_ERRORS
        _pDebugOut->print("**** Parsed constant string objects cleanup error. Remaining: "); _pDebugOut->println(_parsedStringConstObjectCount);
    #endif
        _parsedStringConstObjectErrors += abs(_parsedStringConstObjectCount);
    }

    if (_globalStaticVarStringObjectCount != 0) {
    #if PRINT_OBJECT_COUNT_ERRORS
        _pDebugOut->print("**** Variable string objects cleanup error. Remaining: "); _pDebugOut->println(_globalStaticVarStringObjectCount);
    #endif
        _globalStaticVarStringObjectErrors += abs(_globalStaticVarStringObjectCount);
    }

    if (_globalStaticArrayObjectCount != 0) {
    #if PRINT_OBJECT_COUNT_ERRORS
        _pDebugOut->print("**** Array objects cleanup error. Remaining: "); _pDebugOut->println(_globalStaticArrayObjectCount);
    #endif
        _globalStaticArrayObjectErrors += abs(_globalStaticArrayObjectCount);
    }

#if PRINT_DEBUG_INFO
    _pDebugOut->print("\r\n   Reset stats\r\n   parsed strings "); _pDebugOut->print(_parsedStringConstObjectCount);
    _pDebugOut->print(", prog name strings "); _pDebugOut->print(_identifierNameStringObjectCount);
    _pDebugOut->print(", prog var strings "); _pDebugOut->print(_globalStaticVarStringObjectCount);
    _pDebugOut->print(", prog arrays "); _pDebugOut->print(_globalStaticArrayObjectCount);
#endif

    if (withUserVariables) {
        if (_userVarNameStringObjectCount != 0) {
        #if PRINT_OBJECT_COUNT_ERRORS
            _pDebugOut->print("**** User variable name objects cleanup error. Remaining: "); _pDebugOut->println(_userVarNameStringObjectCount);
        #endif
            _userVarNameStringObjectErrors += abs(_userVarNameStringObjectCount);
        }

        if (_userVarStringObjectCount != 0) {
        #if PRINT_OBJECT_COUNT_ERRORS
            _pDebugOut->print("**** User variable string objects cleanup error. Remaining: "); _pDebugOut->println(_userVarStringObjectCount);
        #endif
            _userVarStringObjectErrors += abs(_userVarStringObjectCount);
        }

        if (_userArrayObjectCount != 0) {
        #if PRINT_OBJECT_COUNT_ERRORS
            _pDebugOut->print("**** User array objects cleanup error. Remaining: "); _pDebugOut->println(_userArrayObjectCount);
        #endif
            _userArrayObjectErrors += abs(_userArrayObjectCount);
        }

        if (_lastValuesStringObjectCount != 0) {
        #if PRINT_OBJECT_COUNT_ERRORS
            _pDebugOut->print("**** Last value FiFo string objects cleanup error. Remaining: "); _pDebugOut->print(_lastValuesStringObjectCount);
        #endif
            _lastValuesStringObjectErrors += abs(_lastValuesStringObjectCount);
        }

        if (_systemStringObjectCount != 0) {
        #if PRINT_OBJECT_COUNT_ERRORS
            _pDebugOut->print("**** System variable string objects cleanup error. Remaining: "); _pDebugOut->println(_systemStringObjectCount);
        #endif
            _systemStringObjectErrors += abs(_systemStringObjectCount);
        }


    #if PRINT_DEBUG_INFO
        _pDebugOut->print(", user var names "); _pDebugOut->print(_userVarNameStringObjectCount);
        _pDebugOut->print(", user var strings "); _pDebugOut->print(_userVarStringObjectCount);
        _pDebugOut->print(", user arrays "); _pDebugOut->print(_userArrayObjectCount);

        _pDebugOut->print(", last value strings "); _pDebugOut->print(_lastValuesStringObjectCount);
    #endif
    }
}


// --------------------------------------------
// *   initialize interpreter object fields   *
// --------------------------------------------

void Justina::initInterpreterVariables(bool fullReset, bool keepDebugLevelBatchFile) {

    // initialized at cold start AND each time the interpreter is reset

    _blockLevel = 0;    // current number of open block commands (during parsing) - block level + parenthesis level = parsing stack depth
    _justinaFunctionCount = 0;
    _paramOnlyCountInFunction = 0;
    _localVarCountInFunction = 0;
    _localVarCount = 0;
    _staticVarCountInFunction = 0;
    _staticVarCount = 0;
    _justinaFunctionBlockOpen = false;
    _JustinaVoidFunctionBlockOpen = false;

    _programVarNameCount = 0;
    if (fullReset) { _userVarCount = 0; }
    else {
        int index = 0;                                                              // clear user variable flag 'variable is used by program'
        while (index++ < _userVarCount) { userVarType[index] = userVarType[index] & ~var_userVarUsedByProgram; }
    }
    *_programStorage = tok_no_token;                                                //  set as current end of program 
    *(_programStorage + _PROGRAM_MEMORY_SIZE) = tok_no_token;                       //  set as current end of program (immediate mode)
    _programCounter = _programStorage + _PROGRAM_MEMORY_SIZE;                       // start of 'immediate mode' program area

    _programName[0] = '\0';

    if (!keepDebugLevelBatchFile) {
        _pEvalStackTop = nullptr; _pEvalStackMinus1 = nullptr;   _pEvalStackMinus2 = nullptr;
        _pFlowCtrlStackTop = nullptr;
        _pParsedCommandLineStackTop = nullptr;
        _activeFunctionData.callerEvalStackLevels = 0;                              // this is the highest program level
        _callStackDepth = 0;                                                        // equals flow control stack depth minus open loop (if, for, ...) blocks (= blocks being executed)
        _openDebugLevels = 0;                                                       // equals imm mode cmd stack depth minus open eval() strings (= eval() strings being executed)
    }

    _localVarValueAreaCount = 0;
    _localVarStringObjectCount = 0;
    _localArrayObjectCount = 0;



    // reset counters for heap objects
    // -------------------------------
    if (!keepDebugLevelBatchFile) {         // if open batch files, not all parsedStatementLineStack levels are cleared: corresponding parsed string objects are preserved  
        _parsedStringConstObjectCount = 0;
    }
    _identifierNameStringObjectCount = 0;                                           // object count
    _intermediateStringObjectCount = 0;
    _globalStaticVarStringObjectCount = 0;
    _globalStaticArrayObjectCount = 0;

    if (fullReset) {
        _lastValuesCount = 0;                                                       // current last result FiFo depth (values currently stored)
        _openFileCount = 0;

        // reset counters for heap objects
        // -------------------------------

        _userVarNameStringObjectCount = 0;
        _lastValuesStringObjectCount = 0;
        _userVarStringObjectCount = 0;
        _userArrayObjectCount = 0;
        _systemStringObjectCount = 0;


        // initialize format settings for numbers and strings (width, characters to print, flags, ...)
        // -------------------------------------------------------------------------------------------

        _dispWidth = DEFAULT_DISP_WIDTH;
        _tabSize = DEFAULT_TAB_SIZE;
        _dispFloatPrecision = DEFAULT_FLOAT_PRECISION;
        _dispIntegerPrecision = DEFAULT_INT_PRECISION;

        strcpy(_dispFloatSpecifier, DEFAULT_FLOAT_SPECIFIER);
        strcpy(_dispIntegerSpecifier, DEFAULT_INT_SPECIFIER);                   // here without 'd' (long integer) : will be added  
        strcpy(_dispStringSpecifier, DEFAULT_STR_SPECIFIER);                    // here without 'd' (long integer) : will be added  

        _dispFloatFmtFlags = DEFAULT_FLOAT_FLAGS;
        _dispIntegerFmtFlags = DEFAULT_INT_FLAGS;
        _dispStringFmtFlags = DEFAULT_STR_FLAGS;

        makeFormatString(_dispIntegerFmtFlags, true, _dispIntegerSpecifier, _dispIntegerFmtString);             // for integers
        makeFormatString(_dispFloatFmtFlags, false, _dispFloatSpecifier, _dispFloatFmtString);                  // for floats
        makeFormatString(_dispStringFmtFlags, false, _dispStringSpecifier, _dispStringFmtString);               // for strings


        // fmt() function settings 
        // -----------------------
        _fmt_width = DEFAULT_FMT_WIDTH;                                         // width

        _fmt_numPrecision = DEFAULT_FLOAT_PRECISION;                            // precision
        _fmt_strCharsToPrint = DEFAULT_STR_CHARS_TO_PRINT;

        strcpy(_fmt_numSpecifier, DEFAULT_FLOAT_SPECIFIER);                     // specifier   
        strcpy(_fmt_stringSpecifier, DEFAULT_STR_SPECIFIER);

        _fmt_numFmtFlags = DEFAULT_FLOAT_FLAGS;                                 // flags
        _fmt_stringFmtFlags = DEFAULT_STR_FLAGS;                                // flags


        // display and other settings
        // --------------------------

        _promptAndEcho = 2, _printLastResult = 1;
        _angleMode = 0;

        _printWatchValueOnly = 0;                                               // init: watch expression texts and values during expression watching
    }
}


    // --------------------------------------------------------------------------------------------------------
    // *   replace a system expression with a copy of a new string value and add a semicolon if not present   * 
    // --------------------------------------------------------------------------------------------------------

void Justina::setNewSystemExpression(char*& systemExpression, const char* newExpression) {

    // delete current system string (if not nullptr)
    if (systemExpression != nullptr) {
    #if PRINT_HEAP_OBJ_CREA_DEL
        _pDebugOut->print("\r\n----- (system exp str) "); _pDebugOut->println((uint32_t)systemExpression, HEX);
        _pDebugOut->print("   remove old sys expression "); _pDebugOut->println(systemExpression);
    #endif
        _systemStringObjectCount--;
        delete[] systemExpression;
        systemExpression = nullptr;
    }

    if (newExpression == nullptr) { return; }                               // nothing to do 

    // create a new system string, based on a new string; add a semicolon if not present

    bool addSemicolon{ false };
    int length = strlen(newExpression);
    int i = length;
    for (i = length - 1; i >= 0; i--) {
        if ((i == 0) || (newExpression[i] != ' ')) { addSemicolon = (newExpression[i] != term_semicolon[0]); break; }
    }

    _systemStringObjectCount++;
    systemExpression = new char[length + (addSemicolon ? 1 : 0) + 1];       // room for (optional: additional semicolon) and terminating '\0'
    strcpy(systemExpression, newExpression);                                // copy the actual string
    if (addSemicolon) {
        systemExpression[length] = term_semicolon[0];
        systemExpression[length + 1] = '\0';
    }
#if PRINT_HEAP_OBJ_CREA_DEL
    _pDebugOut->print("\r\n+++++ (system exp str) "); _pDebugOut->println((uint32_t)systemExpression, HEX);
    _pDebugOut->print("   set new sys expr expression "); _pDebugOut->println(systemExpression);
#endif
}


// -------------------------------------------------------------------------------------
// *   delete all identifier names (char strings)                                      *
// *   note: this excludes generic identifier names stored as alphanumeric constants   *
// -------------------------------------------------------------------------------------

void Justina::deleteIdentifierNameObjects(char** pIdentNameArray, int identifiersInUse, bool isUserVar) {
    int index = 0;          // points to last variable in use
    while (index < identifiersInUse) {                                      // points to variable in use
    #if PRINT_HEAP_OBJ_CREA_DEL
        _pDebugOut->print(isUserVar ? "\r\n----- (usrvar name) " : "\r\n----- (ident name ) "); _pDebugOut->println((uint32_t) * (pIdentNameArray + index), HEX);
        _pDebugOut->print("       delete ident "); _pDebugOut->println(*(pIdentNameArray + index));
    #endif
        isUserVar ? _userVarNameStringObjectCount-- : _identifierNameStringObjectCount--;
        delete[] * (pIdentNameArray + index);
        index++;
    }
}


// --------------------------------------------------------------------------------------------------------------
// *   all string array variables of a specified scope: delete array element character strings (heap objects)   *
// --------------------------------------------------------------------------------------------------------------

void Justina::deleteStringArrayVarsStringObjects(Justina::Val* varValues, char* sourceVarScopeAndFlags, int varNameCount, int paramOnlyCount, bool checkIfGlobalValue, bool isUserVar, bool isLocalVar) {
    int index = paramOnlyCount;                    // skip parameters (if function, otherwise must be zero) - if parameter variables are arrays, they are always a reference variable 
    while (index < varNameCount) {
        if (!checkIfGlobalValue || (sourceVarScopeAndFlags[index] & (var_nameHasGlobalValue))) {                                    // if only for global values: is it a global value ?

            if ((sourceVarScopeAndFlags[index] & (var_isArray | value_typeMask)) == (var_isArray | value_isStringPointer)) {        // array of strings
                deleteOneArrayVarStringObjects(varValues, index, isUserVar, isLocalVar);
            }
        }
        index++;
    }
}


// ---------------------------------------------------------------------------------------------
// *   delete variable heap objects: array variable character strings for ONE array variable   *
// ---------------------------------------------------------------------------------------------

// no checks are made - make sure the variable is an array variable of type 'string' (storing strings)

void Justina::deleteOneArrayVarStringObjects(Justina::Val* varValues, int index, bool isUserVar, bool isLocalVar) {
    void* pArrayStorage = varValues[index].pArray;                                                                                  // void pointer to an array of string pointers; element 0 contains dimensions and dimension count
    int dimensions = (((char*)pArrayStorage)[3]);                                                                                   // can range from 1 to MAX_ARRAY_DIMS
    int arrayElements = 1;                                                                                                          // determine array size
    for (int dimCnt = 0; dimCnt < dimensions; dimCnt++) { arrayElements *= (int)((((char*)pArrayStorage)[dimCnt])); }

    // delete non-empty strings
    for (int arrayElem = 1; arrayElem <= arrayElements; arrayElem++) {                                                              // array element 0 contains dimensions and count
        char* pString = ((char**)pArrayStorage)[arrayElem];
        uint32_t stringPointerAddress = (uint32_t) & (((char**)pArrayStorage)[arrayElem]);
        if (pString != nullptr) {
        #if PRINT_HEAP_OBJ_CREA_DEL
            _pDebugOut->print(isUserVar ? "\r\n----- (usr arr str) " : isLocalVar ? "\r\n-----(loc arr str)" : "\r\n----- (arr string ) "); _pDebugOut->println((uint32_t)pString, HEX);     // applicable to string and array (same pointer)
            _pDebugOut->print(" delete arr.var.str "); _pDebugOut->println(pString);
        #endif
            isUserVar ? _userVarStringObjectCount-- : isLocalVar ? _localVarStringObjectCount-- : _globalStaticVarStringObjectCount--;
            delete[]  pString;                                                                                                      // applicable to string and array (same pointer)
        }
    }
}


// ----------------------------------------------------------------------------------------------
// *   delete variable heap objects: scalar variable strings and array variable array storage   *
// ----------------------------------------------------------------------------------------------

// note: make sure array variable element string objects have been deleted prior to calling this routine

void Justina::deleteVariableValueObjects(Justina::Val* varValues, char* varType, int varNameCount, int paramOnlyCount, bool checkIfGlobalValue, bool isUserVar, bool isLocalVar) {

    int index = 0;
    // do NOT skip parameters if deleting function variables: with constant args, a local copy is created (always scalar) and must be deleted if non-empty string
    while (index < varNameCount) {
        if (!checkIfGlobalValue || (varType[index] & (var_nameHasGlobalValue))) {                                                   // global value ?
            // check for arrays before checking for strings (if both 'var_isArray' and 'value_isStringPointer' bits are set: array of strings, with strings already deleted)
            if (((varType[index] & value_typeMask) != value_isVarRef) && (varType[index] & var_isArray)) {                          // variable is an array: delete array storage          
            #if PRINT_HEAP_OBJ_CREA_DEL
                _pDebugOut->print(isUserVar ? "\r\n----- (usr ar stor) " : isLocalVar ? "\r\n----- (loc ar stor) " : "\r\n----- (array stor ) "); _pDebugOut->println((uint32_t)varValues[index].pArray, HEX);
            #endif
                isUserVar ? _userArrayObjectCount-- : isLocalVar ? _localArrayObjectCount-- : _globalStaticArrayObjectCount--;
                delete[]  varValues[index].pArray;
            }
            else if ((varType[index] & value_typeMask) == value_isStringPointer) {                                                  // variable is a scalar containing a string
                if (varValues[index].pStringConst != nullptr) {
                #if PRINT_HEAP_OBJ_CREA_DEL
                    _pDebugOut->print(isUserVar ? "\r\n----- (usr var str) " : isLocalVar ? "\r\n----- (loc var str)" : "\r\n----- (var string ) "); _pDebugOut->println((uint32_t)varValues[index].pStringConst, HEX);
                    _pDebugOut->print("     del.var.values "); _pDebugOut->println(varValues[index].pStringConst);
                #endif
                    isUserVar ? _userVarStringObjectCount-- : isLocalVar ? _localVarStringObjectCount-- : _globalStaticVarStringObjectCount--;
                    delete[]  varValues[index].pStringConst;
                }
            }
        }
        index++;
    }
}


// --------------------------------------------------------------------
// *   delete variable heap objects: last value FIFO string objects   *
// --------------------------------------------------------------------

void Justina::deleteLastValueFiFoStringObjects() {
    if (_lastValuesCount == 0) return;

    for (int i = 0; i < _lastValuesCount; i++) {
        bool isNonEmptyString = (lastResultTypeFiFo[i] == value_isStringPointer) ? (lastResultValueFiFo[i].pStringConst != nullptr) : false;
        if (isNonEmptyString) {
        #if PRINT_HEAP_OBJ_CREA_DEL
            _pDebugOut->print("\r\n----- (FiFo string) "); _pDebugOut->println((uint32_t)lastResultValueFiFo[i].pStringConst, HEX);
            _pDebugOut->print("del.last.v.fifo str "); _pDebugOut->println(lastResultValueFiFo[i].pStringConst);
        #endif
            _lastValuesStringObjectCount--;
            delete[] lastResultValueFiFo[i].pStringConst;
        }
    }
}


// ----------------------------------------------------------------------------------------------------------
// *   delete all parsed alphanumeric constant value heap objects in a program or in imm. mode statements   *
// *   note: this includes UNQUALIFIED identifier names stored as alphanumeric constants                    *
// ----------------------------------------------------------------------------------------------------------

// must be called before deleting tokens (list elements) 

void Justina::deleteConstStringObjects(char* pFirstToken) {
    char* pAnum;
    TokenPointer prgmCnt;

    prgmCnt.pTokenChars = pFirstToken;
    uint8_t tokenType = *prgmCnt.pTokenChars & 0x0F;
    while (tokenType != tok_no_token) {                                                                 // for all tokens in token list
        // not for predefined symbolic constants
        bool isStringConst = (tokenType == tok_isConstant) ? (((*prgmCnt.pTokenChars >> 4) & value_typeMask) == value_isStringPointer) : false;

        if (isStringConst || (tokenType == tok_isGenericName)) {
            memcpy(&pAnum, prgmCnt.pCstToken->cstValue.pStringConst, sizeof(pAnum));                    // copy pointer (not necessarily aligned with word size: copy memory instead)
            if (pAnum != nullptr) {
            #if PRINT_HEAP_OBJ_CREA_DEL
                _pDebugOut->print("\r\n----- (parsed str ) ");   _pDebugOut->println((uint32_t)pAnum, HEX);
                _pDebugOut->print("   del.const.string ");   _pDebugOut->println(pAnum);
            #endif
                _parsedStringConstObjectCount--;
                delete[] pAnum;
            }
        }
        uint8_t tokenLength = (tokenType >= tok_isTerminalGroup1) ? sizeof(Token_terminal) : (tokenType == tok_isConstant) ? sizeof(Token_constant) :
            (tokenType == tok_isSymbolicConstant) ? sizeof(Token_symbolicConstant) : (*prgmCnt.pTokenChars >> 4) & 0x0F;
        prgmCnt.pTokenChars += tokenLength;
        tokenType = *prgmCnt.pTokenChars & 0x0F;
    }
}


// ---------------------------------------------------------------------------------
// *   delete a variable string object referenced in an evaluation stack element   *
// ---------------------------------------------------------------------------------

// if not a string, then do nothing. If not a string, or string is empty, then exit WITH error

Justina::execResult_type Justina::deleteVarStringObject(LE_evalStack* pStackLvl) {
    if (pStackLvl->varOrConst.tokenType != tok_isVariable) { return result_exec_OK; };                                               // not a variable
    if ((*pStackLvl->varOrConst.varTypeAddress & value_typeMask) != value_isStringPointer) { return result_exec_OK; }                // not a string object
    if (*pStackLvl->varOrConst.value.ppStringConst == nullptr) { return result_exec_OK; }

    char varScope = (pStackLvl->varOrConst.sourceVarScopeAndFlags & var_scopeMask);

    // delete variable string object
#if PRINT_HEAP_OBJ_CREA_DEL
    _pDebugOut->print((varScope == var_isUser) ? "\r\n----- (usr var str) " : ((varScope == var_isGlobal) || (varScope == var_isStaticInFunc)) ? "\r\n----- (var string ) " : "\r\n----- (loc var str) ");
    _pDebugOut->println((uint32_t)*pStackLvl->varOrConst.value.ppStringConst, HEX);
    _pDebugOut->print("     del var string "); _pDebugOut->println(*pStackLvl->varOrConst.value.ppStringConst);
#endif
    (varScope == var_isUser) ? _userVarStringObjectCount-- : ((varScope == var_isGlobal) || (varScope == var_isStaticInFunc)) ? _globalStaticVarStringObjectCount-- : _localVarStringObjectCount--;
    delete[] * pStackLvl->varOrConst.value.ppStringConst;
    return result_exec_OK;
}


// --------------------------------------------------------------------------------------
// *   delete an intermediate string object referenced in an evaluation stack element   *
// --------------------------------------------------------------------------------------

// if not a string, then do nothing. If not an intermediate string object, then exit WITHOUT error

Justina::execResult_type Justina::deleteIntermStringObject(LE_evalStack* pStackLvl) {

    if ((pStackLvl->varOrConst.valueAttributes & constIsIntermediate) != constIsIntermediate) { return result_exec_OK; }             // not an intermediate constant
    if (pStackLvl->varOrConst.valueType != value_isStringPointer) { return result_exec_OK; }                                         // not a string object
    if (pStackLvl->varOrConst.value.pStringConst == nullptr) { return result_exec_OK; }
#if PRINT_HEAP_OBJ_CREA_DEL
    _pDebugOut->print("\r\n----- (Intermd str) ");   _pDebugOut->println((uint32_t)_pEvalStackTop->varOrConst.value.pStringConst, HEX);
    _pDebugOut->print("  del.interm.string ");   _pDebugOut->println(_pEvalStackTop->varOrConst.value.pStringConst);
#endif
    _intermediateStringObjectCount--;
    delete[] pStackLvl->varOrConst.value.pStringConst;

    return result_exec_OK;
}


