/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSimulator Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @brief Reduce parser tokens to abstract syntax tree tokens.
 *
 * Usage:
 *
 *  tokenBegin = returned by TokenBegin.Analyze ()
 *               representing the whole script source
 *               as a flat list of tokens
 *
 *  TokenScript tokenScript = Reduce.Analyze (TokenBegin tokenBegin);
 *
 *  tokenScript = represents the whole script source
 *                as a tree of tokens
 */

using OpenSim.Region.ScriptEngine.Shared.ScriptBase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

using LSL_Float = OpenSim.Region.ScriptEngine.Shared.LSL_Types.LSLFloat;
using LSL_Integer = OpenSim.Region.ScriptEngine.Shared.LSL_Types.LSLInteger;
using LSL_Key = OpenSim.Region.ScriptEngine.Shared.LSL_Types.LSLString;
using LSL_List = OpenSim.Region.ScriptEngine.Shared.LSL_Types.list;
using LSL_Rotation = OpenSim.Region.ScriptEngine.Shared.LSL_Types.Quaternion;
using LSL_String = OpenSim.Region.ScriptEngine.Shared.LSL_Types.LSLString;
using LSL_Vector = OpenSim.Region.ScriptEngine.Shared.LSL_Types.Vector3;

namespace OpenSim.Region.ScriptEngine.Yengine
{
    public class ScriptReduce
    {
        public const uint SDT_PRIVATE = 1;
        public const uint SDT_PROTECTED = 2;
        public const uint SDT_PUBLIC = 4;
        public const uint SDT_ABSTRACT = 8;
        public const uint SDT_FINAL = 16;
        public const uint SDT_NEW = 32;
        public const uint SDT_OVERRIDE = 64;
        public const uint SDT_STATIC = 128;
        public const uint SDT_VIRTUAL = 256;

        private const int ASNPR = 50;

        private readonly static Dictionary<Type, int> precedence = PrecedenceInit();

        private static readonly Type[] brkCloseOnly = new Type[] { typeof(TokenKwBrkClose) };
        private static readonly Type[] cmpGTOnly = new Type[] { typeof(TokenKwCmpGT) };
        private static readonly Type[] colonOnly = new Type[] { typeof(TokenKwColon) };
        private static readonly Type[] commaOrBrcClose = new Type[] { typeof(TokenKwComma), typeof(TokenKwBrcClose) };
        private static readonly Type[] colonOrDotDotDot = new Type[] { typeof(TokenKwColon), typeof(TokenKwDotDotDot) };
        private static readonly Type[] parCloseOnly = new Type[] { typeof(TokenKwParClose) };
        private static readonly Type[] semiOnly = new Type[] { typeof(TokenKwSemi) };

        /**
         * @brief Initialize operator precedence table
         * @returns with precedence table pointer
         */
        private static Dictionary<Type, int> PrecedenceInit()
        {
            return new Dictionary<Type, int>()
            {
                // http://www.lslwiki.net/lslwiki/wakka.php?wakka=operators

                { typeof(TokenKwComma), 30 },

                { typeof(TokenKwAsnLSh), ASNPR },  // all assignment operators of equal precedence
                { typeof(TokenKwAsnRSh), ASNPR },  // ... so they get processed strictly right-to-left
                { typeof(TokenKwAsnAdd), ASNPR },
                { typeof(TokenKwAsnAnd), ASNPR },
                { typeof(TokenKwAsnSub), ASNPR },
                { typeof(TokenKwAsnMul), ASNPR },
                { typeof(TokenKwAsnDiv), ASNPR },
                { typeof(TokenKwAsnMod), ASNPR },
                { typeof(TokenKwAsnOr), ASNPR },
                { typeof(TokenKwAsnXor), ASNPR },
                { typeof(TokenKwAssign), ASNPR },

                { typeof(TokenKwQMark), 60 },

                { typeof(TokenKwOrOrOr), 70 },
                { typeof(TokenKwAndAndAnd), 80 },

                { typeof(TokenKwOrOr), 100 },

                //{ typeof(TokenKwAndAnd), 120 },
                { typeof(TokenKwAndAnd), 100 },

                { typeof(TokenKwOr), 140 },

                { typeof(TokenKwXor), 160 },

                { typeof(TokenKwAnd), 180 },

                { typeof(TokenKwCmpEQ), 200 },
                { typeof(TokenKwCmpNE), 200 },

                { typeof(TokenKwCmpLT), 240 },
                { typeof(TokenKwCmpLE), 240 },
                { typeof(TokenKwCmpGT), 240 },
                { typeof(TokenKwCmpGE), 240 },

                { typeof(TokenKwRSh), 260 },
                { typeof(TokenKwLSh), 260 },

                { typeof(TokenKwAdd), 280 },
                { typeof(TokenKwSub), 280 },

                { typeof(TokenKwMul), 320 },
                { typeof(TokenKwDiv), 320 },
                { typeof(TokenKwMod), 320 }
            };
        }

        /**
         * @brief Reduce raw token stream to a single script token.
         *        Performs a little semantic testing, ie, undefined variables, etc.
         * @param tokenBegin = points to a TokenBegin
         *                     followed by raw tokens
         *                     and last token is a TokenEnd
         * @returns null: not a valid script, error messages have been output
         *          else: valid script top token
         */
        public static TokenScript Reduce(TokenBegin tokenBegin)
        {
            return new ScriptReduce(tokenBegin).tokenScript;
        }

        /*
         * Instance variables.
         */
        private bool errors = false;
        private string lastErrorFile = "";
        private int lastErrorLine = 0;
        private int numTypedefs = 0;
        private TokenDeclVar currentDeclFunc = null;
        private TokenDeclSDType currentDeclSDType = null;
        private TokenScript tokenScript;
        private TokenStmtBlock currentStmtBlock = null;

        /**
         * @brief the constructor does all the processing.
         * @param token = first token of script after the TokenBegin token
         * @returns tokenScript = null: there were errors
         *                        else: successful
         */
        private ScriptReduce(TokenBegin tokenBegin)
        {
             // Create a place to put the top-level script components,
             // eg, state bodies, functions, global variables.
            tokenScript = new TokenScript(tokenBegin.nextToken);

             // 'class', 'delegate', 'instance' all define types.
             // So we pre-scan the source tokens for those keywords
             // to build a script-defined type table and substitute
             // type tokens for those names in the source.  This is
             // done as a separate scan so they can cross-reference
             // each other.  Also does likewise for fixed array types.
             //
             // Also, all 'typedef's are processed here.  Their definitions 
             // remain in the source token stream after this, but they can 
             // be skipped over, because their bodies have been substituted 
             // in the source for any references.
            ParseSDTypePreScanPassOne(tokenBegin);  // catalog definitions
            ParseSDTypePreScanPassTwo(tokenBegin);  // substitute references

            /*
            int braces = 0;
            Token prevTok = null;
            for (Token token = tokenBegin; token != null; token = token.nextToken) {
                if (token is TokenKwParClose) braces -= 2;
                if (token is TokenKwBrcClose) braces -= 4;
                StringBuilder sb = new StringBuilder ("ScriptReduce*: ");
                sb.Append (token.GetHashCode ().ToString ("X8"));
                sb.Append (" ");
                sb.Append (token.line.ToString ().PadLeft (3));
                sb.Append (".");
                sb.Append (token.posn.ToString ().PadLeft (3));
                sb.Append ("  ");
                sb.Append (token.GetType ().Name.PadRight (24));
                sb.Append (" : ");
                for (int i = 0; i < braces; i ++) sb.Append (' ');
                token.DebString (sb);
                Console.WriteLine (sb.ToString ());
                if (token.prevToken != prevTok) {
                    Console.WriteLine ("ScriptReduce*:  -- prevToken link bad => " + token.prevToken.GetHashCode ().ToString ("X8"));
                }
                if (token is TokenKwBrcOpen) braces += 4;
                if (token is TokenKwParOpen) braces += 2;
                prevTok = token;
            }
            */

             // Create a function $globalvarinit to hold all explicit
             // global variable initializations.
            TokenDeclVar gviFunc = new (tokenBegin, null, tokenScript);
            gviFunc.name = new TokenName(gviFunc, "$globalvarinit");
            gviFunc.retType = new TokenTypeVoid(gviFunc);
            gviFunc.argDecl = new TokenArgDecl(gviFunc);
            TokenStmtBlock gviBody = new (gviFunc)
            {
                function = gviFunc
            };
            gviFunc.body = gviBody;
            tokenScript.globalVarInit = gviFunc;
            tokenScript.AddVarEntry(gviFunc);

             // Scan through the tokens until we reach the end.
            for(Token token = tokenBegin.nextToken; token is not TokenEnd;)
            {
                if(token is TokenKwSemi)
                {
                    token = token.nextToken;
                    continue;
                }

                 // Script-defined type declarations.
                if(ParseDeclSDTypes(ref token, null, SDT_PUBLIC))
                    continue;

                 // constant <name> = <rval> ;
                if(token is TokenKwConst)
                {
                    ParseDeclVar(ref token, null);
                    continue;
                }

                 // <type> <name> ;
                 // <type> <name> = <rval> ;
                if((token is TokenType) &&
                    (token.nextToken is TokenName) &&
                    ((token.nextToken.nextToken is TokenKwSemi) ||
                     (token.nextToken.nextToken is TokenKwAssign)))
                {
                    TokenDeclVar tdvar = ParseDeclVar(ref token, gviFunc);
                    if(tdvar != null)
                    {
                        // <name> = <init>;
                        TokenLValName left = new (tdvar.name, tokenScript.variablesStack);
                        DoVarInit(gviFunc, left, tdvar.init);
                    }
                    continue;
                }

                 // <type> <name> { [ get { <body> } ] [ set { <body> } ] }
                if((token is TokenType) &&
                    (token.nextToken is TokenName) &&
                    (token.nextToken.nextToken is TokenKwBrcOpen))
                {
                    ParseProperty(ref token, false, true);
                    continue;
                }

                 // <type> <name> <funcargs> <funcbody>
                 // global function returning specified type
                if(token is TokenType tokenType)
                {
                    token = token.nextToken;
                    if(token is not TokenName)
                    {
                        ErrorMsg(token, "expecting variable/function name");
                        token = SkipPastSemi(token);
                        continue;
                    }
                    TokenName tokenName = (TokenName)token;
                    token = token.nextToken;
                    if(token is not TokenKwParOpen)
                    {
                        ErrorMsg(token, "<type> <name> must be followed by ; = or (");
                        token = SkipPastSemi(token);
                        continue;
                    }
                    token = tokenType;
                    TokenDeclVar tokenDeclFunc = ParseDeclFunc(ref token, false, false, false);
                    if(tokenDeclFunc == null)
                        continue;
                    if(!tokenScript.AddVarEntry(tokenDeclFunc))
                    {
                        ErrorMsg(tokenName, "duplicate function " + tokenDeclFunc.funcNameSig.val);
                    }
                    continue;
                }

                 // <name> <funcargs> <funcbody>
                 // global function returning void
                if(token is TokenName)
                {
                    TokenName tokenName = (TokenName)token;
                    token = token.nextToken;
                    if(token is not TokenKwParOpen)
                    {
                        ErrorMsg(token, "looking for open paren after assuming " +
                                         tokenName.val + " is a function name");
                        token = SkipPastSemi(token);
                        continue;
                    }
                    token = tokenName;
                    TokenDeclVar tokenDeclFunc = ParseDeclFunc(ref token, false, false, false);
                    if(tokenDeclFunc == null)
                        continue;
                    if(!tokenScript.AddVarEntry(tokenDeclFunc))
                        ErrorMsg(tokenName, "duplicate function " + tokenDeclFunc.funcNameSig.val);

                    continue;
                }

                 // default <statebody>
                if(token is TokenKwDefault)
                {
                    TokenDeclState tokenDeclState = new (token);
                    token = token.nextToken;
                    tokenDeclState.body = ParseStateBody(ref token);
                    if(tokenDeclState.body == null)
                        continue;
                    if(tokenScript.defaultState != null)
                    {
                        ErrorMsg(tokenDeclState, "default state already declared");
                        continue;
                    }
                    tokenScript.defaultState = tokenDeclState;
                    continue;
                }

                 // state <name> <statebody>
                if(token is TokenKwState)
                {
                    TokenDeclState tokenDeclState = new (token);
                    token = token.nextToken;
                    if(token is not TokenName TokenNametoken)
                    {
                        ErrorMsg(token, "state must be followed by state name");
                        token = SkipPastSemi(token);
                        continue;
                    }
                    tokenDeclState.name = TokenNametoken;
                    token = token.nextToken;
                    tokenDeclState.body = ParseStateBody(ref token);
                    if(tokenDeclState.body == null)
                        continue;
                    if(tokenScript.states.ContainsKey(tokenDeclState.name.val))
                    {
                        ErrorMsg(tokenDeclState.name, "duplicate state definition");
                        continue;
                    }
                    tokenScript.states.Add(tokenDeclState.name.val, tokenDeclState);
                    continue;
                }

                 // Doesn't fit any of those forms, output message and skip to next statement.
                ErrorMsg(token, "looking for var name, type, state or default, script-defined type declaration");
                token = SkipPastSemi(token);
                continue;
            }

             // Must have a default state to start in.
            if(!errors && (tokenScript.defaultState == null))
            {
                ErrorMsg(tokenScript, "no default state defined");
            }

             // If any error messages were written out, set return value to null.
            if(errors)
                tokenScript = null;
        }

        /**
         * @brief Pre-scan the source for class, delegate, interface, typedef definition keywords.
         *        Clump the keywords and name being defined together, but leave the body intact.
         *        In the case of a delegate with an explicit return type, it reverses the name and return type.
         *        After this completes there shouldn't be any TokenKw{Class,Delegate,Interface,Typedef} 
         *        keywords in the source, they are all replaced by TokenDeclSDType{Class,Delegate,Interface,
         *        Typedef} tokens which also encapsulate the name of the type being defined and any generic 
         *        parameter names.  The body remains intact in the source token stream following the 
         *        TokenDeclSDType* token.
         */
        private void ParseSDTypePreScanPassOne(Token tokenBegin)
        {
            Stack<int> braceLevels = new();
            Stack<TokenDeclSDType> outerLevels = new();
            int openBraceLevel = 0;
            braceLevels.Push(-1);
            outerLevels.Push(null);
            Token t = tokenBegin;
            while((t = t.nextToken) is not TokenEnd)
            {
                 // Keep track of nested definitions so we can link them up.
                 // We also need to detect the end of class and interface definitions.
                if(t is TokenKwBrcOpen)
                {
                    openBraceLevel++;
                    continue;
                }
                if(t is TokenKwBrcClose)
                {
                    if(--openBraceLevel < 0)
                    {
                        ErrorMsg(t, "{ } mismatch");
                        return;
                    }
                    if(braceLevels.Peek() == openBraceLevel)
                    {
                        braceLevels.Pop();
                        outerLevels.Pop().endToken = t;
                    }
                    continue;
                }

                 // Check for 'class' or 'interface'.
                 // They always define a new class or interface.
                 // They can contain nested script-defined type definitions.
                if((t is TokenKwClass) || (t is TokenKwInterface))
                {
                    Token kw = t;
                    t = t.nextToken;
                    if(t is not TokenName)
                    {
                        ErrorMsg(t, "expecting class or interface name");
                        t = SkipPastSemi(t).prevToken;
                        continue;
                    }
                    TokenName name = (TokenName)t;
                    t = t.nextToken;

                     // Malloc the script-defined type object.
                    TokenDeclSDType decl;
                    if(kw is TokenKwClass)
                        decl = new TokenDeclSDTypeClass(name, kw.prevToken is TokenKwPartial);
                    else
                        decl = new TokenDeclSDTypeInterface(name);
                    decl.outerSDType = outerLevels.Peek();

                     // Check for generic parameter list.
                    if(!ParseGenProtoParamList(ref t, decl))
                        continue;

                     // Splice in a TokenDeclSDType token that replaces the keyword and the name tokens 
                     // and any generic parameters including the '<', ','s and '>'.
                     //   kw = points to 'class' or 'interface' keyword.
                     //    t = points to just past last part of class name parsed, hopefully a ':' or '{'.
                    decl.prevToken = decl.isPartial ? kw.prevToken.prevToken : kw.prevToken;
                    decl.nextToken = t;
                    decl.prevToken.nextToken = decl;
                    decl.nextToken.prevToken = decl;

                     // Enter it in name lists so it can be seen by others.
                    Token partialNewBody = CatalogSDTypeDecl(decl);

                     // Start inner type definitions.
                    braceLevels.Push(openBraceLevel);
                    outerLevels.Push(decl);

                     // Scan the body starting on for before the '{'.
                     //
                     // If this body had an old partial merged into it,
                     // resume scanning at the beginning of the new body, 
                     // ie, what used to be the first token after the '{' 
                     // before the old body was spliced in.
                    if(partialNewBody != null)
                    {
                         // We have a partial that has had old partial body merged 
                         // into new partial body.  So resume scanning at the beginning 
                         // of the new partial body so we don't get any duplicate scanning 
                         // of the old partial body.
                         //
                         //   <decl> ... { <oldbody> <newbody> }
                         //                          ^- resume scanning here
                         //                             but inc openBraceLevel because
                         //                             we skipped scanning the '{'
                        openBraceLevel++;
                        t = partialNewBody;
                    }
                    t = t.prevToken;
                    continue;
                }

                 // Check for 'delegate'.
                 // It always defines a new delegate.
                 // Delegates never define nested types.
                if(t is TokenKwDelegate)
                {
                    Token kw = t;
                    t = t.nextToken;

                     // Next thing might be an explicit return type or the delegate's name.
                     // If it's a type token, then it's the return type, simple enough.
                     // But if it's a name token, it might be the name of some other script-defined type.
                     // The way to tell is that the delegate name is followed by a '(', whereas an 
                     // explicit return type is followed by the delegate name.
                    Token retType = t;
                    TokenName delName = null;
                    Token u;
                    int angles = 0;
                    for(u = t; u is not TokenKwParOpen; u = u.nextToken)
                    {
                        if((u is TokenKwSemi) || (u is TokenEnd))
                            break;
                        if(u is TokenKwCmpLT)
                            angles++;
                        if(u is TokenKwCmpGT)
                            angles--;
                        if(u is TokenKwRSh)
                            angles -= 2;  // idiot >>
                        if((angles == 0) && (u is TokenName name))
                            delName = name;
                    }
                    if(u is not TokenKwParOpen)
                    {
                        ErrorMsg(u, "expecting ( for delegate parameter list");
                        t = SkipPastSemi(t).prevToken;
                        continue;
                    }
                    if(delName == null)
                    {
                        ErrorMsg(u, "expecting delegate name");
                        t = SkipPastSemi(t).prevToken;
                        continue;
                    }
                    if(retType == delName)
                        retType = null;

                    // Malloc the script-defined type object.
                    TokenDeclSDTypeDelegate decl = new (delName)
                    {
                        outerSDType = outerLevels.Peek()
                    };

                    // Check for generic parameter list.
                    t = delName.nextToken;
                    if(!ParseGenProtoParamList(ref t, decl))
                        continue;

                     // Enter it in name lists so it can be seen by others.
                    CatalogSDTypeDecl(decl);

                     // Splice in the token that replaces the 'delegate' keyword and the whole name 
                     // (including the '<' name ... '>' parts). The return type token(s), if any, 
                     // follow the splice token and come before the '('.
                    decl.prevToken = kw.prevToken;
                    kw.prevToken.nextToken = decl;

                    if(retType == null)
                    {
                        decl.nextToken = t;
                        t.prevToken = decl;
                    }
                    else
                    {
                        decl.nextToken = retType;
                        retType.prevToken = decl;
                        retType.nextToken = t;
                        t.prevToken = retType;
                    }

                     // Scan for terminating ';'.
                     // There cannot be an intervening class, delegate, interfate, typedef, { or }.
                    for(t = decl; t is not TokenKwSemi; t = u)
                    {
                        u = t.nextToken;
                        if((u is TokenEnd) ||
                            (u is TokenKwClass) ||
                            (u is TokenKwDelegate) ||
                            (u is TokenKwInterface) ||
                            (u is TokenKwTypedef) ||
                            (u is TokenKwBrcOpen) ||
                            (u is TokenKwBrcClose))
                        {
                            ErrorMsg(t, "delegate missing terminating ;");
                            break;
                        }
                    }
                    decl.endToken = t;
                    continue;
                }

                 // Check for 'typedef'.
                 // It always defines a new macro.
                 // Typedefs never define nested types.
                if(t is TokenKwTypedef)
                {
                    Token kw = t;
                    t = t.nextToken;

                    if(t is not TokenName)
                    {
                        ErrorMsg(t, "expecting typedef name");
                        t = SkipPastSemi(t).prevToken;
                        continue;
                    }
                    TokenName tdName = (TokenName)t;
                    t = t.nextToken;

                    // Malloc the script-defined type object.
                    TokenDeclSDTypeTypedef decl = new (tdName)
                    {
                        outerSDType = outerLevels.Peek()
                    };

                    // Check for generic parameter list.
                    if (!ParseGenProtoParamList(ref t, decl))
                        continue;

                     // Enter it in name lists so it can be seen by others.
                    CatalogSDTypeDecl(decl);
                    numTypedefs++;

                     // Splice in the token that replaces the 'typedef' keyword and the whole name 
                     // (including the '<' name ... '>' parts).
                    decl.prevToken = kw.prevToken;
                    kw.prevToken.nextToken = decl;
                    decl.nextToken = t;
                    t.prevToken = decl;

                     // Scan for terminating ';'.
                     // There cannot be an intervening class, delegate, interfate, typedef, { or }.
                    Token u;
                    for(t = decl; t is not TokenKwSemi; t = u)
                    {
                        u = t.nextToken;
                        if((u is TokenEnd) ||
                            (u is TokenKwClass) ||
                            (u is TokenKwDelegate) ||
                            (u is TokenKwInterface) ||
                            (u is TokenKwTypedef) ||
                            (u is TokenKwBrcOpen) ||
                            (u is TokenKwBrcClose))
                        {
                            ErrorMsg(t, "typedef missing terminating ;");
                            break;
                        }
                    }
                    decl.endToken = t;
                    continue;
                }
            }
        }

        /**
         * @brief Parse a possibly generic type definition's parameter list.
         * @param t = points to the possible opening '<' on entry
         *            points just past the closing '>' on return
         * @param decl = the generic type being declared
         * @returns false: parse error
         *           true: decl.genParams = filled in with parameter list
         *                 decl.innerSDTypes = filled in with parameter list
         */
        private bool ParseGenProtoParamList(ref Token t, TokenDeclSDType decl)
        {
             // Maybe there aren't any generic parameters.
             // If so, leave decl.genParams = null.
            if(t is not TokenKwCmpLT)
                return true;

             // Build list of generic parameter names.
            Dictionary<string, int> parms = new();
            do
            {
                t = t.nextToken;
                if(t is not TokenName)
                {
                    ErrorMsg(t, "expecting generic parameter name");
                    break;
                }
                TokenName tn = (TokenName)t;
                if(parms.ContainsKey(tn.val))
                    ErrorMsg(tn, "duplicate use of generic parameter name");
                else
                    parms.Add(tn.val, parms.Count);
                t = t.nextToken;
            } while(t is TokenKwComma);

            if(t is not TokenKwCmpGT)
            {
                ErrorMsg(t, "expecting , for more params or > to end param list");
                return false;
            }
            t = t.nextToken;
            decl.genParams = parms;

            return true;
        }

        /**
         * @brief Catalog a script-defined type.
         *        Its short name (eg, 'Node') gets put in the next outer level (eg, 'List')'s inner type definition table.
         *        Its long name (eg, 'List.Node') gets put in the global type definition table.
         */
        public Token CatalogSDTypeDecl(TokenDeclSDType decl)
        {
            string longName = decl.longName.val;
            if (!tokenScript.sdSrcTypesTryGetValue(longName, out TokenDeclSDType dupDecl))
            {
                tokenScript.sdSrcTypesAdd(longName, decl);
                decl.outerSDType?.innerSDTypes.Add(decl.shortName.val, decl);

                return null;
            }

            if (!dupDecl.isPartial || !decl.isPartial)
            {
                ErrorMsg(decl, "duplicate definition of type " + longName);
                ErrorMsg(dupDecl, "previous definition here");
                return null;
            }

            if(!GenericParametersMatch(decl, dupDecl))
                ErrorMsg(decl, "all partial class generic parameters must match");

             // Have new declaration be the cataloged one because body is going to get 
             // snipped out of old declaration and pasted into new declaration.
            tokenScript.sdSrcTypesRep(longName, decl);
            if(decl.outerSDType != null)
                decl.outerSDType.innerSDTypes[decl.shortName.val] = decl;

             // Find old partial definition's opening brace.
            Token dupBrcOpen;
            for(dupBrcOpen = dupDecl; dupBrcOpen is not TokenKwBrcOpen; dupBrcOpen = dupBrcOpen.nextToken)
            {
                if(dupBrcOpen == dupDecl.endToken)
                {
                    ErrorMsg(dupDecl, "missing {");
                    return null;
                }
            }

             // Find new partial definition's opening brace.
            Token brcOpen;
            for(brcOpen = decl; brcOpen is not TokenKwBrcOpen; brcOpen = brcOpen.nextToken)
            {
                if(brcOpen is TokenEnd)
                {
                    ErrorMsg(decl, "missing {");
                    return null;
                }
            }
            Token body = brcOpen.nextToken;

             // Stick old partial definition's extends/implementeds list just 
             // in front of new partial definition's extends/implementeds list.
             //
             //    class    oldextimp    {          oldbody    }                 ...
             //   dupDecl               dupBrcOpen            dupDecl.endToken
             //
             //    class    newextimp    {          newbody    }                 ...
             //   decl                  brcOpen    body       decl.endToken
             //
             // becomes
             //
             //    class             ...
             //   dupDecl
             //   dupDecl.endToken
             //
             //    class    oldextimp   newextimp    {          oldbody   newbody    }              ...
             //   decl                              brcOpen              body       decl.endToken
            if(dupBrcOpen != dupDecl.nextToken)
            {
                dupBrcOpen.prevToken.nextToken = decl.nextToken;
                dupDecl.nextToken.prevToken = decl;
                decl.nextToken.prevToken = dupBrcOpen.prevToken;
                decl.nextToken = dupDecl.nextToken;
            }

             // Stick old partial definition's body just 
             // in front of new partial definition's body.
            if(dupBrcOpen.nextToken != dupDecl.endToken)
            {
                dupBrcOpen.nextToken.prevToken = brcOpen;
                dupDecl.endToken.prevToken.nextToken = body;
                body.prevToken = dupDecl.endToken.prevToken;
                brcOpen.nextToken = dupBrcOpen.nextToken;
            }

             // Null out old definition's extends/implementeds list and body 
             // by having the declaration token be the only thing left.
            dupDecl.nextToken = dupDecl.endToken.nextToken;
            dupDecl.nextToken.prevToken = dupDecl;
            dupDecl.endToken = dupDecl;

            return body;
        }

        /**
         * @brief Determine whether or not the generic parameters of two class declarations match exactly.
         */
        private static bool GenericParametersMatch(TokenDeclSDType c1, TokenDeclSDType c2)
        {
            if((c1.genParams == null) && (c2.genParams == null))
                return true;
            if((c1.genParams == null) || (c2.genParams == null))
                return false;
            Dictionary<string, int> gp1 = c1.genParams;
            Dictionary<string, int> gp2 = c2.genParams;
            if(gp1.Count != gp2.Count)
                return false;
            foreach(KeyValuePair<string, int> kvp1 in gp1)
            {
                if (!gp2.TryGetValue(kvp1.Key, out int v2))
                    return false;
                if (v2 != kvp1.Value)
                    return false;
            }
            return true;
        }

        /**
         * @brief Replace all TokenName tokens that refer to the script-defined types with 
         *        corresponding TokenTypeSDType{Class,Delegate,GenParam,Interface} tokens.
         *        Also handle generic references, ie, recognize that 'List<integer>' is an
         *        instantiation of 'List<>' and instantiate the generic.
         */
        private const uint REPEAT_NOTYPE = 1;
        private const uint REPEAT_INSTGEN = 2;
        private const uint REPEAT_SUBST = 4;

        private void ParseSDTypePreScanPassTwo(Token tokenBegin)
        {
            List<Token> noTypes = new();
            TokenDeclSDType outerSDType;
            uint repeat;

            do
            {
                repeat = 0;
                outerSDType = null;
                noTypes.Clear();

                for(Token t = tokenBegin; (t = t.nextToken) is not TokenEnd;)
                {
                     // Maybe it's time to pop out of an outer class definition.
                    if((outerSDType != null) && (outerSDType.endToken == t))
                    {
                        outerSDType = outerSDType.outerSDType;
                        continue;
                    }

                     // Skip completely over any script-defined generic prototypes.
                     // We only need to process their instantiations which are non-
                     // generic versions of the generics.
                    if((t is TokenDeclSDType ttype) && ttype.genParams != null)
                    {
                        t = ttype.endToken;
                        continue;
                    }

                     // Check for beginning of non-generic script-defined type definitions.
                     // They can have nested definitions in their innerSDTypes[] that match 
                     // name tokens, so add them to the stack.
                     //
                     // But just ignore any preliminary partial definitions as they have had 
                     // their entire contents spliced out and spliced into a subsequent partial 
                     // definition.  So if we originally had:
                     //    partial class Abc { public intenger one; }
                     //    partial class Abc { public intenger two; }
                     // We now have:
                     //    partial_class_Abc    <== if we are here, just ignore the partial_class_Abc token
                     //    partial_class_Abc { public intenger one; public intenger two; }
                    if(t is TokenDeclSDType ttype2)
                    {
                        if(ttype2.endToken != t)
                            outerSDType = (TokenDeclSDType)t;

                        continue;
                    }

                     // For names not preceded by a '.', scan the script-defined type definition 
                     // stack for that name.  Splice the name out and replace with equivalent token.
                    if((t is TokenName) && t.prevToken is not TokenKwDot)
                        t = TrySpliceTypeRef(t, outerSDType, ref repeat, noTypes);

                     // This handles types such as integer[,][], List<string>[], etc.
                     // They are an instantiation of an internally generated type of the same name, brackets and all.
                     // Note that to malloc an array, use something like 'new float[,][](3,5)', not 'new float[3,5][]'.
                     //
                     // Note that we must not get confused by $idxprop property declarations such as:
                     //    float [string kee] { get { ... } }
                     // ... and try to convert 'float' '[' to an array type.
                    if((t is TokenType) && (t.nextToken is TokenKwBrkOpen))
                    {
                        if((t.nextToken.nextToken is TokenKwBrkClose) ||
                            (t.nextToken.nextToken is TokenKwComma))
                        {
                            t = InstantiateJaggedArray(t, tokenBegin, ref repeat);
                        }
                    }
                }

                 // If we instantiated a generic, loop back to process its contents
                 // just as if the source code had the instantiated code to begin with.
                 // Also repeat if we found a non-type inside the <> of a generic reference 
                 // provided we have made at least one name->type substitution.
            } while(((repeat & REPEAT_INSTGEN) != 0) ||
                     ((repeat & (REPEAT_NOTYPE | REPEAT_SUBST)) == (REPEAT_NOTYPE | REPEAT_SUBST)));

             // These are places where we required a type be present, 
             // eg, a generic type argument or the body of a typedef.
            foreach(Token t in noTypes)
                ErrorMsg(t, "looking for type");
        }

        /**
         * @brief Try to convert the source token string to a type reference
         *        and splice the type reference into the source token string
         *        replacing the original token(s).
         * @param t = points to the initial TokenName token
         * @param outerSDType = null: this is a top-level code reference
         *                      else: this code is within outerSDType
         * @returns pointer to last token parsed
         *          possibly with spliced-in type token
         *          repeat = possibly set true if need to do another pass
         */
        private Token TrySpliceTypeRef(Token t, TokenDeclSDType outerSDType, ref uint repeat, List<Token> noTypes)
        {
            Token start = t;
            string tnamestr = ((TokenName)t).val;

             // Look for the name as a type declared by outerSDType or anything
             // even farther out than that.  If not found, simply return 
             // without updating t, meaning that t isn't the name of a type.
            TokenDeclSDType decl = null;
            while(outerSDType != null)
            {
                if(outerSDType.innerSDTypes.TryGetValue(tnamestr, out decl))
                    break;
                outerSDType = outerSDType.outerSDType;
            }
            if((outerSDType == null) && !tokenScript.sdSrcTypesTryGetValue(tnamestr, out decl))
                return t;

            TokenDeclSDType instdecl;
            while(true)
            {
                 // If it is a generic type, it must be followed by instantiation arguments.
                instdecl = decl;
                if(decl.genParams != null)
                {
                    t = t.nextToken;
                    if(t is not TokenKwCmpLT)
                    {
                        ErrorMsg(t, "expecting < for generic argument list");
                        return t;
                    }
                    tnamestr += "<";
                    int nArgs = decl.genParams.Count;
                    TokenType[] genArgs = new TokenType[nArgs];
                    for(int i = 0; i < nArgs;)
                    {
                        t = t.nextToken;
                        if(t is not TokenType)
                        {
                            repeat |= REPEAT_NOTYPE;
                            noTypes.Add(t);
                            return t.prevToken;  // make sure name gets processed
                                                 // so substitution can occur on it
                        }
                        TokenType ga = (TokenType)t;
                        genArgs[i] = ga;
                        tnamestr += ga.ToString();
                        t = t.nextToken;
                        if(++i < nArgs)
                        {
                            if(t is not TokenKwComma)
                            {
                                ErrorMsg(t, "expecting , for more generic arguments");
                                return t;
                            }
                            tnamestr += ",";
                        }
                    }
                    if(t is TokenKwRSh)
                    {  // idiot >>
                        Token u = new TokenKwCmpGT(t);
                        Token v = new TokenKwCmpGT(t);
                        v.posn++;
                        u.prevToken = t.prevToken;
                        u.nextToken = v;
                        v.nextToken = t.nextToken;
                        v.prevToken = u;
                        u.prevToken.nextToken = u;
                        v.nextToken.prevToken = v;
                        t = u;
                    }
                    if(t is not TokenKwCmpGT)
                    {
                        ErrorMsg(t, "expecting > at end of generic argument list");
                        return t;
                    }
                    tnamestr += ">";
                    if(outerSDType != null)
                    {
                        outerSDType.innerSDTypes.TryGetValue(tnamestr, out instdecl);
                    }
                    else
                    {
                        tokenScript.sdSrcTypesTryGetValue(tnamestr, out instdecl);
                    }

                     // Couldn't find 'List<string>' but found 'List' and we have genArgs = 'string'.
                     // Instantiate the generic to create 'List<string>'.  This splices the definition 
                     // of 'List<string>' into the source token stream just as if it had been there all 
                     // along.  We have to then repeat the scan to process the instance's contents.
                    if(instdecl == null)
                    {
                        instdecl = decl.InstantiateGeneric(tnamestr, genArgs, this);
                        CatalogSDTypeDecl(instdecl);
                        repeat |= REPEAT_INSTGEN;
                    }
                }

                 // Maybe caller wants a subtype by putting a '.' following all that.
                if(t.nextToken is not TokenKwDot)
                    break;
                if(t.nextToken.nextToken is not TokenName nextnextname)
                    break;
                tnamestr = nextnextname.val;
                if(!instdecl.innerSDTypes.TryGetValue(tnamestr, out decl))
                    break;
                t = t.nextToken.nextToken;
                outerSDType = instdecl;
            }

             // Create a reference in the source to the definition
             // that encapsulates the long dotted type name given in
             // the source, and replace the long dotted type name in
             // the source with the reference token, eg, replace 
             // 'Dictionary' '<' 'string' ',' 'integer' '>' '.' 'ValueList'
             // with 'Dictionary<string,integer>.ValueList'.
            TokenType refer = instdecl.MakeRefToken(start);
            if(refer == null)
            {
                // typedef body is not yet a type
                noTypes.Add(start);
                repeat |= REPEAT_NOTYPE;
                return start;
            }
            refer.prevToken = start.prevToken;  // start points right at the first TokenName
            refer.nextToken = t.nextToken;      // t points at the last TokenName or TokenKwCmpGT
            refer.prevToken.nextToken = refer;
            refer.nextToken.prevToken = refer;
            repeat |= REPEAT_SUBST;

            return refer;
        }

        /**
         * @brief We are known to have <type>'[' so make an equivalent array type.
         * @param t = points to the TokenType
         * @param tokenBegin = where we can safely splice in new array class definitions
         * @param repeat = set REPEAT_INSTGEN if new type created
         * @returns pointer to last token parsed
         *          possibly with spliced-in type token
         *          repeat = possibly set true if need to do another pass
         */
        private Token InstantiateJaggedArray(Token t, Token tokenBegin, ref uint repeat)
        {
            Token start = t;
            TokenType ofType = (TokenType)t;

            Stack<int> ranks = new();

            // When script specifies 'float[,][]' it means a two-dimensional matrix
            // that points to one-dimensional vectors of floats.  So we would push 
            // a 2 then a 1 in this parsing code...
            do
            {
                t = t.nextToken;                // point at '['
                int rank = 0;
                do
                {
                    rank++;                // count '[' and ','s
                    t = t.nextToken;        // point at ',' or ']'
                } while(t is TokenKwComma);
                if(t is not TokenKwBrkClose)
                {
                    ErrorMsg(t, "expecting only [ , or ] for array type specification");
                    return t;
                }
                ranks.Push(rank);
            } while(t.nextToken is TokenKwBrkOpen);

             // Now we build the types in reverse order.  For the example above we will:
             //   first, create a type that is a one-dimensional vector of floats, float[]
             //   second, create a type that is a two-dimensional matrix of that.
             // This keeps declaration and referencing similar, eg, 
             //   float[,][] jag = new float[,][] (3,4);
             //     jag[i,j][k] ... is used to access the elements
            do
            {
                int rank = ranks.Pop();
                TokenDeclSDType decl = InstantiateFixedArray(rank, ofType, tokenBegin, ref repeat);
                ofType = decl.MakeRefToken(ofType);
            } while(ranks.Count > 0);

             // Finally splice in the resultant array type to replace the original tokens.
            ofType.prevToken = start.prevToken;
            ofType.nextToken = t.nextToken;
            ofType.prevToken.nextToken = ofType;
            ofType.nextToken.prevToken = ofType;

             // Resume parsing just after the spliced-in array type token.
            return ofType;
        }

        /**
         * @brief Instantiate a script-defined class type to handle fixed-dimension arrays.
         * @param rank = number of dimensions for the array
         * @param ofType = type of each element of the array
         * @returns script-defined class declaration created to handle the array
         */
        private TokenDeclSDType InstantiateFixedArray(int rank, TokenType ofType, Token tokenBegin, ref uint repeat)
        {
             // Create the array type's name.
             // If starting with a non-array type, just append the rank to it, eg, float + rank=1 -> float[]
             // If starting with an array type, slip this rank in front of existing array, eg, float[] + rank=2 -> float[,][].
             // This makes it consistent with what the script-writer sees for both a type specification and when 
             // referencing elements in a jagged array.
            string name = ofType.ToString();
            StringBuilder sb = new(name);
            int ix = name.IndexOf('[');
            if(ix < 0)
                ix = name.Length;
            sb.Insert(ix++, '[');
            for(int i = 0; ++i < rank;)
            {
                sb.Insert(ix++, ',');
            }
            sb.Insert(ix, ']');
            name = sb.ToString();

            TokenDeclSDType fa;
            if(!tokenScript.sdSrcTypesTryGetValue(name, out fa))
            {
                char suffix = 'O';
                if(ofType is TokenTypeChar)
                    suffix = 'C';
                if(ofType is TokenTypeFloat)
                    suffix = 'F';
                if(ofType is TokenTypeInt)
                    suffix = 'I';

                 // Don't already have one, create a new skeleton struct.
                 // Splice in a definition for the class at beginning of source file.
                 //
                 //    class <arraytypename> {
                fa = new TokenDeclSDTypeClass(new TokenName(tokenScript, name), false);
                CatalogSDTypeDecl(fa);
                repeat |= REPEAT_INSTGEN;
                ((TokenDeclSDTypeClass)fa).arrayOfType = ofType;
                ((TokenDeclSDTypeClass)fa).arrayOfRank = rank;

                Token t = SpliceAfter(tokenBegin, fa);
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                 //        public integer len0;
                 //        public integer len1;
                 //        ...
                 //        public object obj;
                for(int i = 0; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenKwPublic(t));
                    t = SpliceAfter(t, new TokenTypeInt(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                }

                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, new TokenTypeObject(t));
                t = SpliceAfter(t, new TokenName(t, "obj"));
                t = SpliceAfter(t, new TokenKwSemi(t));

                 //        public constructor (integer len0, integer len1, ...) {
                 //            this.len0 = len0;
                 //            this.len1 = len1;
                 //            ...
                 //            this.obj = xmrFixedArrayAlloc<suffix> (len0 * len1 * ...);
                 //        }
                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, new TokenKwConstructor(t));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                for(int i = 0; i < rank; i++)
                {
                    if(i > 0)
                        t = SpliceAfter(t, new TokenKwComma(t));
                    t = SpliceAfter(t, new TokenTypeInt(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                }
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                for(int i = 0; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenKwThis(t));
                    t = SpliceAfter(t, new TokenKwDot(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwAssign(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                }

                t = SpliceAfter(t, new TokenKwThis(t));
                t = SpliceAfter(t, new TokenKwDot(t));
                t = SpliceAfter(t, new TokenName(t, "obj"));
                t = SpliceAfter(t, new TokenKwAssign(t));
                t = SpliceAfter(t, new TokenName(t, "xmrFixedArrayAlloc" + suffix));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                for(int i = 0; i < rank; i++)
                {
                    if(i > 0)
                        t = SpliceAfter(t, new TokenKwMul(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                }
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwSemi(t));
                t = SpliceAfter(t, new TokenKwBrcClose(t));

                 //        public integer Length { get {
                 //            return this.len0 * this.len1 * ... ;
                 //        } }
                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "Length"));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));
                t = SpliceAfter(t, new TokenKwGet(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                t = SpliceAfter(t, new TokenKwRet(t));
                for(int i = 0; i < rank; i++)
                {
                    if(i > 0)
                        t = SpliceAfter(t, new TokenKwMul(t));
                    t = SpliceAfter(t, new TokenKwThis(t));
                    t = SpliceAfter(t, new TokenKwDot(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                }
                t = SpliceAfter(t, new TokenKwSemi(t));

                t = SpliceAfter(t, new TokenKwBrcClose(t));
                t = SpliceAfter(t, new TokenKwBrcClose(t));

                 //        public integer Length (integer dim) {
                 //            switch (dim) {
                 //                case 0: return this.len0;
                 //                case 1: return this.len1;
                 //                ...
                 //            }
                 //            return 0;
                 //        }
                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "Length"));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "dim"));
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                t = SpliceAfter(t, new TokenKwSwitch(t));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                t = SpliceAfter(t, new TokenName(t, "dim"));
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                for(int i = 0; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenKwCase(t));
                    t = SpliceAfter(t, new TokenInt(t, i));
                    t = SpliceAfter(t, new TokenKwColon(t));
                    t = SpliceAfter(t, new TokenKwRet(t));
                    t = SpliceAfter(t, new TokenKwThis(t));
                    t = SpliceAfter(t, new TokenKwDot(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                }
                t = SpliceAfter(t, new TokenKwBrcClose(t));

                t = SpliceAfter(t, new TokenKwRet(t));
                t = SpliceAfter(t, new TokenInt(t, 0));
                t = SpliceAfter(t, new TokenKwSemi(t));
                t = SpliceAfter(t, new TokenKwBrcClose(t));

                 //        public integer Index (integer idx0, integet idx1, ...) {
                 //            integer idx = idx0;
                 //            idx *= this.len1; idx += idx1;
                 //            idx *= this.len2; idx += idx2;
                 //            ...
                 //            return idx;
                 //        }
                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "Index"));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                for(int i = 0; i < rank; i++)
                {
                    if(i > 0)
                        t = SpliceAfter(t, new TokenKwComma(t));
                    t = SpliceAfter(t, new TokenTypeInt(t));
                    t = SpliceAfter(t, new TokenName(t, "idx" + i));
                }
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "idx"));
                t = SpliceAfter(t, new TokenKwAssign(t));
                t = SpliceAfter(t, new TokenName(t, "idx0"));
                t = SpliceAfter(t, new TokenKwSemi(t));

                for(int i = 1; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenName(t, "idx"));
                    t = SpliceAfter(t, new TokenKwAsnMul(t));
                    t = SpliceAfter(t, new TokenKwThis(t));
                    t = SpliceAfter(t, new TokenKwDot(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                    t = SpliceAfter(t, new TokenName(t, "idx"));
                    t = SpliceAfter(t, new TokenKwAsnAdd(t));
                    t = SpliceAfter(t, new TokenName(t, "idx" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                }

                t = SpliceAfter(t, new TokenKwRet(t));
                t = SpliceAfter(t, new TokenName(t, "idx"));
                t = SpliceAfter(t, new TokenKwSemi(t));
                t = SpliceAfter(t, new TokenKwBrcClose(t));

                 //        public <oftype> Get (integer idx0, integet idx1, ...) {
                 //            integer idx = idx0;
                 //            idx *= this.len1; idx += idx1;
                 //            idx *= this.len2; idx += idx2;
                 //            ...
                 //            return (<oftype>) xmrFixedArrayGet<suffix> (this.obj, idx);
                 //        }
                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, ofType.CopyToken(t));
                t = SpliceAfter(t, new TokenName(t, "Get"));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                for(int i = 0; i < rank; i++)
                {
                    if(i > 0)
                        t = SpliceAfter(t, new TokenKwComma(t));
                    t = SpliceAfter(t, new TokenTypeInt(t));
                    t = SpliceAfter(t, new TokenName(t, "idx" + i));
                }
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "idx"));
                t = SpliceAfter(t, new TokenKwAssign(t));
                t = SpliceAfter(t, new TokenName(t, "idx0"));
                t = SpliceAfter(t, new TokenKwSemi(t));

                for(int i = 1; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenName(t, "idx"));
                    t = SpliceAfter(t, new TokenKwAsnMul(t));
                    t = SpliceAfter(t, new TokenKwThis(t));
                    t = SpliceAfter(t, new TokenKwDot(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                    t = SpliceAfter(t, new TokenName(t, "idx"));
                    t = SpliceAfter(t, new TokenKwAsnAdd(t));
                    t = SpliceAfter(t, new TokenName(t, "idx" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                }

                t = SpliceAfter(t, new TokenKwRet(t));
                if(suffix == 'O')
                {
                    t = SpliceAfter(t, new TokenKwParOpen(t));
                    t = SpliceAfter(t, ofType.CopyToken(t));
                    t = SpliceAfter(t, new TokenKwParClose(t));
                }
                t = SpliceAfter(t, new TokenName(t, "xmrFixedArrayGet" + suffix));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                t = SpliceAfter(t, new TokenKwThis(t));
                t = SpliceAfter(t, new TokenKwDot(t));
                t = SpliceAfter(t, new TokenName(t, "obj"));
                t = SpliceAfter(t, new TokenKwComma(t));
                t = SpliceAfter(t, new TokenName(t, "idx"));
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwSemi(t));
                t = SpliceAfter(t, new TokenKwBrcClose(t));

                 //        public void Set (integer idx0, integer idx1, ..., <oftype> val) {
                 //            integer idx = idx0;
                 //            idx *= this.len1; idx += idx1;
                 //            idx *= this.len2; idx += idx2;
                 //            ...
                 //            xmrFixedArraySet<suffix> (this.obj, idx, val);
                 //        }
                t = SpliceAfter(t, new TokenKwPublic(t));
                t = SpliceAfter(t, new TokenTypeVoid(t));
                t = SpliceAfter(t, new TokenName(t, "Set"));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                for(int i = 0; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenTypeInt(t));
                    t = SpliceAfter(t, new TokenName(t, "idx" + i));
                    t = SpliceAfter(t, new TokenKwComma(t));
                }
                t = SpliceAfter(t, ofType.CopyToken(t));
                t = SpliceAfter(t, new TokenName(t, "val"));
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwBrcOpen(t));

                t = SpliceAfter(t, new TokenTypeInt(t));
                t = SpliceAfter(t, new TokenName(t, "idx"));
                t = SpliceAfter(t, new TokenKwAssign(t));
                t = SpliceAfter(t, new TokenName(t, "idx0"));
                t = SpliceAfter(t, new TokenKwSemi(t));
                for(int i = 1; i < rank; i++)
                {
                    t = SpliceAfter(t, new TokenName(t, "idx"));
                    t = SpliceAfter(t, new TokenKwAsnMul(t));
                    t = SpliceAfter(t, new TokenKwThis(t));
                    t = SpliceAfter(t, new TokenKwDot(t));
                    t = SpliceAfter(t, new TokenName(t, "len" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                    t = SpliceAfter(t, new TokenName(t, "idx"));
                    t = SpliceAfter(t, new TokenKwAsnAdd(t));
                    t = SpliceAfter(t, new TokenName(t, "idx" + i));
                    t = SpliceAfter(t, new TokenKwSemi(t));
                }

                t = SpliceAfter(t, new TokenName(t, "xmrFixedArraySet" + suffix));
                t = SpliceAfter(t, new TokenKwParOpen(t));
                t = SpliceAfter(t, new TokenKwThis(t));
                t = SpliceAfter(t, new TokenKwDot(t));
                t = SpliceAfter(t, new TokenName(t, "obj"));
                t = SpliceAfter(t, new TokenKwComma(t));
                t = SpliceAfter(t, new TokenName(t, "idx"));
                t = SpliceAfter(t, new TokenKwComma(t));
                t = SpliceAfter(t, new TokenName(t, "val"));
                t = SpliceAfter(t, new TokenKwParClose(t));
                t = SpliceAfter(t, new TokenKwSemi(t));

                t = SpliceAfter(t, new TokenKwBrcClose(t));
                t = SpliceAfter(t, new TokenKwBrcClose(t));
            }
            return fa;
        }
        private Token SpliceAfter(Token before, Token after)
        {
            after.nextToken = before.nextToken;
            after.prevToken = before;
            before.nextToken = after;
            after.nextToken.prevToken = after;
            return after;
        }

        /**
         * @brief Parse script-defined type declarations.
         * @param token = points to possible script-defined type keyword
         * @param outerSDType = null: top-level type
         *                      else: sub-type of this type
         * @param flags = access level (SDT_{PRIVATE,PROTECTED,PUBLIC})
         * @returns true: something defined; else: not a sd type def
         */
        private bool ParseDeclSDTypes(ref Token token, TokenDeclSDType outerSDType, uint flags)
        {
            if(token is not TokenDeclSDType)
                return false;

            TokenDeclSDType decl = (TokenDeclSDType)token;

            /*
             * If declaration of generic type, skip it.
             * The instantiations get parsed (ie, when we know the concrete types)
             * below because they appear as non-generic types.
             */
            if(decl.genParams != null)
            {
                token = decl.endToken.nextToken;
                return true;
            }

            /*
             * Also skip over any typedefs.  They were all processed in 
             * ParseSDTypePreScanPassTwo().
             */
            if(decl is TokenDeclSDTypeTypedef)
            {
                token = decl.endToken.nextToken;
                return true;
            }

            /*
             * Non-generic types get parsed inline because we know all their types.
             */
            if(decl is TokenDeclSDTypeClass)
            {
                ParseDeclClass(ref token, outerSDType, flags);
                return true;
            }
            if(decl is TokenDeclSDTypeDelegate)
            {
                ParseDeclDelegate(ref token, outerSDType, flags);
                return true;
            }
            if(decl is TokenDeclSDTypeInterface)
            {
                ParseDeclInterface(ref token, outerSDType, flags);
                return true;
            }

            throw new Exception("unhandled token " + token.GetType().ToString());
        }

        /**
         * @brief Parse a class declaration.
         * @param token = points to TokenDeclSDTypeClass token
         *                points just past closing '}' on return
         * @param outerSDType = null: this is a top-level class
         *                      else: this class is being defined inside this type
         * @param flags = SDT_{PRIVATE,PROTECTED,PUBLIC}
         */
        private void ParseDeclClass(ref Token token, TokenDeclSDType outerSDType, uint flags)
        {
            bool haveExplicitConstructor = false;
            Token u = token;
            TokenDeclSDTypeClass tokdeclcl;

            tokdeclcl = (TokenDeclSDTypeClass)u;
            tokdeclcl.outerSDType = outerSDType;
            tokdeclcl.accessLevel = flags;
            u = u.nextToken;

            // maybe it is a partial class that had its body snipped out
            // by a later partial class declaration of the same class
            if(tokdeclcl.endToken == tokdeclcl)
            {
                token = u;
                return;
            }

            // make this class the currently compiled class
            // used for retrieving stuff like 'this' possibly
            // in field initialization code
            TokenDeclSDType saveCurSDType = currentDeclSDType;
            currentDeclSDType = tokdeclcl;

            // next can be ':' followed by list of implemented
            // interfaces and one extended class
            if(u is TokenKwColon)
            {
                u = u.nextToken;
                while(true)
                {
                    if(u is TokenTypeSDTypeClass uc)
                    {
                        TokenDeclSDTypeClass c = uc.decl;
                        if(tokdeclcl.extends == null)
                        {
                            tokdeclcl.extends = c;
                        }
                        else if(tokdeclcl.extends != c)
                        {
                            ErrorMsg(u, "can extend from only one class");
                        }
                    }
                    else if(u is TokenTypeSDTypeInterface iu)
                    {
                        TokenDeclSDTypeInterface i = iu.decl;
                        i.AddToClassDecl(tokdeclcl);
                    }
                    else
                    {
                        ErrorMsg(u, "expecting class or interface name");
                        if(u is TokenKwBrcOpen)
                            break;
                    }
                    u = u.nextToken;

                    // allow : in case it is spliced from multiple partial class definitions
                    if(u is not TokenKwComma && u is not TokenKwColon)
                        break;
                    u = u.nextToken;
                }
            }

            // next must be '{' to open class declaration body
            if(u is not TokenKwBrcOpen)
            {
                ErrorMsg(u, "expecting { to open class declaration body");
                token = SkipPastSemi(token);
                goto ret;
            }
            token = u.nextToken;

            // push a var frame to put all the class members in
            tokdeclcl.members.thisClass = tokdeclcl;
            tokenScript.PushVarFrame(tokdeclcl.members);

             // Create a function $instfieldnit to hold all explicit
             // instance field initializations.
            TokenDeclVar ifiFunc = new(tokdeclcl, null, tokenScript);
            ifiFunc.name = new TokenName(ifiFunc, "$instfieldinit");
            ifiFunc.retType = new TokenTypeVoid(ifiFunc);
            ifiFunc.argDecl = new TokenArgDecl(ifiFunc);
            ifiFunc.sdtClass = tokdeclcl;
            ifiFunc.sdtFlags = SDT_PUBLIC | SDT_NEW;
            TokenStmtBlock ifiBody = new(ifiFunc)
            {
                function = ifiFunc
            };
            ifiFunc.body = ifiBody;
            tokdeclcl.instFieldInit = ifiFunc;
            tokenScript.AddVarEntry(ifiFunc);

             // Create a function $staticfieldnit to hold all explicit
             // static field initializations.
            TokenDeclVar sfiFunc = new(tokdeclcl, null, tokenScript);
            sfiFunc.name = new TokenName(sfiFunc, "$staticfieldinit");
            sfiFunc.retType = new TokenTypeVoid(sfiFunc);
            sfiFunc.argDecl = new TokenArgDecl(sfiFunc);
            sfiFunc.sdtClass = tokdeclcl;
            sfiFunc.sdtFlags = SDT_PUBLIC | SDT_STATIC | SDT_NEW;
            TokenStmtBlock sfiBody = new(sfiFunc)
            {
                function = sfiFunc
            };
            sfiFunc.body = sfiBody;
            tokdeclcl.staticFieldInit = sfiFunc;
            tokenScript.AddVarEntry(sfiFunc);

            // process declaration statements until '}'
            while(!(token is TokenKwBrcClose))
            {
                if(token is TokenKwSemi)
                {
                    token = token.nextToken;
                    continue;
                }

                 // Check for all qualifiers.
                 // typedef has an implied 'public' qualifier.
                flags = SDT_PUBLIC;
                if(!(token is TokenDeclSDTypeTypedef))
                {
                    flags = ParseQualifierFlags(ref token);
                }

                 // Parse nested script-defined type definitions.
                if(ParseDeclSDTypes(ref token, tokdeclcl, flags))
                    continue;

                 // constant <name> = <rval> ;
                if(token is TokenKwConst)
                {
                    if((flags & (SDT_ABSTRACT | SDT_NEW | SDT_OVERRIDE | SDT_VIRTUAL)) != 0)
                    {
                        ErrorMsg(token, "cannot have abstract, new, override or virtual field");
                    }
                    TokenDeclVar var = ParseDeclVar(ref token, null);
                    if(var != null)
                    {
                        var.sdtClass = tokdeclcl;
                        var.sdtFlags = flags | SDT_STATIC;
                    }
                    continue;
                }

                 // <type> <name> ;
                 // <type> <name> = <rval> ;
                if((token is TokenType) &&
                    (token.nextToken is TokenName) &&
                    ((token.nextToken.nextToken is TokenKwSemi) ||
                     (token.nextToken.nextToken is TokenKwAssign)))
                {
                    if((flags & (SDT_ABSTRACT | SDT_FINAL | SDT_NEW | SDT_OVERRIDE | SDT_VIRTUAL)) != 0)
                    {
                        ErrorMsg(token, "cannot have abstract, final, new, override or virtual field");
                    }
                    TokenDeclVar var = ParseDeclVar(ref token, ifiFunc);
                    if(var != null)
                    {
                        var.sdtClass = tokdeclcl;
                        var.sdtFlags = flags;
                        if((flags & SDT_STATIC) != 0)
                        {
                            // <type>.<name> = <init>;
                            TokenLValSField left = new(var.init)
                            {
                                baseType = tokdeclcl.MakeRefToken(var),
                                fieldName = var.name
                            };
                            DoVarInit(sfiFunc, left, var.init);
                        }
                        else if(var.init != null)
                        {
                            // this.<name> = <init>;
                            TokenLValIField left = new(var.init)
                            {
                                baseRVal = new TokenRValThis(var.init, tokdeclcl),
                                fieldName = var.name
                            };
                            DoVarInit(ifiFunc, left, var.init);
                        }
                    }
                    continue;
                }

                 // <type> <name> [ : <implintfs> ] { [ get { <body> } ] [ set { <body> } ] }
                 // <type> '[' ... ']' [ : <implintfs> ] { [ get { <body> } ] [ set { <body> } ] }
                bool prop = (token is TokenType) &&
                    (token.nextToken is TokenName) &&
                    (token.nextToken.nextToken is TokenKwBrcOpen ||
                     token.nextToken.nextToken is TokenKwColon);
                prop |= (token is TokenType) && (token.nextToken is TokenKwBrkOpen);
                if(prop)
                {
                    TokenDeclVar var = ParseProperty(ref token, (flags & SDT_ABSTRACT) != 0, true);
                    if(var != null)
                    {
                        var.sdtClass = tokdeclcl;
                        var.sdtFlags = flags;
                        if(var.getProp != null)
                        {
                            var.getProp.sdtClass = tokdeclcl;
                            var.getProp.sdtFlags = flags;
                        }
                        if(var.setProp != null)
                        {
                            var.setProp.sdtClass = tokdeclcl;
                            var.setProp.sdtFlags = flags;
                        }
                    }
                    continue;
                }

                 // 'constructor' '(' arglist ')' [ ':' [ 'base' ] '(' baseconstructorcall ')' ] '{' body '}'
                if(token is TokenKwConstructor)
                {
                    ParseSDTClassCtorDecl(ref token, flags, tokdeclcl);
                    haveExplicitConstructor = true;
                    continue;
                }

                 // <type> <name> <funcargs> <funcbody>
                 // method with explicit return type
                if(token is TokenType)
                {
                    ParseSDTClassMethodDecl(ref token, flags, tokdeclcl);
                    continue;
                }

                 // <name> <funcargs> <funcbody>
                 // method returning void
                if((token is TokenName) || (token is TokenKw kwt && kwt.sdtClassOp))
                {
                    ParseSDTClassMethodDecl(ref token, flags, tokdeclcl);
                    continue;
                }

                 // That's all we support in a class declaration.
                ErrorMsg(token, "expecting field or method declaration");
                token = SkipPastSemi(token);
            }

             // If script didn't specify any constructor, create a default no-argument one.
            if(!haveExplicitConstructor)
            {
                TokenDeclVar tokenDeclFunc = new(token, null, tokenScript)
                {
                    name = new TokenName(token, "$ctor"),
                    retType = new TokenTypeVoid(token),
                    argDecl = new TokenArgDecl(token),
                    sdtClass = tokdeclcl,
                    sdtFlags = SDT_PUBLIC | SDT_NEW,
                    body = new TokenStmtBlock(token)
                };
                tokenDeclFunc.body.function = tokenDeclFunc;

                if(tokdeclcl.extends != null)
                {
                    SetUpDefaultBaseCtorCall(tokenDeclFunc);
                }
                else
                {
                    // default constructor that doesn't do anything is trivial
                    tokenDeclFunc.triviality = Triviality.trivial;
                }

                tokenScript.AddVarEntry(tokenDeclFunc);
            }

             // Skip over the closing brace and pop corresponding var frame.
            token = token.nextToken;
            tokenScript.PopVarFrame();
            ret:
            currentDeclSDType = saveCurSDType;
        }

        /**
         * @brief Parse out abstract/override/private/protected/public/static/virtual keywords.
         * @param token = first token to evaluate
         * @returns flags found; token = unprocessed token
         */
        private Dictionary<uint, Token> foundFlags = new();
        private uint ParseQualifierFlags(ref Token token)
        {
            foundFlags.Clear();
            while(true)
            {
                if(token is TokenKwPrivate)
                {
                    token = AddQualifierFlag(token, SDT_PRIVATE, SDT_PROTECTED | SDT_PUBLIC);
                    continue;
                }
                if(token is TokenKwProtected)
                {
                    token = AddQualifierFlag(token, SDT_PROTECTED, SDT_PRIVATE | SDT_PUBLIC);
                    continue;
                }
                if(token is TokenKwPublic)
                {
                    token = AddQualifierFlag(token, SDT_PUBLIC, SDT_PRIVATE | SDT_PROTECTED);
                    continue;
                }
                if(token is TokenKwAbstract)
                {
                    token = AddQualifierFlag(token, SDT_ABSTRACT, SDT_FINAL | SDT_STATIC | SDT_VIRTUAL);
                    continue;
                }
                if(token is TokenKwFinal)
                {
                    token = AddQualifierFlag(token, SDT_FINAL, SDT_ABSTRACT | SDT_VIRTUAL);
                    continue;
                }
                if(token is TokenKwNew)
                {
                    token = AddQualifierFlag(token, SDT_NEW, SDT_OVERRIDE);
                    continue;
                }
                if(token is TokenKwOverride)
                {
                    token = AddQualifierFlag(token, SDT_OVERRIDE, SDT_NEW | SDT_STATIC);
                    continue;
                }
                if(token is TokenKwStatic)
                {
                    token = AddQualifierFlag(token, SDT_STATIC, SDT_ABSTRACT | SDT_OVERRIDE | SDT_VIRTUAL);
                    continue;
                }
                if(token is TokenKwVirtual)
                {
                    token = AddQualifierFlag(token, SDT_VIRTUAL, SDT_ABSTRACT | SDT_STATIC);
                    continue;
                }
                break;
            }

            uint flags = 0;
            foreach(uint flag in foundFlags.Keys)
                flags |= flag;

            if((flags & (SDT_PRIVATE | SDT_PROTECTED | SDT_PUBLIC)) == 0)
                ErrorMsg(token, "must specify exactly one of private, protected or public");

            return flags;
        }
        private Token AddQualifierFlag(Token token, uint add, uint confs)
        {
            while(confs != 0)
            {
                uint conf = (uint)(confs & -confs);
                if (foundFlags.TryGetValue(conf, out Token confToken))
                {
                    ErrorMsg(token, "conflicts with " + confToken.ToString());
                }
                confs -= conf;
            }
            foundFlags[add] = token;
            return token.nextToken;
        }

        /**
         * @brief Parse a property declaration.
         * @param token = points to the property type token on entry
         *                points just past the closing brace on return
         * @param abs = true: property is abstract
         *             false: property is concrete
         * @param imp = allow implemented interface specs
         * @returns null: parse failure
         *          else: property
         *
         * <type> <name> [ : <implintfs> ] { [ get { <body> } ] [ set { <body> } ] }
         * <type> '[' ... ']' [ : <implintfs> ] { [ get { <body> } ] [ set { <body> } ] }
         */
        private TokenDeclVar ParseProperty(ref Token token, bool abs, bool imp)
        {
             // Parse out the property's type and name.
             //   <type> <name>
            TokenType type = (TokenType)token;
            TokenName name;
            TokenArgDecl args;
            Token argTokens = null;
            token = token.nextToken;
            if(token is TokenKwBrkOpen)
            {
                argTokens = token;
                name = new TokenName(token, "$idxprop");
                args = ParseFuncArgs(ref token, typeof(TokenKwBrkClose));
            }
            else
            {
                name = (TokenName)token;
                token = token.nextToken;
                args = new TokenArgDecl(token);
            }

             // Maybe it claims to implement some interface properties.
             //   [ ':' <ifacetype>[.<propname>] ',' ... ]
            TokenIntfImpl implements = null;
            if(token is TokenKwColon)
            {
                implements = ParseImplements(ref token, name);
                if(implements == null)
                    return null;
                if(!imp)
                {
                    ErrorMsg(token, "cannot implement interface property");
                }
            }

             // Should have an opening brace.
            if(token is not TokenKwBrcOpen)
            {
                ErrorMsg(token, "expect { to open property definition");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;

             // Parse out the getter and/or setter.
             //   'get' { <body> | ';' }
             //   'set' { <body> | ';' }
            TokenDeclVar getFunc = null;
            TokenDeclVar setFunc = null;
            while(token is not TokenKwBrcClose)
            {
                 // Maybe create a getter function.
                if(token is TokenKwGet)
                {
                    getFunc = new TokenDeclVar(token, null, tokenScript)
                    {
                        name = new TokenName(token, name.val + "$get"),
                        retType = type,
                        argDecl = args,
                        implements = MakePropertyImplements(implements, "$get")
                    };

                    token = token.nextToken;
                    if(!ParseFunctionBody(ref token, getFunc, abs))
                    {
                        getFunc = null;
                    }
                    else if(!tokenScript.AddVarEntry(getFunc))
                    {
                        ErrorMsg(getFunc, "duplicate getter");
                    }
                    continue;
                }

                 // Maybe create a setter function.
                if(token is TokenKwSet)
                {
                    TokenArgDecl argDecl = args;
                    if(getFunc != null)
                    {
                        argDecl = (argTokens == null) ? new TokenArgDecl(token) :
                            ParseFuncArgs(ref argTokens, typeof(TokenKwBrkClose));
                    }
                    argDecl.AddArg(type, new TokenName(token, "value"));

                    setFunc = new TokenDeclVar(token, null, tokenScript)
                    {
                        name = new TokenName(token, name.val + "$set"),
                        retType = new TokenTypeVoid(token),
                        argDecl = argDecl,
                        implements = MakePropertyImplements(implements, "$set")
                    };

                    token = token.nextToken;
                    if(!ParseFunctionBody(ref token, setFunc, abs))
                    {
                        setFunc = null;
                    }
                    else if(!tokenScript.AddVarEntry(setFunc))
                    {
                        ErrorMsg(setFunc, "duplicate setter");
                    }
                    continue;
                }

                ErrorMsg(token, "expecting get or set");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;

            if((getFunc == null) && (setFunc == null))
            {
                ErrorMsg(name, "must specify at least one of get, set");
                return null;
            }

            // Set up a variable for the property.
            TokenDeclVar tokenDeclVar = new(name, null, tokenScript)
            {
                type = type,
                name = name,
                getProp = getFunc,
                setProp = setFunc
            };

            // Can't be same name already in block.
            if (!tokenScript.AddVarEntry(tokenDeclVar))
            {
                ErrorMsg(tokenDeclVar, "duplicate member " + name.val);
                return null;
            }
            return tokenDeclVar;
        }

        /**
         * @brief Given a list of implemented interface methods, create a similar list with suffix added to all the names
         * @param implements = original list of implemented interface methods
         * @param suffix = string to be added to end of implemented interface method names
         * @returns list similar to implements with suffix added to end of implemented interface method names
         */
        private TokenIntfImpl MakePropertyImplements(TokenIntfImpl implements, string suffix)
        {
            TokenIntfImpl gsimpls = null;
            for(TokenIntfImpl impl = implements; impl != null; impl = (TokenIntfImpl)impl.nextToken)
            {
                TokenIntfImpl gsimpl = new(impl.intfType, new TokenName(impl.methName, impl.methName.val + suffix))
                {
                    nextToken = gsimpls
                };
                gsimpls = gsimpl;
            }
            return gsimpls;
        }

        /**
         * @brief Parse a constructor definition for a script-defined type class.
         * @param token = points to 'constructor' keyword
         * @param flags = abstract/override/static/virtual flags
         * @param tokdeclcl = which script-defined type class this method is in
         * @returns with method parsed and cataloged (or error message(s) printed)
         */
        private void ParseSDTClassCtorDecl(ref Token token, uint flags, TokenDeclSDTypeClass tokdeclcl)
        {
            if((flags & (SDT_ABSTRACT | SDT_OVERRIDE | SDT_STATIC | SDT_VIRTUAL)) != 0)
            {
                ErrorMsg(token, "cannot have abstract, override, static or virtual constructor");
            }

            TokenDeclVar tokenDeclFunc = new(token, null, tokenScript);
            tokenDeclFunc.name = new TokenName(tokenDeclFunc, "$ctor");
            tokenDeclFunc.retType = new TokenTypeVoid(token);
            tokenDeclFunc.sdtClass = tokdeclcl;
            tokenDeclFunc.sdtFlags = flags | SDT_NEW;

            token = token.nextToken;
            if(token is not TokenKwParOpen)
            {
                ErrorMsg(token, "expecting ( for constructor argument list");
                token = SkipPastSemi(token);
                return;
            }
            tokenDeclFunc.argDecl = ParseFuncArgs(ref token, typeof(TokenKwParClose));
            if(tokenDeclFunc.argDecl == null)
                return;

            TokenDeclVar saveDeclFunc = currentDeclFunc;
            currentDeclFunc = tokenDeclFunc;
            tokenScript.PushVarFrame(tokenDeclFunc.argDecl.varDict);
            try
            {
                 // Set up reference to base constructor.
                TokenLValBaseField baseCtor = new(token, new TokenName(token, "$ctor"), tokdeclcl);

                 // Parse any base constructor call as if it were the first statement of the
                 // constructor itself.
                if(token is TokenKwColon)
                {
                    token = token.nextToken;
                    if(token is TokenKwBase)
                    {
                        token = token.nextToken;
                    }
                    if(token is not TokenKwParOpen)
                    {
                        ErrorMsg(token, "expecting ( for base constructor call arguments");
                        token = SkipPastSemi(token);
                        return;
                    }
                    TokenRValCall rvc = ParseRValCall(ref token, baseCtor);
                    if(rvc == null)
                        return;
                    if(tokdeclcl.extends != null)
                    {
                        tokenDeclFunc.baseCtorCall = rvc;
                        tokenDeclFunc.unknownTrivialityCalls.AddLast(rvc);
                    }
                    else
                    {
                        ErrorMsg(rvc, "base constructor call cannot be specified if not extending anything");
                    }
                }
                else if(tokdeclcl.extends != null)
                {
                     // Caller didn't specify a constructor but we are extending, so we will 
                     // call the extended class's default constructor.
                    SetUpDefaultBaseCtorCall(tokenDeclFunc);
                }

                 // Parse the constructor body.
                tokenDeclFunc.body = ParseStmtBlock(ref token);
                if(tokenDeclFunc.body == null)
                    return;
                if(tokenDeclFunc.argDecl == null)
                    return;
            }
            finally
            {
                tokenScript.PopVarFrame();
                currentDeclFunc = saveDeclFunc;
            }

             // Add to list of methods defined by this class.
             // It has the name "$ctor(argsig)".
            if(!tokenScript.AddVarEntry(tokenDeclFunc))
            {
                ErrorMsg(tokenDeclFunc, "duplicate constructor definition");
            }
        }

        /**
         * @brief Set up a call from a constructor to its default base constructor.
         */
        private void SetUpDefaultBaseCtorCall(TokenDeclVar thisCtor)
        {
            TokenLValBaseField baseCtor = new(thisCtor, new TokenName(thisCtor, "$ctor"), (TokenDeclSDTypeClass)thisCtor.sdtClass);
            TokenRValCall rvc = new(thisCtor)
            {
                meth = baseCtor
            };
            thisCtor.baseCtorCall = rvc;
            thisCtor.unknownTrivialityCalls.AddLast(rvc);
        }

        /**
         * @brief Parse a method definition for a script-defined type class.
         * @param token = points to return type (or method name for implicit return type of void)
         * @param flags = abstract/override/static/virtual flags
         * @param tokdeclcl = which script-defined type class this method is in
         * @returns with method parsed and cataloged (or error message(s) printed)
         */
        private void ParseSDTClassMethodDecl(ref Token token, uint flags, TokenDeclSDTypeClass tokdeclcl)
        {
            TokenDeclVar tokenDeclFunc = ParseDeclFunc(ref token,
                                                        (flags & SDT_ABSTRACT) != 0,
                                                        (flags & SDT_STATIC) == 0,
                                                        (flags & SDT_STATIC) == 0);
            if(tokenDeclFunc != null)
            {
                tokenDeclFunc.sdtClass = tokdeclcl;
                tokenDeclFunc.sdtFlags = flags;
                if(!tokenScript.AddVarEntry(tokenDeclFunc))
                {
                    string funcNameSig = tokenDeclFunc.funcNameSig.val;
                    ErrorMsg(tokenDeclFunc.funcNameSig, "duplicate method name " + funcNameSig);
                }
            }
        }

        /**
         * @brief Parse a delegate declaration statement.
         * @param token = points to TokenDeclSDTypeDelegate token on entry
         *                points just past ';' on return
         * @param outerSDType = null: this is a top-level delegate
         *                      else: this delegate is being defined inside this type
         * @param flags = SDT_{PRIVATE,PROTECTED,PUBLIC}
         */
        private void ParseDeclDelegate(ref Token token, TokenDeclSDType outerSDType, uint flags)
        {
            Token u = token;
            TokenDeclSDTypeDelegate tokdecldel;
            TokenType retType;

            tokdecldel = (TokenDeclSDTypeDelegate)u;
            tokdecldel.outerSDType = outerSDType;
            tokdecldel.accessLevel = flags;

            // first thing following that should be return type
            // but we will fill in 'void' if it is missing
            u = u.nextToken;
            if(u is TokenType tu)
            {
                retType = tu;
                u = u.nextToken;
            }
            else
            {
                retType = new TokenTypeVoid(u);
            }

            // get list of argument types until we see a ')'
            List<TokenType> args = new();
            bool first = true;
            do
            {
                if(first)
                {

                    // first time should have '(' ')' or '(' <type>
                    if(u is not TokenKwParOpen)
                    {
                        ErrorMsg(u, "expecting ( after delegate name");
                        token = SkipPastSemi(token);
                        return;
                    }
                    first = false;
                    u = u.nextToken;
                    if(u is TokenKwParClose)
                        break;
                }
                else
                {
                    // other times should have ',' <type>
                    if(u is not TokenKwComma)
                    {
                        ErrorMsg(u, "expecting , separating arg types");
                        token = SkipPastSemi(token);
                        return;
                    }
                    u = u.nextToken;
                }
                if(u is not TokenType)
                {
                    ErrorMsg(u, "expecting argument type");
                    token = SkipPastSemi(token);
                    return;
                }
                args.Add((TokenType)u);
                u = u.nextToken;

                // they can put in a dummy name that we toss out
                if(u is TokenName)
                    u = u.nextToken;

                // scanning ends on a ')'
            } while(u is not TokenKwParClose);

            // fill in the return type and argment type array
            tokdecldel.SetRetArgTypes(retType, args.ToArray());

            // and finally must have ';' to finish the delegate declaration statement
            u = u.nextToken;
            if(u is not TokenKwSemi)
            {
                ErrorMsg(u, "expecting ; after ) in delegate");
                token = SkipPastSemi(token);
                return;
            }
            token = u.nextToken;
        }

        /**
         * @brief Parse an interface declaration.
         * @param token = points to TokenDeclSDTypeInterface token on entry
         *                points just past closing '}' on return
         * @param outerSDType = null: this is a top-level interface
         *                      else: this interface is being defined inside this type
         * @param flags = SDT_{PRIVATE,PROTECTED,PUBLIC}
         */
        private void ParseDeclInterface(ref Token token, TokenDeclSDType outerSDType, uint flags)
        {
            Token u = token;
            TokenDeclSDTypeInterface tokdeclin;

            tokdeclin = (TokenDeclSDTypeInterface)u;
            tokdeclin.outerSDType = outerSDType;
            tokdeclin.accessLevel = flags;
            u = u.nextToken;

            // next can be ':' followed by list of implemented interfaces
            if(u is TokenKwColon)
            {
                u = u.nextToken;
                while(true)
                {
                    if(u is TokenTypeSDTypeInterface iu)
                    {
                        TokenDeclSDTypeInterface i = iu.decl;
                        if(!tokdeclin.implements.Contains(i))
                        {
                            tokdeclin.implements.Add(i);
                        }
                    }
                    else
                    {
                        ErrorMsg(u, "expecting interface name");
                        if(u is TokenKwBrcOpen)
                            break;
                    }
                    u = u.nextToken;
                    if(u is not TokenKwComma)
                        break;
                    u = u.nextToken;
                }
            }

            // next must be '{' to open interface declaration body
            if(u is not TokenKwBrcOpen)
            {
                ErrorMsg(u, "expecting { to open interface declaration body");
                token = SkipPastSemi(token);
                return;
            }
            token = u.nextToken;

            // start a var definition frame to collect the interface members
            tokenScript.PushVarFrame(false);
            tokdeclin.methsNProps = tokenScript.variablesStack;

            // process declaration statements until '}'
            while(token is not TokenKwBrcClose)
            {
                if(token is TokenKwSemi)
                {
                    token = token.nextToken;
                    continue;
                }

                 // Parse nested script-defined type definitions.
                if(ParseDeclSDTypes(ref token, tokdeclin, SDT_PUBLIC))
                    continue;

                 // <type> <name> <funcargs> ;
                 // abstract method with explicit return type
                if((token is TokenType) &&
                    (token.nextToken is TokenName) &&
                    (token.nextToken.nextToken is TokenKwParOpen))
                {
                    Token name = token.nextToken;
                    TokenDeclVar tokenDeclFunc = ParseDeclFunc(ref token, true, false, false);
                    if(tokenDeclFunc == null)
                        continue;
                    if(!tokenScript.AddVarEntry(tokenDeclFunc))
                    {
                        ErrorMsg(name, "duplicate method name");
                        continue;
                    }
                    continue;
                }

                 // <name> <funcargs> ;
                 // abstract method returning void
                if((token is TokenName) &&
                    (token.nextToken is TokenKwParOpen))
                {
                    Token name = token;
                    TokenDeclVar tokenDeclFunc = ParseDeclFunc(ref token, true, false, false);
                    if(tokenDeclFunc == null)
                        continue;
                    if(!tokenScript.AddVarEntry(tokenDeclFunc))
                    {
                        ErrorMsg(name, "duplicate method name");
                    }
                    continue;
                }

                 // <type> <name> { [ get ; ] [ set ; ] }
                 // <type> '[' ... ']' { [ get ; ] [ set ; ] }
                 // abstract property
                bool prop = (token is TokenType) &&
                    (token.nextToken is TokenName) &&
                    (token.nextToken.nextToken is TokenKwBrcOpen ||
                     token.nextToken.nextToken is TokenKwColon);
                prop |= (token is TokenType) && (token.nextToken is TokenKwBrkOpen);
                if(prop)
                {
                    ParseProperty(ref token, true, false);
                    continue;
                }

                 // That's all we support in an interface declaration.
                ErrorMsg(token, "expecting method or property prototype");
                token = SkipPastSemi(token);
            }

             // Skip over the closing brace and pop the corresponding var frame.
            token = token.nextToken;
            tokenScript.PopVarFrame();
        }

        /**
         * @brief parse state body (including all its event handlers)
         * @param token = points to TokenKwBrcOpen
         * @returns null: state body parse error
         *          else: token representing state
         *          token = points past close brace
         */
        private TokenStateBody ParseStateBody(ref Token token)
        {
            TokenStateBody tokenStateBody = new(token);

            if(token is not TokenKwBrcOpen)
            {
                ErrorMsg(token, "expecting { at beg of state");
                token = SkipPastSemi(token);
                return null;
            }

            token = token.nextToken;
            while(token is not TokenKwBrcClose)
            {
                if(token is TokenEnd)
                {
                    ErrorMsg(tokenStateBody, "eof parsing state body");
                    return null;
                }
                TokenDeclVar tokenDeclFunc = ParseDeclFunc(ref token, false, false, false);
                if(tokenDeclFunc == null)
                    return null;
                if(tokenDeclFunc.retType is not TokenTypeVoid)
                {
                    ErrorMsg(tokenDeclFunc.retType, "event handlers don't have return types");
                    return null;
                }
                tokenDeclFunc.nextToken = tokenStateBody.eventFuncs;
                tokenStateBody.eventFuncs = tokenDeclFunc;
            }
            token = token.nextToken;
            return tokenStateBody;
        }

        /**
         * @brief Parse a function declaration, including its arg list and body
         * @param token = points to function return type token (or function name token if return type void)
         * @param abs = false: concrete function; true: abstract declaration
         * @param imp = allow implemented interface specs
         * @param ops = accept operators (==, +, etc) for function name
         * @returns null: error parsing function definition
         *          else: function declaration
         *          token = advanced just past function, ie, just past the closing brace
         */
        private TokenDeclVar ParseDeclFunc(ref Token token, bool abs, bool imp, bool ops)
        {
            if(token is TokenType retType)
                token = token.nextToken;
            else
                retType = new TokenTypeVoid(token);

            TokenName simpleName;
            if((token is TokenKw Kwt) && Kwt.sdtClassOp)
            {
                if(!ops)
                    ErrorMsg(token, "operator functions disallowed in static contexts");
                simpleName = new TokenName(token, "$op" + token.ToString());
            }
            else if(token is not TokenName tname)
            {
                ErrorMsg(token, "expecting function name");
                token = SkipPastSemi(token);
                return null;
            }
            else
            {
                simpleName = tname;
            }
            token = token.nextToken;

            return ParseDeclFunc(ref token, abs, imp, retType, simpleName);
        }

        /**
         * @brief Parse a function declaration, including its arg list and body
         *        This version enters with token pointing to the '(' at beginning of arg list
         * @param token = points to the '(' of the arg list
         * @param abs = false: concrete function; true: abstract declaration
         * @param imp = allow implemented interface specs
         * @param retType = return type (TokenTypeVoid if void, never null)
         * @param simpleName = function name without any signature
         * @returns null: error parsing remainder of function definition
         *          else: function declaration
         *          token = advanced just past function, ie, just past the closing brace
         */
        private TokenDeclVar ParseDeclFunc(ref Token token, bool abs, bool imp, TokenType retType, TokenName simpleName)
        {
            TokenDeclVar tokenDeclFunc = new(simpleName, null, tokenScript)
            {
                name = simpleName,
                retType = retType,
                argDecl = ParseFuncArgs(ref token, typeof(TokenKwParClose))
            };
            if (tokenDeclFunc.argDecl == null)
                return null;

            if(token is TokenKwColon)
            {
                tokenDeclFunc.implements = ParseImplements(ref token, simpleName);
                if(tokenDeclFunc.implements == null)
                    return null;
                if(!imp)
                {
                    ErrorMsg(tokenDeclFunc.implements, "cannot implement interface method");
                    tokenDeclFunc.implements = null;
                }
            }

            if(!ParseFunctionBody(ref token, tokenDeclFunc, abs))
                return null;
            if(tokenDeclFunc.argDecl == null)
                return null;
            return tokenDeclFunc;
        }

        /**
         * @brief Parse interface implementation list.
         * @param token = points to ':' on entry
         *                points just past list on return
         * @param simpleName = simple name (no arg signature) of method/property that 
         *                     is implementing the interface method/property
         * @returns list of implemented interface methods/properties
         */
        private TokenIntfImpl ParseImplements(ref Token token, TokenName simpleName)
        {
            TokenIntfImpl implements = null;
            do
            {
                token = token.nextToken;
                if(token is not TokenTypeSDTypeInterface)
                {
                    ErrorMsg(token, "expecting interface type");
                    token = SkipPastSemi(token);
                    return null;
                }
                TokenTypeSDTypeInterface intfType = (TokenTypeSDTypeInterface)token;
                token = token.nextToken;
                TokenName methName = simpleName;
                if((token is TokenKwDot) && (token.nextToken is TokenName tname))
                {
                    methName = tname;
                    token = token.nextToken.nextToken;
                }
                TokenIntfImpl intfImpl = new(intfType, methName)
                {
                    nextToken = implements
                };
                implements = intfImpl;
            } while(token is TokenKwComma);
            return implements;
        }


        /**
         * @brief Parse function declaration's body
         * @param token = points to body, ie, ';' or '{'
         * @param tokenDeclFunc = function being declared
         * @param abs = false: concrete function; true: abstract declaration
         * @returns whether or not the function definition parsed correctly
         */
        private bool ParseFunctionBody(ref Token token, TokenDeclVar tokenDeclFunc, bool abs)
        {
            if(token is TokenKwSemi)
            {
                if(!abs)
                {
                    ErrorMsg(token, "concrete function must have body");
                    token = SkipPastSemi(token);
                    return false;
                }
                token = token.nextToken;
                return true;
            }

             // Declare this function as being the one currently being processed
             // for anything that cares.  We also start a variable frame that 
             // includes all the declared parameters.
            TokenDeclVar saveDeclFunc = currentDeclFunc;
            currentDeclFunc = tokenDeclFunc;
            tokenScript.PushVarFrame(tokenDeclFunc.argDecl.varDict);

             // Now parse the function statement block.
            tokenDeclFunc.body = ParseStmtBlock(ref token);

             // Pop the var frame that contains the arguments.
            tokenScript.PopVarFrame();
            currentDeclFunc = saveDeclFunc;

             // Check final errors.
            if(tokenDeclFunc.body == null)
                return false;
            if(abs)
            {
                ErrorMsg(tokenDeclFunc.body, "abstract function must not have body");
                tokenDeclFunc.body = null;
                return false;
            }
            return true;
        }


        /**
         * @brief Parse statement
         * @param token = first token of statement
         * @returns null: parse error
         *          else: token representing whole statement
         *          token = points past statement
         */
        private TokenStmt ParseStmt(ref Token token)
        {
             // Statements that begin with a specific keyword.
            if(token is TokenKwAt)
                return ParseStmtLabel(ref token);
            if(token is TokenKwBrcOpen)
                return ParseStmtBlock(ref token);
            if(token is TokenKwBreak)
                return ParseStmtBreak(ref token);
            if(token is TokenKwCont)
                return ParseStmtCont(ref token);
            if(token is TokenKwDo)
                return ParseStmtDo(ref token);
            if(token is TokenKwFor)
                return ParseStmtFor(ref token);
            if(token is TokenKwForEach)
                return ParseStmtForEach(ref token);
            if(token is TokenKwIf)
                return ParseStmtIf(ref token);
            if(token is TokenKwJump)
                return ParseStmtJump(ref token);
            if(token is TokenKwRet)
                return ParseStmtRet(ref token);
            if(token is TokenKwSemi)
                return ParseStmtNull(ref token);
            if(token is TokenKwState)
                return ParseStmtState(ref token);
            if(token is TokenKwSwitch)
                return ParseStmtSwitch(ref token);
            if(token is TokenKwThrow)
                return ParseStmtThrow(ref token);
            if(token is TokenKwTry)
                return ParseStmtTry(ref token);
            if(token is TokenKwWhile)
                return ParseStmtWhile(ref token);

             // Try to parse anything else as an expression, possibly calling
             // something and/or writing to a variable.
            TokenRVal tokenRVal = ParseRVal(ref token, semiOnly);
            if(tokenRVal != null)
            {
                TokenStmtRVal tokenStmtRVal = new(tokenRVal) { rVal = tokenRVal };
                return tokenStmtRVal;
            }

             // Who knows what it is...
            ErrorMsg(token, "unknown statement");
            token = SkipPastSemi(token);
            return null;
        }

        /**
         * @brief parse a statement block, ie, group of statements between braces
         * @param token = points to { token
         * @returns null: error parsing
         *          else: statements bundled in this token
         *          token = advanced just past the } token
         */
        private TokenStmtBlock ParseStmtBlock(ref Token token)
        {
            if(token is not TokenKwBrcOpen)
            {
                ErrorMsg(token, "statement block body must begin with a {");
                token = SkipPastSemi(token);
                return null;
            }
            TokenStmtBlock tokenStmtBlock = new(token)
            {
                function = currentDeclFunc,
                outerStmtBlock = currentStmtBlock
            };
            currentStmtBlock = tokenStmtBlock;
            VarDict outerVariablesStack = tokenScript.variablesStack;
            try
            {
                Token prevStmt = null;
                token = token.nextToken;
                while(token is not TokenKwBrcClose)
                {
                    if(token is TokenEnd)
                    {
                        ErrorMsg(tokenStmtBlock, "missing }");
                        return null;
                    }
                    Token thisStmt;
                    if(((token is TokenType) && (token.nextToken is TokenName)) ||
                        (token is TokenKwConst))
                    {
                        thisStmt = ParseDeclVar(ref token, null);
                    }
                    else
                    {
                        thisStmt = ParseStmt(ref token);
                    }
                    if(thisStmt == null)
                        return null;
                    if(prevStmt == null)
                        tokenStmtBlock.statements = thisStmt;
                    else
                        prevStmt.nextToken = thisStmt;
                    prevStmt = thisStmt;
                }
                token = token.nextToken;
            }
            finally
            {
                tokenScript.variablesStack = outerVariablesStack;
                currentStmtBlock = tokenStmtBlock.outerStmtBlock;
            }
            return tokenStmtBlock;
        }

        /**
         * @brief parse a 'break' statement
         * @param token = points to break keyword token
         * @returns null: error parsing
         *          else: statements bundled in this token
         *          token = advanced just past the ; token
         */
        private TokenStmtBreak ParseStmtBreak(ref Token token)
        {
            TokenStmtBreak tokenStmtBreak = new(token);
            token = token.nextToken;
            if(token is not TokenKwSemi)
            {
                ErrorMsg(token, "expecting ;");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            return tokenStmtBreak;
        }

        /**
         * @brief parse a 'continue' statement
         * @param token = points to continue keyword token
         * @returns null: error parsing
         *          else: statements bundled in this token
         *          token = advanced just past the ; token
         */
        private TokenStmtCont ParseStmtCont(ref Token token)
        {
            TokenStmtCont tokenStmtCont = new(token);
            token = token.nextToken;
            if(token is not TokenKwSemi)
            {
                ErrorMsg(token, "expecting ;");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            return tokenStmtCont;
        }

        /**
         * @brief parse a 'do' statement
         * @params token = points to 'do' keyword token
         * @returns null: parse error
         *          else: pointer to token encapsulating the do statement, including body
         *          token = advanced just past the body statement
         */
        private TokenStmtDo ParseStmtDo(ref Token token)
        {
            currentDeclFunc.triviality = Triviality.complex;
            TokenStmtDo tokenStmtDo = new(token);
            token = token.nextToken;
            tokenStmtDo.bodyStmt = ParseStmt(ref token);
            if(tokenStmtDo.bodyStmt == null)
                return null;
            if(token is not TokenKwWhile)
            {
                ErrorMsg(token, "expecting while clause");
                return null;
            }
            token = token.nextToken;
            tokenStmtDo.testRVal = ParseRValParen(ref token);
            if(tokenStmtDo.testRVal == null)
                return null;
            if(token is not TokenKwSemi)
            {
                ErrorMsg(token, "while clause must terminate on semicolon");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            return tokenStmtDo;
        }

        /**
         * @brief parse a for statement
         * @param token = points to 'for' keyword token
         * @returns null: parse error
         *          else: pointer to encapsulated for statement token
         *          token = advanced just past for body statement
         */
        private TokenStmt ParseStmtFor(ref Token token)
        {
            currentDeclFunc.triviality = Triviality.complex;

             // Create encapsulating token and skip past 'for ('
            TokenStmtFor tokenStmtFor = new(token);
            token = token.nextToken;
            if(token is not TokenKwParOpen)
            {
                ErrorMsg(token, "for must be followed by (");
                return null;
            }
            token = token.nextToken;

             // If a plain for, ie, not declaring a variable, it's straightforward.
            if(token is not TokenType)
            {
                tokenStmtFor.initStmt = ParseStmt(ref token);
                if(tokenStmtFor.initStmt == null)
                    return null;
                return ParseStmtFor2(tokenStmtFor, ref token) ? tokenStmtFor : null;
            }

            // Initialization declares a variable, so encapsulate it in a block so
            // variable has scope only in the for statement, including its body.
            TokenStmtBlock forStmtBlock = new(tokenStmtFor)
            {
                outerStmtBlock = currentStmtBlock,
                function = currentDeclFunc
            };
            currentStmtBlock = forStmtBlock;
            tokenScript.PushVarFrame(true);

            TokenDeclVar tokenDeclVar = ParseDeclVar(ref token, null);
            if(tokenDeclVar == null)
            {
                tokenScript.PopVarFrame();
                currentStmtBlock = forStmtBlock.outerStmtBlock;
                return null;
            }

            forStmtBlock.statements = tokenDeclVar;
            tokenDeclVar.nextToken = tokenStmtFor;

            bool ok = ParseStmtFor2(tokenStmtFor, ref token);
            tokenScript.PopVarFrame();
            currentStmtBlock = forStmtBlock.outerStmtBlock;
            return ok ? forStmtBlock : null;
        }

        /**
         * @brief parse rest of 'for' statement starting with the test expression.
         * @param tokenStmtFor = token encapsulating the for statement
         * @param token = points to test expression
         * @returns false: parse error
         *           true: successful
         *          token = points just past body statement
         */
        private bool ParseStmtFor2(TokenStmtFor tokenStmtFor, ref Token token)
        {
            if(token is TokenKwSemi)
            {
                token = token.nextToken;
            }
            else
            {
                tokenStmtFor.testRVal = ParseRVal(ref token, semiOnly);
                if(tokenStmtFor.testRVal == null)
                    return false;
            }
            if(token is TokenKwParClose)
            {
                token = token.nextToken;
            }
            else
            {
                tokenStmtFor.incrRVal = ParseRVal(ref token, parCloseOnly);
                if(tokenStmtFor.incrRVal == null)
                    return false;
            }
            tokenStmtFor.bodyStmt = ParseStmt(ref token);
            return tokenStmtFor.bodyStmt != null;
        }

        /**
         * @brief parse a foreach statement
         * @param token = points to 'foreach' keyword token
         * @returns null: parse error
         *          else: pointer to encapsulated foreach statement token
         *          token = advanced just past for body statement
         */
        private TokenStmt ParseStmtForEach(ref Token token)
        {
            currentDeclFunc.triviality = Triviality.complex;

             // Create encapsulating token and skip past 'foreach ('
            TokenStmtForEach tokenStmtForEach = new(token);
            token = token.nextToken;
            if(token is not TokenKwParOpen)
            {
                ErrorMsg(token, "foreach must be followed by (");
                return null;
            }
            token = token.nextToken;

            if(token is TokenName tname)
            {
                tokenStmtForEach.keyLVal = new TokenLValName(tname, tokenScript.variablesStack);
                token = token.nextToken;
            }
            if(token is not TokenKwComma)
            {
                ErrorMsg(token, "expecting comma");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            if(token is TokenName tname2)
            {
                tokenStmtForEach.valLVal = new TokenLValName(tname2, tokenScript.variablesStack);
                token = token.nextToken;
            }
            if(token is not TokenKwIn)
            {
                ErrorMsg(token, "expecting 'in'");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            tokenStmtForEach.arrayRVal = GetOperand(ref token);
            if(tokenStmtForEach.arrayRVal == null)
                return null;
            if(token is not TokenKwParClose)
            {
                ErrorMsg(token, "expecting )");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            tokenStmtForEach.bodyStmt = ParseStmt(ref token);
            if(tokenStmtForEach.bodyStmt == null)
                return null;
            return tokenStmtForEach;
        }

        private TokenStmtIf ParseStmtIf(ref Token token)
        {
            TokenStmtIf tokenStmtIf = new(token);
            token = token.nextToken;
            tokenStmtIf.testRVal = ParseRValParen(ref token);
            if(tokenStmtIf.testRVal == null)
                return null;
            tokenStmtIf.trueStmt = ParseStmt(ref token);
            if(tokenStmtIf.trueStmt == null)
                return null;
            if(token is TokenKwElse)
            {
                token = token.nextToken;
                tokenStmtIf.elseStmt = ParseStmt(ref token);
                if(tokenStmtIf.elseStmt == null)
                    return null;
            }
            return tokenStmtIf;
        }

        private TokenStmtJump ParseStmtJump(ref Token token)
        {
             // Create jump statement token to encapsulate the whole statement.
            TokenStmtJump tokenStmtJump = new(token);
            token = token.nextToken;
            if(token is not TokenName || token.nextToken is not TokenKwSemi)
            {
                ErrorMsg(token, "expecting label;");
                token = SkipPastSemi(token);
                return null;
            }
            tokenStmtJump.label = (TokenName)token;
            token = token.nextToken.nextToken;

             // If label is already defined, it means this is a backward (looping)
             // jump, so remember the label has backward jump references.
             // We also then assume the function is complex, ie, it has a loop.
            if(currentDeclFunc.labels.ContainsKey(tokenStmtJump.label.val))
            {
                currentDeclFunc.labels[tokenStmtJump.label.val].hasBkwdRefs = true;
                currentDeclFunc.triviality = Triviality.complex;
            }

            return tokenStmtJump;
        }

        /**
         * @brief parse a jump target label statement
         * @param token = points to the '@' token
         * @returns null: error parsing
         *          else: the label
         *          token = advanced just past the ;
         */
        private TokenStmtLabel ParseStmtLabel(ref Token token)
        {
            if(token.nextToken is not TokenName tname || token.nextToken.nextToken is not TokenKwSemi)
            {
                ErrorMsg(token, "invalid label");
                token = SkipPastSemi(token);
                return null;
            }
            TokenStmtLabel stmtLabel = new(token)
            {
                name = tname,
                block = currentStmtBlock
            };
            if (currentDeclFunc.labels.ContainsKey(stmtLabel.name.val))
            {
                ErrorMsg(token.nextToken, "duplicate label");
                ErrorMsg(currentDeclFunc.labels[stmtLabel.name.val], "previously defined here");
                token = SkipPastSemi(token);
                return null;
            }
            currentDeclFunc.labels.Add(stmtLabel.name.val, stmtLabel);
            token = token.nextToken.nextToken.nextToken;
            return stmtLabel;
        }

        private TokenStmtNull ParseStmtNull(ref Token token)
        {
            TokenStmtNull tokenStmtNull = new(token);
            token = token.nextToken;
            return tokenStmtNull;
        }

        private TokenStmtRet ParseStmtRet(ref Token token)
        {
            TokenStmtRet tokenStmtRet = new(token);
            token = token.nextToken;
            if(token is TokenKwSemi)
            {
                token = token.nextToken;
            }
            else
            {
                tokenStmtRet.rVal = ParseRVal(ref token, semiOnly);
                if(tokenStmtRet.rVal == null)
                    return null;
            }
            return tokenStmtRet;
        }

        private TokenStmtSwitch ParseStmtSwitch(ref Token token)
        {
            TokenStmtSwitch tokenStmtSwitch = new(token);
            token = token.nextToken;
            tokenStmtSwitch.testRVal = ParseRValParen(ref token);
            if(tokenStmtSwitch.testRVal == null)
                return null;
            if(!(token is TokenKwBrcOpen))
            {
                ErrorMsg(token, "expecting open brace");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;
            TokenSwitchCase tokenSwitchCase = null;
            bool haveComplained = false;
            while(!(token is TokenKwBrcClose))
            {
                if(token is TokenKwCase)
                {
                    tokenSwitchCase = new TokenSwitchCase(token);
                    if(tokenStmtSwitch.lastCase == null)
                    {
                        tokenStmtSwitch.cases = tokenSwitchCase;
                    }
                    else
                    {
                        tokenStmtSwitch.lastCase.nextCase = tokenSwitchCase;
                    }
                    tokenStmtSwitch.lastCase = tokenSwitchCase;

                    token = token.nextToken;
                    tokenSwitchCase.rVal1 = ParseRVal(ref token, colonOrDotDotDot);
                    if(tokenSwitchCase.rVal1 == null)
                        return null;
                    if(token is TokenKwDotDotDot)
                    {
                        token = token.nextToken;
                        tokenSwitchCase.rVal2 = ParseRVal(ref token, colonOnly);
                        if(tokenSwitchCase.rVal2 == null)
                            return null;
                    }
                    else
                    {
                        if(!(token is TokenKwColon))
                        {
                            ErrorMsg(token, "expecting : or ...");
                            token = SkipPastSemi(token);
                            return null;
                        }
                        token = token.nextToken;
                    }
                }
                else if(token is TokenKwDefault)
                {
                    tokenSwitchCase = new TokenSwitchCase(token);
                    if(tokenStmtSwitch.lastCase == null)
                    {
                        tokenStmtSwitch.cases = tokenSwitchCase;
                    }
                    else
                    {
                        tokenStmtSwitch.lastCase.nextCase = tokenSwitchCase;
                    }
                    tokenStmtSwitch.lastCase = tokenSwitchCase;

                    token = token.nextToken;
                    if(!(token is TokenKwColon))
                    {
                        ErrorMsg(token, "expecting :");
                        token = SkipPastSemi(token);
                        return null;
                    }
                    token = token.nextToken;
                }
                else if(tokenSwitchCase != null)
                {
                    TokenStmt bodyStmt = ParseStmt(ref token);
                    if(bodyStmt == null)
                        return null;
                    if(tokenSwitchCase.lastStmt == null)
                    {
                        tokenSwitchCase.stmts = bodyStmt;
                    }
                    else
                    {
                        tokenSwitchCase.lastStmt.nextToken = bodyStmt;
                    }
                    tokenSwitchCase.lastStmt = bodyStmt;
                    bodyStmt.nextToken = null;
                }
                else if(!haveComplained)
                {
                    ErrorMsg(token, "expecting case or default label");
                    token = SkipPastSemi(token);
                    haveComplained = true;
                }
            }
            token = token.nextToken;
            return tokenStmtSwitch;
        }

        private TokenStmtState ParseStmtState(ref Token token)
        {
            TokenStmtState tokenStmtState = new(token);
            token = token.nextToken;
            if((token is not TokenName && token is not TokenKwDefault) || token.nextToken is not TokenKwSemi)
            {
                ErrorMsg(token, "expecting state;");
                token = SkipPastSemi(token);
                return null;
            }
            if(token is TokenName tname)
            {
                tokenStmtState.state = tname;
            }
            token = token.nextToken.nextToken;
            return tokenStmtState;
        }

        private TokenStmtThrow ParseStmtThrow(ref Token token)
        {
            TokenStmtThrow tokenStmtThrow = new(token);
            token = token.nextToken;
            if(token is TokenKwSemi)
            {
                token = token.nextToken;
            }
            else
            {
                tokenStmtThrow.rVal = ParseRVal(ref token, semiOnly);
                if(tokenStmtThrow.rVal == null)
                    return null;
            }
            return tokenStmtThrow;
        }

        /**
         * @brief Parse a try { ... } catch { ... } finally { ... } statement block
         * @param token = point to 'try' keyword on entry
         *                points past last '}' processed on return
         * @returns encapsulated try/catch/finally or null if parsing error
         */
        private TokenStmtTry ParseStmtTry(ref Token token)
        {
             // Parse out the 'try { ... }' part
            Token tryKw = token;
            token = token.nextToken;
            TokenStmt body = ParseStmtBlock(ref token);

            while(true)
            {
                TokenStmtTry tokenStmtTry;
                if(token is TokenKwCatch)
                {
                    if(token.nextToken is not TokenKwParOpen ||
                        token.nextToken.nextToken is not TokenType ||
                        token.nextToken.nextToken.nextToken is not TokenName ||
                        token.nextToken.nextToken.nextToken.nextToken is not TokenKwParClose)
                    {
                        ErrorMsg(token, "catch must be followed by ( <type> <varname> ) { <statement>... }");
                        return null;
                    }
                    token = token.nextToken.nextToken;     // skip over 'catch' '('
                    TokenDeclVar tag = new(token.nextToken, currentDeclFunc, tokenScript) { type = (TokenType)token };
                    token = token.nextToken;            // skip over <type>
                    tag.name = (TokenName)token;
                    token = token.nextToken.nextToken;  // skip over <varname> ')'

                    if((tag.type is not TokenTypeExc) && (tag.type is not TokenTypeStr))
                    {
                        ErrorMsg(tag.type, "must be type 'exception' or 'string'");
                    }

                    tokenStmtTry = new TokenStmtTry(tryKw)
                    {
                        tryStmt = WrapTryCatFinInBlock(body),
                        catchVar = tag
                    };
                    tokenScript.PushVarFrame(false);
                    tokenScript.AddVarEntry(tag);
                    tokenStmtTry.catchStmt = ParseStmtBlock(ref token);
                    tokenScript.PopVarFrame();
                    if(tokenStmtTry.catchStmt == null)
                        return null;
                    tokenStmtTry.tryStmt.isTry = true;
                    tokenStmtTry.tryStmt.tryStmt = tokenStmtTry;
                    tokenStmtTry.catchStmt.isCatch = true;
                    tokenStmtTry.catchStmt.tryStmt = tokenStmtTry;
                }
                else if(token is TokenKwFinally)
                {
                    token = token.nextToken;

                    tokenStmtTry = new TokenStmtTry(tryKw)
                    {
                        tryStmt = WrapTryCatFinInBlock(body),
                        finallyStmt = ParseStmtBlock(ref token)
                    };
                    if (tokenStmtTry.finallyStmt == null)
                        return null;
                    tokenStmtTry.tryStmt.isTry = true;
                    tokenStmtTry.tryStmt.tryStmt = tokenStmtTry;
                    tokenStmtTry.finallyStmt.isFinally = true;
                    tokenStmtTry.finallyStmt.tryStmt = tokenStmtTry;
                }
                else
                    break;

                body = tokenStmtTry;
            }

            if(!(body is TokenStmtTry))
            {
                ErrorMsg(body, "try must have a matching catch and/or finally");
                return null;
            }
            return (TokenStmtTry)body;
        }

        /**
         * @brief Wrap a possible try/catch/finally statement block in a block statement.
         *
         * Given body = try { } catch (string s) { }
         *
         * we return { try { } catch (string s) { } }
         *
         * @param body = a TokenStmtTry or a TokenStmtBlock
         * @returns a TokenStmtBlock
         */
        private TokenStmtBlock WrapTryCatFinInBlock(TokenStmt body)
        {
            if(body is TokenStmtBlock tb)
                return tb;

            TokenStmtTry innerTry = (TokenStmtTry)body;

            TokenStmtBlock wrapper = new(body)
            {
                statements = innerTry,
                outerStmtBlock = currentStmtBlock,
                function = currentDeclFunc
            };

            innerTry.tryStmt.outerStmtBlock = wrapper;
            if(innerTry.catchStmt != null)
                innerTry.catchStmt.outerStmtBlock = wrapper;
            if(innerTry.finallyStmt != null)
                innerTry.finallyStmt.outerStmtBlock = wrapper;

            return wrapper;
        }

        private TokenStmtWhile ParseStmtWhile(ref Token token)
        {
            currentDeclFunc.triviality = Triviality.complex;
            TokenStmtWhile tokenStmtWhile = new(token);
            token = token.nextToken;
            tokenStmtWhile.testRVal = ParseRValParen(ref token);
            if(tokenStmtWhile.testRVal == null)
                return null;
            tokenStmtWhile.bodyStmt = ParseStmt(ref token);
            if(tokenStmtWhile.bodyStmt == null)
                return null;
            return tokenStmtWhile;
        }

        /**
         * @brief parse a variable declaration statement, including init value if any.
         * @param token = points to type or 'constant' token
         * @param initFunc = null: parsing a local var declaration
         *                         put initialization code in .init
         *                   else: parsing a global var or field var declaration
         *                         put initialization code in initFunc.body
         * @returns null: parsing error
         *          else: variable declaration encapulating token
         *          token = advanced just past semi-colon
         *          variables = modified to include the new variable
         */
        private TokenDeclVar ParseDeclVar(ref Token token, TokenDeclVar initFunc)
        {
            TokenDeclVar tokenDeclVar = new(token.nextToken, currentDeclFunc, tokenScript);

             // Handle constant declaration.
             // It ends up in the declared variables list for the statement block just like
             // any other variable, except it has .constant = true.
             // The code generator will test that the initialization expression is constant.
             //
             //  constant <name> = <value> ;
            if(token is TokenKwConst)
            {
                token = token.nextToken;
                if(token is not TokenName)
                {
                    ErrorMsg(token, "expecting constant name");
                    token = SkipPastSemi(token);
                    return null;
                }
                tokenDeclVar.name = (TokenName)token;
                token = token.nextToken;
                if(token is not TokenKwAssign)
                {
                    ErrorMsg(token, "expecting =");
                    token = SkipPastSemi(token);
                    return null;
                }
                token = token.nextToken;
                TokenRVal rVal = ParseRVal(ref token, semiOnly);
                if(rVal == null)
                    return null;
                tokenDeclVar.init = rVal;
                tokenDeclVar.constant = true;
            }

             // Otherwise, normal variable declaration with optional initialization value.
            else
            {
                 // Build basic encapsulating token with type and name.
                tokenDeclVar.type = (TokenType)token;
                token = token.nextToken;
                if(!(token is TokenName))
                {
                    ErrorMsg(token, "expecting variable name");
                    token = SkipPastSemi(token);
                    return null;
                }
                tokenDeclVar.name = (TokenName)token;
                token = token.nextToken;

                 // If just a ;, there is no explicit initialization value.
                 // Otherwise, look for an =RVal; expression that has init value.
                if(token is TokenKwSemi)
                {
                    token = token.nextToken;
                    if(initFunc != null)
                    {
                        tokenDeclVar.init = TokenRValInitDef.Construct(tokenDeclVar);
                    }
                }
                else if(token is TokenKwAssign)
                {
                    token = token.nextToken;
                    if(initFunc != null)
                    {
                        currentDeclFunc = initFunc;
                        tokenDeclVar.init = ParseRVal(ref token, semiOnly);
                        currentDeclFunc = null;
                    }
                    else
                    {
                        tokenDeclVar.init = ParseRVal(ref token, semiOnly);
                    }
                    if(tokenDeclVar.init == null)
                        return null;
                }
                else
                {
                    ErrorMsg(token, "expecting = or ;");
                    token = SkipPastSemi(token);
                    return null;
                }
            }

            // If doing local vars, each var goes in its own var frame,
            // to make sure no code before this point can reference it.
            if (currentStmtBlock != null)
            {
                tokenScript.PushVarFrame(true);
            }
            /*
            ScriptConst scriptConst = ScriptConst.Lookup(tokenDeclVar.name.val);
            if (scriptConst != null)
            {
                ErrorMsg(tokenDeclVar, "reserved constant name " + tokenDeclVar.name.val);
                return null;
            }
            */
            // Can't be same name already in block.
            if (!tokenScript.AddVarEntry(tokenDeclVar))
            {
                ErrorMsg(tokenDeclVar, "duplicate variable " + tokenDeclVar.name.val);
                return null;
            }

            /*
            if (TokenDeclInline.inlineFunctions.HasAnyExact(tokenDeclVar.name.val))
            {
                ErrorMsg(tokenDeclVar, "reserved name " + tokenDeclVar.name.val);
                return null;
            }
            */

            return tokenDeclVar;
        }

        /**
         * @brief Add variable initialization to $globalvarinit, $staticfieldinit or $instfieldinit function.
         * @param initFunc = $globalvarinit, $staticfieldinit or $instfieldinit function
         * @param left = variable being initialized
         * @param init = null: initialize to default value
         *               else: initialize to this value
         */
        private void DoVarInit(TokenDeclVar initFunc, TokenLVal left, TokenRVal init)
        {
             // Make a statement that assigns the initialization value to the variable.
            TokenStmt stmt;
            if(init == null)
            {
                stmt = new TokenStmtVarIniDef(left) { var = left };
            }
            else
            {
                TokenKw op = new TokenKwAssign(left);
                stmt = new TokenStmtRVal(init) { rVal = new TokenRValOpBin(left, op, init) };
            }

             // Add statement to end of initialization function.
             // Be sure to execute them in same order as in source
             // as some doofus scripts depend on it.
            Token lastStmt = initFunc.body.statements;
            if(lastStmt == null)
            {
                initFunc.body.statements = stmt;
            }
            else
            {
                Token nextStmt;
                while((nextStmt = lastStmt.nextToken) != null)
                {
                    lastStmt = nextStmt;
                }
                lastStmt.nextToken = stmt;
            }
        }

        /**
         * @brief parse function declaration argument list
         * @param token = points to TokenKwParOpen
         * @returns null: parse error
         *          else: points to token with types and names
         *          token = updated past the TokenKw{Brk,Par}Close
         */
        private TokenArgDecl ParseFuncArgs(ref Token token, Type end)
        {
            TokenArgDecl tokenArgDecl = new(token);

            bool first = true;
            do
            {
                token = token.nextToken;
                if((token.GetType() == end) && first)
                    break;
                if(!(token is TokenType type))
                {
                    ErrorMsg(token, "expecting arg type");
                    token = SkipPastSemi(token);
                    return null;
                }
                token = token.nextToken;
                if(token is not TokenName name)
                {
                    ErrorMsg(token, "expecting arg name");
                    token = SkipPastSemi(token);
                    return null;
                }
                token = token.nextToken;

                if(!tokenArgDecl.AddArg(type, name))
                {
                    ErrorMsg(name, "duplicate arg name");
                }
                first = false;
            } while(token is TokenKwComma);

            if(token.GetType() != end)
            {
                ErrorMsg(token, "expecting comma or close bracket/paren");
                token = SkipPastSemi(token);
                return null;
            }
            token = token.nextToken;

            return tokenArgDecl;
        }

        /**
         * @brief parse right-hand value expression
         *        this is where arithmetic-like expressions are processed
         * @param token = points to first token expression
         * @param termTokenType = expression termination token type
         * @returns null: not an RVal
         *          else: single token representing whole expression
         *          token = if termTokenType.Length == 1, points just past terminating token
         *                                          else, points right at terminating token
         */
        public TokenRVal ParseRVal(ref Token token, Type[] termTokenTypes)
        {
             // Start with pushing the first operand on operand stack.
            BinOp binOps = null;
            TokenRVal operands = GetOperand(ref token);
            if(operands == null)
                return null;

             // Keep scanning until we hit the termination token.
            while(true)
            {
                Type tokType = token.GetType();
                for(int i = termTokenTypes.Length; --i >= 0;)
                {
                    if(tokType == termTokenTypes[i])
                        goto done;
                }

                 // Special form:
                 //   <operand> is <typeexp>
                if(token is TokenKwIs)
                {
                    TokenRValIsType tokenRValIsType = new(token);
                    token = token.nextToken;

                     // Parse the <typeexp>.
                    tokenRValIsType.typeExp = ParseTypeExp(ref token);
                    if(tokenRValIsType.typeExp == null)
                        return null;

                     // Replace top operand with result of <operand> is <typeexp>
                    tokenRValIsType.rValExp = operands;
                    tokenRValIsType.nextToken = operands.nextToken;
                    operands = tokenRValIsType;

                     // token points just past <typeexp> so see if it is another operator.
                    continue;
                }

                 // Peek at next operator.
                BinOp binOp = GetOperator(ref token);
                if(binOp == null)
                    return null;

                 // If there are stacked operators of higher or same precedence than new one,
                 // perform their computation then push result back on operand stack.
                 //
                 //  higher or same = left-to-right application of operators
                 //                   eg, a - b - c becomes (a - b) - c
                 //
                 //  higher precedence = right-to-left application of operators
                 //                      eg, a - b - c becomes a - (b - c)
                 //
                 // Now of course, there is some ugliness necessary:
                 //      we want:  a  - b - c  =>  (a - b) - c    so we do 'higher or same'
                 //  but we want:  a += b = c  =>  a += (b = c)   so we do 'higher only'
                 //
                 // binOps is the first operator (or null if only one)
                 // binOp is the second operator (or first if only one)
                while(binOps != null)
                {
                    if(binOps.preced < binOp.preced)
                        break;       // 1st operator lower than 2nd, so leave 1st on stack to do later
                    if(binOps.preced > binOp.preced)
                        goto do1st;  // 1st op higher than 2nd, so we always do 1st op first
                    if(binOps.preced == ASNPR)
                        break;             // equal preced, if assignment type, leave 1st on stack to do later
                                           //               if non-asn type, do 1st op first (ie left-to-right)
                    do1st:
                    TokenRVal result = PerformBinOp((TokenRVal)operands.prevToken, binOps, (TokenRVal)operands);
                    result.prevToken = operands.prevToken.prevToken;
                    operands = result;
                    binOps = binOps.pop;
                }

                 // Handle conditional expression as a special form:
                 //    <condexp> ? <trueexp> : <falseexp>
                if(binOp.token is TokenKwQMark)
                {
                    TokenRValCondExpr condExpr = new(binOp.token)
                    {
                        condExpr = operands,
                        trueExpr = ParseRVal(ref token, new Type[] { typeof(TokenKwColon) }),
                        falseExpr = ParseRVal(ref token, termTokenTypes),
                        prevToken = operands.prevToken
                    };
                    operands = condExpr;
                    termTokenTypes = Array.Empty<Type>();
                    goto done;
                }

                 // Push new operator on its stack.
                binOp.pop = binOps;
                binOps = binOp;

                 // Push next operand on its stack.
                TokenRVal operand = GetOperand(ref token);
                if(operand == null)
                    return null;
                operand.prevToken = operands;
                operands = operand;
            }
            done:

             // At end of expression, perform any stacked computations.
            while(binOps != null)
            {
                TokenRVal result = PerformBinOp((TokenRVal)operands.prevToken, binOps, (TokenRVal)operands);
                result.prevToken = operands.prevToken.prevToken;
                operands = result;
                binOps = binOps.pop;
            }

             // There should be exactly one remaining operand on the stack which is our final result.
            if(operands.prevToken != null)
                throw new Exception("too many operands");

             // If only one terminator type possible, advance past the terminator.
            if(termTokenTypes.Length == 1)
                token = token.nextToken;

            return operands;
        }

        private TokenTypeExp ParseTypeExp(ref Token token)
        {
            TokenTypeExp leftOperand = GetTypeExp(ref token);
            if(leftOperand == null)
                return null;

            while((token is TokenKwAnd) || (token is TokenKwOr))
            {
                Token typeBinOp = token;
                token = token.nextToken;
                TokenTypeExp rightOperand = GetTypeExp(ref token);
                if(rightOperand == null)
                    return null;
                TokenTypeExpBinOp typeExpBinOp = new(typeBinOp)
                {
                    leftOp = leftOperand,
                    binOp = typeBinOp,
                    rightOp = rightOperand
                };
                leftOperand = typeExpBinOp;
            }
            return leftOperand;
        }

        private TokenTypeExp GetTypeExp(ref Token token)
        {
            if(token is TokenKwTilde)
            {
                TokenTypeExpNot typeExpNot = new(token);
                token = token.nextToken;
                typeExpNot.typeExp = GetTypeExp(ref token);
                if(typeExpNot.typeExp == null)
                    return null;
                return typeExpNot;
            }
            if(token is TokenKwParOpen)
            {
                TokenTypeExpPar typeExpPar = new(token);
                token = token.nextToken;
                typeExpPar.typeExp = GetTypeExp(ref token);
                if(typeExpPar.typeExp == null)
                    return null;
                if(!(token is TokenKwParClose))
                {
                    ErrorMsg(token, "expected close parenthesis");
                    token = SkipPastSemi(token);
                    return null;
                }
                return typeExpPar;
            }
            if(token is TokenKwUndef)
            {
                TokenTypeExpUndef typeExpUndef = new(token);
                token = token.nextToken;
                return typeExpUndef;
            }
            if(token is TokenType ttype)
            {
                TokenTypeExpType typeExpType = new(token) { typeToken = ttype };
                token = token.nextToken;
                return typeExpType;
            }
            ErrorMsg(token, "expected type");
            token = SkipPastSemi(token);
            return null;
        }

        /**
         * @brief get a right-hand operand expression token
         * @param token = first token of operand to parse
         * @returns null: invalid operand
         *          else: token that bundles or wraps the operand
         *          token = points to token following last operand token
         */
        private TokenRVal GetOperand(ref Token token)
        {
             // Prefix unary operators (eg ++, --) requiring an L-value.
            if((token is TokenKwIncr) || (token is TokenKwDecr))
            {
                TokenRValAsnPre asnPre = new(token) { prefix = token };
                token = token.nextToken;
                TokenRVal op = GetOperand(ref token);
                if(op == null)
                    return null;
                if(op is not TokenLVal)
                {
                    ErrorMsg(op, "can pre{in,de}crement only an L-value");
                    return null;
                }
                asnPre.lVal = (TokenLVal)op;
                return asnPre;
            }

             // Get the bulk of the operand, ie, without any of the below suffixes.
            TokenRVal operand = GetOperandNoMods(ref token);
            if(operand == null)
                return null;
            modifiers:

             // If followed by '++' or '--', it is post-{in,de}cremented.
            if((token is TokenKwIncr) || (token is TokenKwDecr))
            {
                TokenRValAsnPost asnPost = new(token) { postfix = token };
                token = token.nextToken;
                if(operand is not TokenLVal)
                {
                    ErrorMsg(operand, "can post{in,de}crement only an L-value");
                    return null;
                }
                asnPost.lVal = (TokenLVal)operand;
                return asnPost;
            }

             // If followed by a '.', it is an instance field or instance method reference.
            if(token is TokenKwDot)
            {
                token = token.nextToken;
                if(token is not TokenName)
                {
                    ErrorMsg(token, ". must be followed by field/method name");
                    return null;
                }
                TokenLValIField field = new(token)
                {
                    baseRVal = operand,
                    fieldName = (TokenName)token
                };
                operand = field;
                token = token.nextToken;
                goto modifiers;
            }

             // If followed by a '[', it is an array subscript.
            if(token is TokenKwBrkOpen)
            {
                TokenLValArEle tokenLValArEle = new(token);
                token = token.nextToken;

                 // Parse subscript(s) expression.
                tokenLValArEle.subRVal = ParseRVal(ref token, brkCloseOnly);
                if(tokenLValArEle.subRVal == null)
                {
                    ErrorMsg(tokenLValArEle, "invalid subscript");
                    return null;
                }

                // See if comma-separated list of values.
                int numSubscripts = SplitCommaRVals(tokenLValArEle.subRVal, out TokenRVal subscriptRVals);
                if (numSubscripts > 1)
                {
                    // If so, put the values in an LSL_List object.
                    TokenRValList rValList = new(tokenLValArEle)
                    {
                        rVal = subscriptRVals,
                        nItems = numSubscripts
                    };
                    tokenLValArEle.subRVal = rValList;
                }

                 // Either way, save array variable name
                 // and substitute whole reference for L-value
                tokenLValArEle.baseRVal = operand;
                operand = tokenLValArEle;
                goto modifiers;
            }

             // If followed by a '(', it is a function/method call.
            if(token is TokenKwParOpen)
            {
                operand = ParseRValCall(ref token, operand);
                goto modifiers;
            }

             // If 'new' arraytipe '{', it is an array initializer.
            if((token is TokenKwBrcOpen) && (operand is TokenLValSField) &&
                (((TokenLValSField)operand).fieldName.val == "$new") &&
                ((TokenLValSField)operand).baseType.ToString().EndsWith("]"))
            {
                operand = ParseRValNewArIni(ref token, (TokenLValSField)operand);
                if(operand != null)
                    goto modifiers;
            }

            return operand;
        }

        /**
         * @brief same as GetOperand() except doesn't check for any modifiers
         */
        private TokenRVal GetOperandNoMods(ref Token token)
        {
             // Simple unary operators.
            if((token is TokenKwSub) ||
                (token is TokenKwTilde) ||
                (token is TokenKwExclam))
            {
                Token uop = token;
                token = token.nextToken;
                TokenRVal rVal = GetOperand(ref token);
                if(rVal == null)
                    return null;
                return PerformUnOp(uop, rVal);
            }

             // Type casting.
            if((token is TokenKwParOpen) &&
                (token.nextToken is TokenType) &&
                (token.nextToken.nextToken is TokenKwParClose))
            {
                TokenType type = (TokenType)token.nextToken;
                token = token.nextToken.nextToken.nextToken;
                TokenRVal rVal = GetOperand(ref token);
                if(rVal == null)
                    return null;
                return new TokenRValCast(type, rVal);
            }

             // Parenthesized expression.
            if(token is TokenKwParOpen)
            {
                return ParseRValParen(ref token);
            }

             // Constants.
            if(token is TokenChar tchar)
            {
                TokenRValConst rValConst = new(token, tchar.val);
                token = token.nextToken;
                return rValConst;
            }
            if(token is TokenFloat tfloat)
            {
                TokenRValConst rValConst = new(token, tfloat.val);
                token = token.nextToken;
                return rValConst;
            }
            if(token is TokenInt tint)
            {
                TokenRValConst rValConst = new(token, tint.val);
                token = token.nextToken;
                return rValConst;
            }
            if(token is TokenStr tstr)
            {
                TokenRValConst rValConst = new(token, tstr.val);
                token = token.nextToken;
                return rValConst;
            }
            if(token is TokenKwUndef tundef)
            {
                TokenRValUndef rValUndef = new(tundef);
                token = token.nextToken;
                return rValUndef;
            }

             // '<'value,...'>', ie, rotation or vector
            if(token is TokenKwCmpLT)
            {
                Token openBkt = token;
                token = token.nextToken;
                TokenRVal rValAll = ParseRVal(ref token, cmpGTOnly);
                if(rValAll == null)
                    return null;
                int nVals = SplitCommaRVals(rValAll, out TokenRVal rVals);
                switch (nVals)
                {
                    case 3:
                        {
                            return new TokenRValVec(openBkt)
                            {
                                xRVal = rVals,
                                yRVal = (TokenRVal)rVals.nextToken,
                                zRVal = (TokenRVal)rVals.nextToken.nextToken
                            };
                        }
                    case 4:
                        {
                            return new TokenRValRot(openBkt)
                            {
                                xRVal = rVals,
                                yRVal = (TokenRVal)rVals.nextToken,
                                zRVal = (TokenRVal)rVals.nextToken.nextToken,
                                wRVal = (TokenRVal)rVals.nextToken.nextToken.nextToken
                            };
                        }
                    default:
                        {
                            ErrorMsg(openBkt, "bad rotation/vector");
                            token = SkipPastSemi(token);
                            return null;
                        }
                }
            }

             // '['value,...']', ie, list
            if(token is TokenKwBrkOpen)
            {
                TokenRValList rValList = new(token);
                token = token.nextToken;
                if(token is TokenKwBrkClose)
                {
                    token = token.nextToken;  // empty list
                }
                else
                {
                    TokenRVal rValAll = ParseRVal(ref token, brkCloseOnly);
                    if(rValAll == null)
                        return null;
                    rValList.nItems = SplitCommaRVals(rValAll, out rValList.rVal);
                }
                return rValList;
            }

             // Maybe we have <type>.<name> referencing a static field or method of some type.
            if((token is TokenType) && (token.nextToken is TokenKwDot) && (token.nextToken.nextToken is TokenName))
            {
                TokenLValSField field = new(token.nextToken.nextToken)
                {
                    baseType = (TokenType)token,
                    fieldName = (TokenName)token.nextToken.nextToken
                };
                token = token.nextToken.nextToken.nextToken;
                return field;
            }

             // Maybe we have 'this' referring to the object of the instance method.
            if(token is TokenKwThis)
            {
                if((currentDeclSDType == null) || currentDeclSDType is not TokenDeclSDTypeClass)
                {
                    ErrorMsg(token, "using 'this' outside class definition");
                    token = SkipPastSemi(token);
                    return null;
                }
                TokenRValThis zhis = new(token, (TokenDeclSDTypeClass)currentDeclSDType);
                token = token.nextToken;
                return zhis;
            }

             // Maybe we have 'base' referring to a field/method of the extended class.
            if(token is TokenKwBase)
            {
                if((currentDeclFunc == null) || (currentDeclFunc.sdtClass == null) || !(currentDeclFunc.sdtClass is TokenDeclSDTypeClass))
                {
                    ErrorMsg(token, "using 'base' outside method");
                    token = SkipPastSemi(token);
                    return null;
                }
                if(token.nextToken is not TokenKwDot || token.nextToken.nextToken is not TokenName nextnexttname)
                {
                    ErrorMsg(token, "base must be followed by . then field or method name");
                    TokenRValThis zhis = new(token, (TokenDeclSDTypeClass)currentDeclFunc.sdtClass);
                    token = token.nextToken;
                    return zhis;
                }
                TokenLValBaseField baseField = new(token, nextnexttname, (TokenDeclSDTypeClass)currentDeclFunc.sdtClass);
                token = token.nextToken.nextToken.nextToken;
                return baseField;
            }

             // Maybe we have 'new <script-defined-type>' saying to create an object instance.
             // This ends up generating a call to static function <script-defined-type>.$new(...)
             // whose CIL code is generated by GenerateNewobjBody().
            if(token is TokenKwNew)
            {
                if(token.nextToken is not TokenType)
                {
                    ErrorMsg(token.nextToken, "new must be followed by type");
                    token = SkipPastSemi(token);
                    return null;
                }
                TokenLValSField field = new(token.nextToken.nextToken)
                {
                    baseType = (TokenType)token.nextToken,
                    fieldName = new TokenName(token, "$new")
                };
                token = token.nextToken.nextToken;
                return field;
            }

             // All we got left is <name>, eg, arg, function, global or local variable reference
            if(token is TokenName)
            {
                TokenLValName name = new((TokenName)token, tokenScript.variablesStack);
                token = token.nextToken;
                return name;
            }

             // Who knows what it is supposed to be?
            ErrorMsg(token, "invalid operand token");
            token = SkipPastSemi(token);
            return null;
        }

        /**
         * @brief Parse a call expression
         * @param token = points to arg list '('
         * @param meth = points to method name being called
         * @returns call expression value
         *          token = points just past arg list ')'
         */
        private TokenRValCall ParseRValCall(ref Token token, TokenRVal meth)
        {
            // Set up basic function call struct with function name.
            TokenRValCall rValCall = new(token) { meth = meth };

            // Parse the call parameters, if any.
            token = token.nextToken;
            if(token is TokenKwParClose)
            {
                token = token.nextToken;
            }
            else
            {
                rValCall.args = ParseRVal(ref token, parCloseOnly);
                if(rValCall.args == null)
                    return null;
                rValCall.nArgs = SplitCommaRVals(rValCall.args, out rValCall.args);
            }

            currentDeclFunc.unknownTrivialityCalls.AddLast(rValCall);

            return rValCall;
        }

        /**
         * @brief decode binary operator token
         * @param token = points to token to decode
         * @returns null: invalid operator token
         *          else: operator token and precedence
         */
        private BinOp GetOperator(ref Token token)
        {
            BinOp binOp = new();
            if(precedence.TryGetValue(token.GetType(), out binOp.preced))
            {
                binOp.token = (TokenKw)token;
                token = token.nextToken;
                return binOp;
            }

            if((token is TokenKwSemi) || (token is TokenKwBrcOpen) || (token is TokenKwBrcClose))
            {
                ErrorMsg(token, "premature expression end");
            }
            else
            {
                ErrorMsg(token, "invalid operator");
            }
            token = SkipPastSemi(token);
            return null;
        }

        private class BinOp
        {
            public BinOp pop;
            public TokenKw token;
            public int preced;
        }

        /**
         * @brief Return an R-value expression token that will be used to
         *        generate code to perform the operation at runtime.
         * @param left  = left-hand operand
         * @param binOp = operator
         * @param right = right-hand operand
         * @returns resultant expression
         */
        private static TokenRVal PerformBinOp(TokenRVal left, BinOp binOp, TokenRVal right)
        {
            return new TokenRValOpBin(left, binOp.token, right);
        }

        /**
         * @brief Return an R-value expression token that will be used to
         *        generate code to perform the operation at runtime.
         * @param unOp  = operator
         * @param right = right-hand operand
         * @returns resultant constant or expression
         */
        private static TokenRVal PerformUnOp(Token unOp, TokenRVal right)
        {
            return new TokenRValOpUn((TokenKw)unOp, right);
        }

        /**
         * @brief Parse an array initialization expression.
         * @param token = points to '{' on entry
         * @param newCall = encapsulates a '$new' call
         * @return resultant operand encapsulating '$new' call and initializers
         *             token = points just past terminating '}'
         *         ...or null if parse error
         */
        private TokenRVal ParseRValNewArIni(ref Token token, TokenLValSField newCall)
        {
            Stack<TokenList> stack = new();
            TokenRValNewArIni arini = new(token) { arrayType = newCall.baseType };
            TokenList values = null;
            while(true)
            {

                // open brace means start a (sub-)list
                if(token is TokenKwBrcOpen)
                {
                    stack.Push(values);
                    values = new TokenList(token);
                    token = token.nextToken;
                    continue;
                }

                // close brace means end of (sub-)list
                // if final '}' all done parsing
                if(token is TokenKwBrcClose)
                {
                    token = token.nextToken;          // skip over the '}'
                    TokenList innerds = values;        // save the list just closed
                    arini.valueList = innerds;         // it's the top list if it's the last closed
                    values = stack.Pop();             // pop to next outer list
                    if(values == null)
                        return arini;  // final '}', we are done
                    values.tl.Add(innerds);           // put the inner list on end of outer list
                    if(token is TokenKwComma)
                    {       // should have a ',' or '}' next
                        token = token.nextToken;   // skip over the ','
                    }
                    else if(!(token is TokenKwBrcClose))
                    {
                        ErrorMsg(token, "expecting , or } after sublist");
                    }
                    continue;
                }

                // this is a comma that doesn't have a value expression before it
                // so we take it to mean skip initializing element (leave it zeroes/null etc)
                if(token is TokenKwComma)
                {
                    values.tl.Add(token);
                    token = token.nextToken;
                    continue;
                }

                // parse value expression and skip terminating ',' if any
                TokenRVal append = ParseRVal(ref token, commaOrBrcClose);
                if(append == null)
                    return null;
                values.tl.Add(append);
                if(token is TokenKwComma)
                {
                    token = token.nextToken;
                }
            }
        }

        /**
         * @brief parse out a parenthesized expression.
         * @param token = points to open parenthesis
         * @returns null: invalid expression
         *          else: parenthesized expression token or constant token
         *          token = points past the close parenthesis
         */
        private TokenRValParen ParseRValParen(ref Token token)
        {
            if(!(token is TokenKwParOpen))
            {
                ErrorMsg(token, "expecting (");
                token = SkipPastSemi(token);
                return null;
            }
            TokenRValParen tokenRValParen = new(token);
            token = token.nextToken;
            tokenRValParen.rVal = ParseRVal(ref token, parCloseOnly);
            if(tokenRValParen.rVal == null)
                return null;
            return tokenRValParen;
        }

        /**
         * @brief Split a comma'd RVal into separate expressions
         * @param rValAll = expression containing commas
         * @returns number of comma separated values
         *          rVals = values in a null-terminated list linked by rVals.nextToken
         */
        private int SplitCommaRVals(TokenRVal rValAll, out TokenRVal rVals)
        {
            if(rValAll is not TokenRValOpBin opBin || opBin.opcode is not TokenKwComma)
            {
                rVals = rValAll;
                if(rVals.nextToken != null)
                    throw new Exception("expected null");
                return 1;
            }
            int leftCount = SplitCommaRVals(opBin.rValLeft, out TokenRVal rValLeft);
            int rightCount = SplitCommaRVals(opBin.rValRight, out TokenRVal rValRight);
            rVals = rValLeft;
            while(rValLeft.nextToken != null)
                rValLeft = (TokenRVal)rValLeft.nextToken;
            rValLeft.nextToken = rValRight;
            return leftCount + rightCount;
        }

        /**
         * @brief output error message and remember that there is an error.
         * @param token = what token is associated with the error
         * @param message = error message string
         */
        private void ErrorMsg(Token token, string message)
        {
            if(!errors || (token.file != lastErrorFile) || (token.line > lastErrorLine))
            {
                errors = true;
                lastErrorFile = token.file;
                lastErrorLine = token.line;
                token.ErrorMsg(message);
            }
        }

        /**
         * @brief Skip past the next semicolon (or matched braces)
         * @param token = points to token to skip over
         * @returns token just after the semicolon or close brace
         */
        private Token SkipPastSemi(Token token)
        {
            int braceLevel = 0;

            while(!(token is TokenEnd))
            {
                if((token is TokenKwSemi) && (braceLevel == 0))
                {
                    return token.nextToken;
                }
                if(token is TokenKwBrcOpen)
                {
                    braceLevel++;
                }
                if((token is TokenKwBrcClose) && (--braceLevel <= 0))
                {
                    return token.nextToken;
                }
                token = token.nextToken;
            }
            return token;
        }
    }

    /**
     * @brief Script-defined type declarations
     */
    public abstract class TokenDeclSDType: Token
    {
        protected const byte CLASS = 0;
        protected const byte DELEGATE = 1;
        protected const byte INTERFACE = 2;
        protected const byte TYPEDEF = 3;

        // stuff that gets cloned/copied/transformed when instantiating a generic
        // see InstantiateGeneric() below
        public TokenDeclSDType outerSDType;          // null if top-level
                                                     // else points to defining script-defined type
        public Dictionary<string, TokenDeclSDType> innerSDTypes = new();
        // indexed by shortName
        public Token begToken;                       // token that begins the definition (might be this or something like 'public')
        public Token endToken;                       // the '}' or ';' that ends the definition

        // generic instantiation assumes none of the rest needs to be cloned (well except for the shortName)
        public int sdTypeIndex = -1;                 // index in scriptObjCode.sdObjTypesIndx[] array
        public TokenDeclSDTypeClass extends;         // only non-null for TokenDeclSDTypeClass's
        public uint accessLevel;                     // SDT_PRIVATE, SDT_PROTECTED or SDT_PUBLIC
                                                     // ... all top-level types are SDT_PUBLIC
        public VarDict members = new (false);  // declared fields, methods, properties if any

        public Dictionary<string, int> genParams;    // list of parameters for generic prototypes
                                                     // null for non-generic prototypes
                                                     // eg, for 'Dictionary<K,V>'
                                                     //     ...genParams gives K->0; V->1

        public bool isPartial;                       // was declared with 'partial' keyword
                                                     // classes only, all others always false

        /*
         * Name of the type.
         *   shortName = doesn't include outer class type names
         *               eg, 'Engine' for non-generic
         *                   'Dictionary<,>' for generic prototype
         *                   'Dictionary<string,integer>' for generic instantiation
         *   longName = includes all outer class type names if any
         */
        private TokenName _shortName;
        private TokenName _longName;

        public TokenName shortName
        {
            get
            {
                return _shortName;
            }
            set
            {
                _shortName = value;
                _longName = null;
            }
        }

        public TokenName longName
        {
            get
            {
                if(_longName == null)
                {
                    _longName = _shortName;
                    if(outerSDType != null)
                    {
                        _longName = new TokenName(_shortName, outerSDType.longName.val + "." + _shortName.val);
                    }
                }
                return _longName;
            }
        }

        /*
         * Dictionary used when reading from object file that holds all script-defined types.
         * Not complete though until all types have been read from the object file.
         */
        private Dictionary<string, TokenDeclSDType> sdTypes;

        public TokenDeclSDType(Token t) : base(t) { }
        protected abstract TokenDeclSDType MakeBlank(TokenName shortName);
        public abstract TokenType MakeRefToken(Token t);
        public abstract Type GetSysType();
        public abstract void WriteToFile(BinaryWriter objFileWriter);
        public abstract void ReadFromFile(BinaryReader objFileReader, TextWriter asmFileWriter);

        /**
         * @brief Given that this is a generic prototype, apply the supplied genArgs 
         *        to create an equivalent instantiated non-generic.  This basically 
         *        makes a copy replacing all the parameter types with the actual 
         *        argument types.
         * @param this = the prototype to be instantiated, eg, 'Dictionary<string,integer>.Converter'
         * @param name = short name with arguments, eg, 'Converter<float>'.
         * @param genArgs = argument types of just this level, eg, 'float'.
         * @returns clone of this but with arguments applied and spliced in source token stream
         */
        public TokenDeclSDType InstantiateGeneric(string name, TokenType[] genArgs, ScriptReduce reduce)
        {
             // Malloc the struct and give it a name.
            TokenDeclSDType instdecl = this.MakeBlank(new TokenName(this, name));

             // If the original had an outer type, then so does the new one.
             // The outer type will never be a generic prototype, eg, if this 
             // is 'ValueList' it will always be inside 'Dictionary<string,integer>'
             // not 'Dictionary' at this point.
            if((this.outerSDType != null) && (this.outerSDType.genParams != null))
                throw new Exception();
            instdecl.outerSDType = this.outerSDType;

             // The generic prototype may have stuff like 'public' just before it and we need to copy that too.
            Token prefix = this;
            while((prefix = prefix.prevToken) != null)
            {
                if(prefix is not TokenKwPublic && prefix is not TokenKwProtected && prefix is not TokenKwPrivate)
                    break;
            }
            this.begToken = prefix.nextToken;

             // Splice in a copy of the prefix tokens, just before the beginning token of prototype (this.begToken).
            while((prefix = prefix.nextToken) != this)
            {
                SpliceSourceToken(prefix.CopyToken(prefix));
            }

             // Splice instantiation (instdecl) in just before the beginning token of prototype (this.begToken).
            SpliceSourceToken(instdecl);

            // Now for the fun part...  Copy the rest of the prototype body to the 
            // instantiated body, replacing all generic parameter type tokens with 
            // the corresponding generic argument types.  Note that the parameters 
            // are numbered starting with the outermost so we need the full genArgs 
            // array.  Eg if we are doing 'Converter<V=float>' from 
            // 'Dictionary<T=string,U=integer>.Converter<V=float>', any V's are 
            // numbered [2].  Any [0]s or [1]s should be gone by now but it doesn't 
            // matter.
            Token it, pt;
            TokenDeclSDType innerProto = this;
            TokenDeclSDType innerInst = instdecl;
            for(pt = this; (pt = pt.nextToken) != this.endToken;)
            {
                 // Coming across a sub-type's declaration involves a deep copy of the 
                 // declaration token.  Fortunately we are early on in parsing, so there 
                 // really isn't much to copy:
                 //   1) short name is the same, eg, doing List of Dictionary<string,integer>.List is same short name as Dictionary<T,U>.List
                 //      if generic, eg doing Converter<W> of Dictionary<T,U>.Converter<W>, we have to manually copy the W as well.
                 //   2) outerSDType is transformed from Dictionary<T,U> to Dictionary<string,integer>.
                 //   3) innerSDTypes is rebuilt when/if we find classes that are inner to this one.
                if(pt is TokenDeclSDType ptSDType)
                {
                     // Make a new TokenDeclSDType{Class,Delegate,Interface}.
                    TokenDeclSDType itSDType = ptSDType.MakeBlank(new TokenName(ptSDType.shortName, ptSDType.shortName.val));

                     // Set up the transformed outerSDType.
                     // Eg, if we are creating Enumerator of Dictionary<string,integer>.Enumerator,
                     // innerProto = Dictionary<T,U> and innerInst = Dictionary<string,integer>.
                    itSDType.outerSDType = innerInst;

                     // This clone is an inner type of its next outer level.
                    reduce.CatalogSDTypeDecl(itSDType);

                     // We need to manually copy any generic parameters of the class declaration being cloned.
                     // eg, if we are cloning Converter<W>, this is where the W gets copied.
                     // Since it is an immutable array of strings, just copy the array pointer, if any.
                    itSDType.genParams = ptSDType.genParams;

                     // We are now processing tokens for this cloned type declaration.
                    innerProto = ptSDType;
                    innerInst = itSDType;

                     // Splice this clone token in.
                    it = itSDType;
                }

                 // Check for an generic parameter to substitute out.
                else if((pt is TokenName namept) && this.genParams.TryGetValue(namept.val, out int index))
                {
                    it = genArgs[index].CopyToken(pt);
                }

                 // Everything else is a simple copy.
                else
                    it = pt.CopyToken(pt);

                 // Whatever we came up with, splice it into the source token stream.
                SpliceSourceToken(it);

                 // Maybe we just finished copying an inner type definition.
                 // If so, remember where it ends and pop it from the stack.
                if(innerProto.endToken == pt)
                {
                    innerInst.endToken = it;
                    innerProto = innerProto.outerSDType;
                    innerInst = innerInst.outerSDType;
                }
            }

             // Clone and insert the terminator, either '}' or ';'
            it = pt.CopyToken(pt);
            SpliceSourceToken(it);
            instdecl.endToken = it;

            return instdecl;
        }

        /**
         * @brief Splice a source token in just before the type's beginning keyword.
         */
        private void SpliceSourceToken(Token it)
        {
            it.nextToken = this.begToken;
            (it.prevToken = this.begToken.prevToken).nextToken = it;
            this.begToken.prevToken = it;
        }

        /**
         * @brief Read one of these in from the object file.
         * @param sdTypes = dictionary of script-defined types, not yet complete
         * @param name = script-visible name of this type
         * @param objFileReader = reads from the object file
         * @param asmFileWriter = writes to the disassembly file (might be null)
         */
        public static TokenDeclSDType ReadFromFile(Dictionary<string, TokenDeclSDType> sdTypes, string name,
                                                    BinaryReader objFileReader, TextWriter asmFileWriter)
        {
            string file = objFileReader.ReadString();
            int line = objFileReader.ReadInt32();
            int posn = objFileReader.ReadInt32();
            byte code = objFileReader.ReadByte();
            TokenName n = new(null, file, line, posn, name);
            TokenDeclSDType sdt;
            switch(code)
            {
                case CLASS:
                    {
                        sdt = new TokenDeclSDTypeClass(n, false);
                        break;
                    }
                case DELEGATE:
                    {
                        sdt = new TokenDeclSDTypeDelegate(n);
                        break;
                    }
                case INTERFACE:
                    {
                        sdt = new TokenDeclSDTypeInterface(n);
                        break;
                    }
                case TYPEDEF:
                    {
                        sdt = new TokenDeclSDTypeTypedef(n);
                        break;
                    }
                default:
                    throw new Exception();
            }
            sdt.sdTypes = sdTypes;
            sdt.sdTypeIndex = objFileReader.ReadInt32();
            sdt.ReadFromFile(objFileReader, asmFileWriter);
            return sdt;
        }

        /**
         * @brief Convert a typename string to a type token
         * @param name = script-visible name of token to create, 
         *               either a script-defined type or an LSL-defined type
         * @returns type token
         */
        protected TokenType MakeTypeToken(string name)
        {
            if (sdTypes.TryGetValue(name, out TokenDeclSDType sdtdecl))
                return sdtdecl.MakeRefToken(this);
            return TokenType.FromLSLType(this, name);
        }

        // debugging - returns, eg, 'Dictionary<T,U>.Enumerator.Node'
        public override void DebString(StringBuilder sb)
        {
            // get long name broken down into segments from outermost to this
            Stack<TokenDeclSDType> declStack = new();
            for(TokenDeclSDType decl = this; decl != null; decl = decl.outerSDType)
            {
                declStack.Push(decl);
            }

            // output each segment's name followed by our args for it
            // starting with outermost and ending with this
            while(declStack.Count > 0)
            {
                TokenDeclSDType decl = declStack.Pop();
                sb.Append(decl.shortName.val);
                if(decl.genParams != null)
                {
                    sb.Append('<');
                    string[] parms = new string[decl.genParams.Count];
                    foreach(KeyValuePair<string, int> kvp in decl.genParams)
                    {
                        parms[kvp.Value] = kvp.Key;
                    }
                    for(int j = 0; j < parms.Length;)
                    {
                        sb.Append(parms[j]);
                        if(++j < parms.Length)
                            sb.Append(',');
                    }
                    sb.Append('>');
                }
                if(declStack.Count > 0)
                    sb.Append('.');
            }
        }
    }

    public class TokenDeclSDTypeClass: TokenDeclSDType
    {
        public List<TokenDeclSDTypeInterface> implements = new();
        public TokenDeclVar instFieldInit;         // $instfieldinit function to do instance field initializations
        public TokenDeclVar staticFieldInit;       // $staticfieldinit function to do static field initializations

        public Dictionary<string, int> intfIndices = new();  // longname => this.iFaces index
        public TokenDeclSDTypeInterface[] iFaces;  // array of implemented interfaces
                                                   //   low-end entries copied from rootward classes
        public TokenDeclVar[][] iImplFunc;         // iImplFunc[i][j]:
                                                   //   low-end [i] entries copied from rootward classes
                                                   //   i = interface number from this.intfIndices[name]
                                                   //   j = method of interface from iface.methods[name].vTableIndex

        public TokenType arrayOfType;     // if array, it's an array of this type, else null
        public int arrayOfRank;           // if array, it has this number of dimensions, else zero

        public bool slotsAssigned;        // set true when slots have been assigned...
        public XMRInstArSizes instSizes = new();
        // number of instance fields of various types
        public int numVirtFuncs;          // number of virtual functions
        public int numInterfaces;         // number of implemented interfaces

        private string extendsStr;
        private string arrayOfTypeStr;
        private List<StackedMethod> stackedMethods;
        private List<StackedIFace> stackedIFaces;

        public DynamicMethod[] vDynMeths;    // virtual method entrypoints
        public Type[] vMethTypes;            // virtual method delegate types
        public DynamicMethod[][] iDynMeths;  // interface method entrypoints
        public Type[][] iMethTypes;          // interface method types
                                             //   low-end [i] entries copied from rootward classes
                                             //   i = interface number from this.intfIndices[name]
                                             //   j = method of interface from iface.methods[name].vTableIndex

        public TokenDeclSDTypeClass(TokenName shortName, bool isPartial) : base(shortName)
        {
            this.shortName = shortName;
            this.isPartial = isPartial;
        }

        protected override TokenDeclSDType MakeBlank(TokenName shortName)
        {
            return new TokenDeclSDTypeClass(shortName, false);
        }

        public override TokenType MakeRefToken(Token t)
        {
            return new TokenTypeSDTypeClass(t, this);
        }

        public override Type GetSysType()
        {
            return typeof(XMRSDTypeClObj);
        }

        /**
         * @brief See if the class implements the interface.
         *        Do a recursive (deep) check in all rootward classes.
         */
        public bool CanCastToIntf(TokenDeclSDTypeInterface intf)
        {
            if(this.implements.Contains(intf))
                return true;
            if(this.extends == null)
                return false;
            return this.extends.CanCastToIntf(intf);
        }

        /**
         * @brief Write enough out so we can reconstruct with ReadFromFile.
         */
        public override void WriteToFile(BinaryWriter objFileWriter)
        {
            objFileWriter.Write(this.file);
            objFileWriter.Write(this.line);
            objFileWriter.Write(this.posn);
            objFileWriter.Write((byte)CLASS);
            objFileWriter.Write(this.sdTypeIndex);

            this.instSizes.WriteToFile(objFileWriter);
            objFileWriter.Write(numVirtFuncs);

            if(extends == null)
            {
                objFileWriter.Write("");
            }
            else
            {
                objFileWriter.Write(extends.longName.val);
            }

            objFileWriter.Write(arrayOfRank);
            if(arrayOfRank > 0)
                objFileWriter.Write(arrayOfType.ToString());

            foreach(TokenDeclVar meth in members)
            {
                if((meth.retType != null) && (meth.vTableIndex >= 0))
                {
                    objFileWriter.Write(meth.vTableIndex);
                    objFileWriter.Write(meth.GetObjCodeName());
                    objFileWriter.Write(meth.GetDelType().decl.GetWholeSig());
                }
            }
            objFileWriter.Write(-1);

            int numIFaces = iImplFunc.Length;
            objFileWriter.Write(numIFaces);
            for(int i = 0; i < numIFaces; i++)
            {
                objFileWriter.Write(iFaces[i].longName.val);
                TokenDeclVar[] meths = iImplFunc[i];
                int numMeths = 0;
                if(meths != null)
                    numMeths = meths.Length;
                objFileWriter.Write(numMeths);
                for(int j = 0; j < numMeths; j++)
                {
                    TokenDeclVar meth = meths[j];
                    objFileWriter.Write(meth.vTableIndex);
                    objFileWriter.Write(meth.GetObjCodeName());
                    objFileWriter.Write(meth.GetDelType().decl.GetWholeSig());
                }
            }
        }

        /**
         * @brief Reconstruct from the file.
         */
        public override void ReadFromFile(BinaryReader objFileReader, TextWriter asmFileWriter)
        {
            instSizes.ReadFromFile(objFileReader);
            numVirtFuncs = objFileReader.ReadInt32();

            extendsStr = objFileReader.ReadString();
            arrayOfRank = objFileReader.ReadInt32();
            if(arrayOfRank > 0)
                arrayOfTypeStr = objFileReader.ReadString();

            if(asmFileWriter != null)
            {
                instSizes.WriteAsmFile(asmFileWriter, extendsStr + "." + shortName.val + ".numInst");
            }

            stackedMethods = new List<StackedMethod>();
            int vTableIndex;
            while((vTableIndex = objFileReader.ReadInt32()) >= 0)
            {
                StackedMethod sm;
                sm.methVTI = vTableIndex;
                sm.methName = objFileReader.ReadString();
                sm.methSig = objFileReader.ReadString();
                stackedMethods.Add(sm);
            }

            int numIFaces = objFileReader.ReadInt32();
            if(numIFaces > 0)
            {
                iDynMeths = new DynamicMethod[numIFaces][];
                iMethTypes = new Type[numIFaces][];
                stackedIFaces = new List<StackedIFace>();
                for(int i = 0; i < numIFaces; i++)
                {
                    string iFaceName = objFileReader.ReadString();
                    intfIndices[iFaceName] = i;
                    int numMeths = objFileReader.ReadInt32();
                    iDynMeths[i] = new DynamicMethod[numMeths];
                    iMethTypes[i] = new Type[numMeths];
                    for(int j = 0; j < numMeths; j++)
                    {
                        StackedIFace si;
                        si.iFaceIndex = i;
                        si.methIndex = j;
                        si.vTableIndex = objFileReader.ReadInt32();
                        si.methName = objFileReader.ReadString();
                        si.methSig = objFileReader.ReadString();
                        stackedIFaces.Add(si);
                    }
                }
            }
        }

        private struct StackedMethod
        {
            public int methVTI;
            public string methName;
            public string methSig;
        }

        private struct StackedIFace
        {
            public int iFaceIndex;   // which implemented interface
            public int methIndex;    // which method of that interface
            public int vTableIndex;  // <0: implemented by non-virtual; else: implemented by virtual
            public string methName;  // object code name of implementing method (GetObjCodeName)
            public string methSig;   // method signature incl return type (GetWholeSig)
        }

        /**
         * @brief Called after all dynamic method code has been generated to fill in vDynMeths and vMethTypes
         *        Also fills in iDynMeths, iMethTypes.
         */
        public void FillVTables(ScriptObjCode scriptObjCode)
        {
            if(extendsStr != null)
            {
                if(extendsStr != "")
                {
                    extends = (TokenDeclSDTypeClass)scriptObjCode.sdObjTypesName[extendsStr];
                    extends.FillVTables(scriptObjCode);
                }
                extendsStr = null;
            }
            if(arrayOfTypeStr != null)
            {
                arrayOfType = MakeTypeToken(arrayOfTypeStr);
                arrayOfTypeStr = null;
            }

            if((numVirtFuncs > 0) && (stackedMethods != null))
            {
                 // Allocate arrays big enough for mine plus type we are extending.
                vDynMeths = new DynamicMethod[numVirtFuncs];
                vMethTypes = new Type[numVirtFuncs];

                 // Fill in low parts from type we are extending.
                if(extends != null)
                {
                    int n = extends.numVirtFuncs;
                    for(int i = 0; i < n; i++)
                    {
                        vDynMeths[i] = extends.vDynMeths[i];
                        vMethTypes[i] = extends.vMethTypes[i];
                    }
                }

                 // Fill in high parts with my own methods.
                 // Might also overwrite lower ones with 'override' methods.
                foreach(StackedMethod sm in stackedMethods)
                {
                    int i = sm.methVTI;
                    string methName = sm.methName;
                    if (scriptObjCode.dynamicMethods.TryGetValue(methName, out DynamicMethod dm))
                    {
                        // method is not abstract
                        vDynMeths[i] = dm;
                        vMethTypes[i] = GetDynamicMethodDelegateType(dm, sm.methSig);
                    }
                }
                stackedMethods = null;
            }

            if(stackedIFaces != null)
            {
                foreach(StackedIFace si in stackedIFaces)
                {
                    int i = si.iFaceIndex;
                    int j = si.methIndex;
                    int vti = si.vTableIndex;
                    string methName = si.methName;
                    DynamicMethod dm = scriptObjCode.dynamicMethods[methName];
                    iDynMeths[i][j] = (vti < 0) ? dm : vDynMeths[vti];
                    iMethTypes[i][j] = GetDynamicMethodDelegateType(dm, si.methSig);
                }
                stackedIFaces = null;
            }
        }

        private Type GetDynamicMethodDelegateType(DynamicMethod dm, string methSig)
        {
            Type retType = dm.ReturnType;
            ParameterInfo[] pi = dm.GetParameters();
            Type[] argTypes = new Type[pi.Length];
            for(int j = 0; j < pi.Length; j++)
            {
                argTypes[j] = pi[j].ParameterType;
            }
            return DelegateCommon.GetType(retType, argTypes, methSig);
        }

        public override void DebString(StringBuilder sb)
        {
             // Don't output if array of some type.
             // They will be re-instantiated as referenced by rest of script.
            if(arrayOfType != null)
                return;

             // This class name and extended/implemented type declaration.
            sb.Append("class ");
            sb.Append(shortName.val);
            bool first = true;
            if(extends != null)
            {
                sb.Append(" : ");
                sb.Append(extends.longName);
                first = false;
            }
            foreach(TokenDeclSDType impld in implements)
            {
                sb.Append(first ? " : " : ", ");
                sb.Append(impld.longName);
                first = false;
            }
            sb.Append(" {");

             // Inner type definitions.
            foreach(TokenDeclSDType subs in innerSDTypes.Values)
            {
                subs.DebString(sb);
            }

             // Members (fields, methods, properties).
            foreach(TokenDeclVar memb in members)
            {
                if((memb == instFieldInit) || (memb == staticFieldInit))
                {
                    memb.DebStringInitFields(sb);
                }
                else if(memb.retType != null)
                {
                    memb.DebString(sb);
                }
            }

            sb.Append('}');
        }
    }

    public class TokenDeclSDTypeDelegate: TokenDeclSDType
    {
        private TokenType retType;
        private TokenType[] argTypes;

        private string argSig;
        private string wholeSig;
        private Type sysType;
        private Type retSysType;
        private Type[] argSysTypes;

        private string retStr;
        private string[] argStrs;

        private static Dictionary<string, TokenDeclSDTypeDelegate> inlines = new();
        private static Dictionary<Type, string> inlrevs = new();

        public TokenDeclSDTypeDelegate(TokenName shortName) : base(shortName)
        {
            this.shortName = shortName;
        }
        public void SetRetArgTypes(TokenType retType, TokenType[] argTypes)
        {
            this.retType = retType;
            this.argTypes = argTypes;
        }

        protected override TokenDeclSDType MakeBlank(TokenName shortName)
        {
            return new TokenDeclSDTypeDelegate(shortName);
        }

        public override TokenType MakeRefToken(Token t)
        {
            return new TokenTypeSDTypeDelegate(t, this);
        }

        /**
         * @brief Get system type for the whole delegate.
         */
        public override Type GetSysType()
        {
            if(sysType == null)
                FillInStuff();
            return sysType;
        }

        /**
         * @brief Get the function's return value type (TokenTypeVoid if void, never null)
         */
        public TokenType GetRetType()
        {
            if(retType == null)
                FillInStuff();
            return retType;
        }

        /**
         * @brief Get the function's argument types
         */
        public TokenType[] GetArgTypes()
        {
            if(argTypes == null)
                FillInStuff();
            return argTypes;
        }

        /**
         * @brief Get signature for the whole delegate, eg, "void(integer,list)"
         */
        public string GetWholeSig()
        {
            if(wholeSig == null)
                FillInStuff();
            return wholeSig;
        }

        /**
         * @brief Get signature for the arguments, eg, "(integer,list)"
         */
        public string GetArgSig()
        {
            if(argSig == null)
                FillInStuff();
            return argSig;
        }

        /**
         * @brief Find out how to create one of these delegates.
         */
        public ConstructorInfo GetConstructorInfo()
        {
            if(sysType == null)
                FillInStuff();
            return sysType.GetConstructor(DelegateCommon.constructorArgTypes);
        }

        /**
         * @brief Find out how to call what one of these delegates points to.
         */
        public MethodInfo GetInvokerInfo()
        {
            if(sysType == null)
                FillInStuff();
            return sysType.GetMethod("Invoke", argSysTypes);
        }

        /**
         * @brief Write enough out to a file so delegate can be reconstructed in ReadFromFile().
         */
        public override void WriteToFile(BinaryWriter objFileWriter)
        {
            objFileWriter.Write(this.file);
            objFileWriter.Write(this.line);
            objFileWriter.Write(this.posn);
            objFileWriter.Write((byte)DELEGATE);
            objFileWriter.Write(this.sdTypeIndex);

            objFileWriter.Write(retType.ToString());
            int nArgs = argTypes.Length;
            objFileWriter.Write(nArgs);
            for(int i = 0; i < nArgs; i++)
            {
                objFileWriter.Write(argTypes[i].ToString());
            }
        }

        /**
         * @brief Read that data from file so we can reconstruct.
         *        Don't actually reconstruct yet in case any forward-referenced types are undefined.
         */
        public override void ReadFromFile(BinaryReader objFileReader, TextWriter asmFileWriter)
        {
            retStr = objFileReader.ReadString();
            int nArgs = objFileReader.ReadInt32();
            asmFileWriter?.Write("  delegate " + retStr + " " + longName.val + "(");
            argStrs = new string[nArgs];
            for(int i = 0; i < nArgs; i++)
            {
                argStrs[i] = objFileReader.ReadString();
                if(asmFileWriter != null)
                {
                    if(i > 0)
                        asmFileWriter.Write(",");
                    asmFileWriter.Write(argStrs[i]);
                }
            }
            asmFileWriter?.WriteLine(");");
        }

        /**
         * @brief Fill in missing internal data.
         */
        private void FillInStuff()
        {
             // This happens when the node was restored via ReadFromFile().
             // It leaves the types in retStr/argStrs for resolution after
             // all definitions have been read from the object file in case
             // there are forward references.
            retType ??= MakeTypeToken(retStr);
            if(argTypes == null)
            {
                argTypes = new TokenType[argStrs.Length];
                for(int i = 0; i < argStrs.Length; i++)
                {
                    argTypes[i] = MakeTypeToken(argStrs[i]);
                }
            }

             // Fill in system types from token types.
             // Might as well build the signature strings too from token types.
            retSysType = retType.ToSysType();

            StringBuilder sb = new();
            sb.Append('(');
            argSysTypes = new Type[argTypes.Length];
            for(int i = 0; i < argTypes.Length; i++)
            {
                if(i > 0)
                    sb.Append(',');
                sb.Append(argTypes[i].ToString());
                argSysTypes[i] = argTypes[i].ToSysType();
            }
            sb.Append(')');
            argSig = sb.ToString();
            wholeSig = retType.ToString() + argSig;

             // Now we can create a system delegate type from the given
             // return and argument types.  Give it an unique name using
             // the whole signature string.
            sysType = DelegateCommon.GetType(retSysType, argSysTypes, wholeSig);
        }

        /**
         * @brief create delegate reference token for inline functions.
         *        there is just one instance of these per inline function
         *        shared by all scripts, and it is just used when the
         *        script engine is loaded.
         */
        public static TokenDeclSDTypeDelegate CreateInline(TokenType retType, TokenType[] argTypes)
        {
            // Name it after the whole signature string.
            StringBuilder sb = new("$inline");
            sb.Append(retType.ToString());
            sb.Append('(');
            bool first = true;
            foreach(TokenType at in argTypes)
            {
                if(!first)
                    sb.Append(',');
                sb.Append(at.ToString());
                first = false;
            }
            sb.Append(')');
            string inlname = sb.ToString();
            if(!inlines.TryGetValue(inlname, out TokenDeclSDTypeDelegate decldel))
            {
                 // Create the corresponding declaration and link to it
                TokenName name = new(null, inlname);
                decldel = new TokenDeclSDTypeDelegate(name)
                {
                    retType = retType,
                    argTypes = argTypes
                };
                inlines.Add(inlname, decldel);
                inlrevs.Add(decldel.GetSysType(), inlname);
            }
            return decldel;
        }

        public static string TryGetInlineName(Type sysType)
        {
            if(inlrevs.TryGetValue(sysType, out string name))
                return name;
            return null;
        }

        public static Type TryGetInlineSysType(string name)
        {
            if (inlines.TryGetValue(name, out TokenDeclSDTypeDelegate decl))
                return decl.GetSysType();
            return null;
        }
    }

    public class TokenDeclSDTypeInterface: TokenDeclSDType
    {
        public VarDict methsNProps = new(false);
        // any class that implements this interface
        // must implement all of these methods & properties

        public List<TokenDeclSDTypeInterface> implements = new();
        // any class that implements this interface
        // must also implement all of the methods & properties 
        // of all of these interfaces

        public TokenDeclSDTypeInterface(TokenName shortName) : base(shortName)
        {
            this.shortName = shortName;
        }

        protected override TokenDeclSDType MakeBlank(TokenName shortName)
        {
            return new TokenDeclSDTypeInterface(shortName);
        }

        public override TokenType MakeRefToken(Token t)
        {
            return new TokenTypeSDTypeInterface(t, this);
        }

        public override Type GetSysType()
        {
            // interfaces are implemented as arrays of delegates
            // they are taken from iDynMeths[interfaceIndex] of a script-defined class object
            return typeof(Delegate[]);
        }

        public override void WriteToFile(BinaryWriter objFileWriter)
        {
            objFileWriter.Write(this.file);
            objFileWriter.Write(this.line);
            objFileWriter.Write(this.posn);
            objFileWriter.Write((byte)INTERFACE);
            objFileWriter.Write(this.sdTypeIndex);
        }

        public override void ReadFromFile(BinaryReader objFileReader, TextWriter asmFileWriter)
        {
        }

        /**
         * @brief Add this interface to the list of interfaces implemented by a class if not already.
         *        And also add this interface's implemented interfaces to the class for those not already there, 
         *        just as if the class itself had declared to implement those interfaces.
         */
        public void AddToClassDecl(TokenDeclSDTypeClass tokdeclcl)
        {
            if(!tokdeclcl.implements.Contains(this))
            {
                tokdeclcl.implements.Add(this);
                foreach(TokenDeclSDTypeInterface subimpl in this.implements)
                {
                    subimpl.AddToClassDecl(tokdeclcl);
                }
            }
        }

        /**
         * @brief See if the 'this' interface implements the new interface.
         *        Do a recursive (deep) check.
         */
        public bool Implements(TokenDeclSDTypeInterface newDecl)
        {
            foreach(TokenDeclSDTypeInterface ii in this.implements)
            {
                if(ii == newDecl)
                    return true;
                if(ii.Implements(newDecl))
                    return true;
            }
            return false;
        }

        /**
         * @brief Scan an interface and all its implemented interfaces for a method or property
         * @param scg = script code generator (ie, which script is being compiled)
         * @param fieldName = name of the member being looked for
         * @param argsig = the method's argument signature
         * @returns null: no such member; intf = undefined
         *          else: member; intf = which interface actually found in
         */
        public TokenDeclVar FindIFaceMember(ScriptCodeGen scg, TokenName fieldName, TokenType[] argsig, out TokenDeclSDTypeInterface intf)
        {
            intf = this;
            TokenDeclVar var = scg.FindSingleMember(this.methsNProps, fieldName, argsig);
            if(var == null)
            {
                foreach(TokenDeclSDTypeInterface ii in this.implements)
                {
                    var = ii.FindIFaceMember(scg, fieldName, argsig, out intf);
                    if(var != null)
                        break;
                }
            }
            return var;
        }
    }

    public class TokenDeclSDTypeTypedef: TokenDeclSDType
    {

        public TokenDeclSDTypeTypedef(TokenName shortName) : base(shortName)
        {
            this.shortName = shortName;
        }

        protected override TokenDeclSDType MakeBlank(TokenName shortName)
        {
            return new TokenDeclSDTypeTypedef(shortName);
        }

        public override TokenType MakeRefToken(Token t)
        {
            // if our body is a single type token, that is what we return
            // otherwise return null saying maybe our body needs some substitutions
            if(nextToken is not TokenType)
                return null;
            if(nextToken.nextToken != this.endToken)
            {
                nextToken.nextToken.ErrorMsg("extra tokens for typedef");
                return null;
            }
            return (TokenType)nextToken.CopyToken(t);
        }

        public override Type GetSysType()
        {
            // we are just a macro
            // we are asked for system type because we are cataloged
            // but we don't really have one so return null
            return null;
        }

        public override void WriteToFile(BinaryWriter objFileWriter)
        {
            objFileWriter.Write(this.file);
            objFileWriter.Write(this.line);
            objFileWriter.Write(this.posn);
            objFileWriter.Write((byte)TYPEDEF);
            objFileWriter.Write(this.sdTypeIndex);
        }

        public override void ReadFromFile(BinaryReader objFileReader, TextWriter asmFileWriter)
        {
        }
    }

    /**
     * @brief Script-defined type references.
     *        These occur in the source code wherever it specifies (eg, variable declaration) a script-defined type.
     *        These must be copyable via CopyToken().
     */
    public abstract class TokenTypeSDType: TokenType
    {
        public TokenTypeSDType(TokenErrorMessage emsg, string file, int line, int posn) : base(emsg, file, line, posn) { }
        public TokenTypeSDType(Token t) : base(t) { }
        public abstract TokenDeclSDType GetDecl();
        public abstract void SetDecl(TokenDeclSDType decl);
    }

    public class TokenTypeSDTypeClass: TokenTypeSDType
    {
        private static readonly FieldInfo iarSDTClObjsFieldInfo = typeof(XMRInstArrays).GetField("iarSDTClObjs");

        public TokenDeclSDTypeClass decl;

        public TokenTypeSDTypeClass(Token t, TokenDeclSDTypeClass decl) : base(t)
        {
            this.decl = decl;
        }
        public override TokenDeclSDType GetDecl()
        {
            return decl;
        }
        public override void SetDecl(TokenDeclSDType decl)
        {
            this.decl = (TokenDeclSDTypeClass)decl;
        }
        public override string ToString()
        {
            return decl.longName.val;
        }
        public override Type ToSysType()
        {
            return typeof(XMRSDTypeClObj);
        }

        public override void AssignVarSlot(TokenDeclVar declVar, XMRInstArSizes ias)
        {
            declVar.vTableArray = iarSDTClObjsFieldInfo;
            declVar.vTableIndex = ias.iasSDTClObjs++;
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append(decl.longName);
        }
    }

    public class TokenTypeSDTypeDelegate: TokenTypeSDType
    {
        private static readonly FieldInfo iarObjectsFieldInfo = typeof(XMRInstArrays).GetField("iarObjects");

        public TokenDeclSDTypeDelegate decl;

        /**
         * @brief create a reference to an explicitly declared delegate
         * @param t = where the reference is being made in the source file
         * @param decl = the explicit delegate declaration
         */
        public TokenTypeSDTypeDelegate(Token t, TokenDeclSDTypeDelegate decl) : base(t)
        {
            this.decl = decl;
        }
        public override TokenDeclSDType GetDecl()
        {
            return decl;
        }
        public override void SetDecl(TokenDeclSDType decl)
        {
            this.decl = (TokenDeclSDTypeDelegate)decl;
        }

        /**
         * @brief create a reference to a possibly anonymous delegate
         * @param t = where the reference is being made in the source file
         * @param retType = return type (TokenTypeVoid if void, never null)
         * @param argTypes = script-visible argument types
         * @param tokenScript = what script this is part of
         */
        public TokenTypeSDTypeDelegate(Token t, TokenType retType, TokenType[] argTypes, TokenScript tokenScript) : base(t)
        {
            TokenDeclSDTypeDelegate decldel;

             // See if we already have a matching declared one cataloged.
            int nArgs = argTypes.Length;
            foreach(TokenDeclSDType decl in tokenScript.sdSrcTypesValues)
            {
                if(decl is TokenDeclSDTypeDelegate decldelg)
                {
                    TokenType rt = decldelg.GetRetType();
                    TokenType[] ats = decldelg.GetArgTypes();
                    if((rt.ToString() == retType.ToString()) && (ats.Length == nArgs))
                    {
                        for(int i = 0; i < nArgs; i++)
                        {
                            if(ats[i].ToString() != argTypes[i].ToString())
                                goto nomatch;
                        }
                        this.decl = decldelg;
                        return;
                    }
                }
                nomatch:
                ;
            }

             // No such luck, create a new anonymous declaration.
            StringBuilder sb = new("$anondel$");
            sb.Append(retType.ToString());
            sb.Append('(');
            bool first = true;
            foreach(TokenType at in argTypes)
            {
                if(!first)
                    sb.Append(',');
                sb.Append(at.ToString());
                first = false;
            }
            sb.Append(')');
            TokenName name = new(t, sb.ToString());
            decldel = new TokenDeclSDTypeDelegate(name);
            decldel.SetRetArgTypes(retType, argTypes);
            tokenScript.sdSrcTypesAdd(name.val, decldel);
            this.decl = decldel;
        }

        public override Type ToSysType()
        {
            return decl.GetSysType();
        }

        public override string ToString()
        {
            return decl.longName.val;
        }

        /**
         * @brief Assign slots in the gblObjects[] array because we have to typecast out in any case.
         *        Likewise with the sdtcObjects[] array.
         */
        public override void AssignVarSlot(TokenDeclVar declVar, XMRInstArSizes ias)
        {
            declVar.vTableArray = iarObjectsFieldInfo;
            declVar.vTableIndex = ias.iasObjects++;
        }

        /**
         * @brief create delegate reference token for inline functions.
         */
        public TokenTypeSDTypeDelegate(TokenType retType, TokenType[] argTypes) : base(null)
        {
            this.decl = TokenDeclSDTypeDelegate.CreateInline(retType, argTypes);
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append(decl.longName);
        }
    }

    public class TokenTypeSDTypeInterface: TokenTypeSDType
    {
        private static readonly FieldInfo iarSDTIntfObjsFieldInfo = typeof(XMRInstArrays).GetField("iarSDTIntfObjs");

        public TokenDeclSDTypeInterface decl;

        public TokenTypeSDTypeInterface(Token t, TokenDeclSDTypeInterface decl) : base(t)
        {
            this.decl = decl;
        }
        public override TokenDeclSDType GetDecl()
        {
            return decl;
        }
        public override void SetDecl(TokenDeclSDType decl)
        {
            this.decl = (TokenDeclSDTypeInterface)decl;
        }

        public override string ToString()
        {
            return decl.longName.val;
        }
        public override Type ToSysType()
        {
            return typeof(Delegate[]);
        }

        /**
         * @brief Assign slots in the gblSDTIntfObjs[] array
         *        Likewise with the sdtcSDTIntfObjs[] array.
         */
        public override void AssignVarSlot(TokenDeclVar declVar, XMRInstArSizes ias)
        {
            declVar.vTableArray = iarSDTIntfObjsFieldInfo;
            declVar.vTableIndex = ias.iasSDTIntfObjs++;
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append(decl.longName);
        }
    }

    /**
     * @brief function argument list declaration
     */
    public class TokenArgDecl: Token
    {
        public VarDict varDict = new (false);

        public TokenArgDecl(Token original) : base(original) { }

        public bool AddArg(TokenType type, TokenName name)
        {
            TokenDeclVar var = new (name, null, null)
            {
                name = name,
                type = type,
                vTableIndex = varDict.Count
            };
            return varDict.AddEntry(var);
        }

        /**
         * @brief Get an array of the argument types.
         */
        private TokenType[] _types;
        public TokenType[] types
        {
            get
            {
                if(_types == null)
                {
                    _types = new TokenType[varDict.Count];
                    foreach(TokenDeclVar var in varDict)
                    {
                        _types[var.vTableIndex] = var.type;
                    }
                }
                return _types;
            }
        }

        /**
         * @brief Access the arguments as an array of variables.
         */
        private TokenDeclVar[] _vars;
        public TokenDeclVar[] vars
        {
            get
            {
                if(_vars == null)
                {
                    _vars = new TokenDeclVar[varDict.Count];
                    foreach(TokenDeclVar var in varDict)
                    {
                        _vars[var.vTableIndex] = var;
                    }
                }
                return _vars;
            }
        }

        /**
         * @brief Get argument signature string, eg, "(list,vector,integer)"
         */
        private string argSig = null;
        public string GetArgSig()
        {
            argSig ??= ScriptCodeGen.ArgSigString(types);
            return argSig;
        }
    }

    /**
     * @brief encapsulate a state declaration in a single token
     */
    public class TokenDeclState: Token
    {

        public TokenName name;  // null for default state
        public TokenStateBody body;

        public TokenDeclState(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            if(name == null)
            {
                sb.Append("default");
            }
            else
            {
                sb.Append("state ");
                sb.Append(name);
            }
            body.DebString(sb);
        }
    }

    /**
     * @brief encapsulate the declaration of a field/function/method/property/variable.
     */

    public enum Triviality
    {  // function triviality: has no loops and doesn't call anything that has loops
       // such a function does not need all the CheckRun() and stack serialization stuff
        unknown,   // function's Triviality unknown as of yet
                   // - it does not have any loops or backward gotos
                   // - nothing it calls is known to be complex
        trivial,   // function known to be trivial
                   // - it does not have any loops or backward gotos
                   // - everything it calls is known to be trivial
        complex,   // function known to be complex
                   // - it has loops or backward gotos
                   // - something it calls is known to be complex
        analyzing  // triviality is being analyzed (used to detect recursive loops)
    };

    public class TokenDeclVar: TokenStmt
    {
        public TokenName name;                 // vars: name; funcs: bare name, ie, no signature
        public TokenRVal init;                 // vars: null if none; funcs: null
        public bool constant;                  // vars: 'constant'; funcs: false
        public uint sdtFlags;                  // SDT_<*> flags

        public CompValu location;              // used by codegen to keep track of location
        public FieldInfo vTableArray;
        public int vTableIndex = -1;           // local vars: not used (-1)
                                               // arg vars: index in the arg list
                                               // global vars: which slot in gbl<Type>s[] array it is stored
                                               // instance vars: which slot in inst<Types>s[] array it is stored
                                               // static vars: which slot in gbl<Type>s[] array it is stored
                                               // global funcs: not used (-1)
                                               // virt funcs: which slot in vTable[] array it is stored
                                               // instance func: not used (-1)
        public TokenDeclVar getProp;           // if property, function that reads value
        public TokenDeclVar setProp;           // if property, function that writes value

        public TokenScript tokenScript;        // what script this function is part of
        public TokenDeclSDType sdtClass;       // null: script global member
                                               // else: member is part of this script-defined type

        // function-only data:

        public TokenType retType;              // vars: null; funcs: TokenTypeVoid if void
        public TokenArgDecl argDecl;           // vars: null; funcs: argument list prototypes
        public TokenStmtBlock body;            // vars: null; funcs: statements (null iff abstract)
        public Dictionary<string, TokenStmtLabel> labels = new();
        // all labels defined in the function
        public LinkedList<TokenDeclVar> localVars = new();
        // all local variables declared by this function
        // - doesn't include argument variables
        public TokenIntfImpl implements;       // if script-defined type method, what interface method(s) this func implements
        public TokenRValCall baseCtorCall;     // if script-defined type constructor, call to base constructor, if any
        public Triviality triviality = Triviality.unknown;
        // vars: unknown (not used for any thing); funcs: unknown/trivial/complex
        public LinkedList<TokenRValCall> unknownTrivialityCalls = new();
        // reduction puts all calls here
        // compilation sorts it all out

        public ScriptObjWriter ilGen;          // codegen stores emitted code here

        /**
         * @brief Set up a variable declaration token.
         * @param original = original source token that triggered definition
         *                   (for error messages)
         * @param func = null: global variable
         *               else: local to the given function
         */
        public TokenDeclVar(Token original, TokenDeclVar func, TokenScript ts) : base(original)
        {
            func?.localVars.AddLast(this);
            tokenScript = ts;
        }

        /**
         * @brief Get/Set overall type
         *        For vars, this is the type of the location
         *        For funcs, this is the delegate type
         */
        private TokenType _type;
        public TokenType type
        {
            get
            {
                if(_type == null)
                {
                    GetDelType();
                }
                return _type;
            }
            set
            {
                _type = value;
            }
        }

        /**
         * @brief Full name: <fulltype>.<name>(<argsig>)
         *        (<argsig>) missing for fields/variables
         *        <fulltype>. missing for top-level functions/variables
         */
        public string fullName
        {
            get
            {
                if(sdtClass == null)
                {
                    if(retType == null)
                        return name.val;
                    return funcNameSig.val;
                }
                if(retType == null)
                    return sdtClass.longName.val + "." + name.val;
                return sdtClass.longName.val + "." + funcNameSig.val;
            }
        }

        /**
         * @brief See if reading or writing the variable is trivial.
         *        Note that for functions, this is reading the function itself, 
         *        as in 'someDelegate = SomeFunction;', not calling it as such.
         *        The triviality of actually calling the function is IsFuncTrivial().
         */
        public bool IsVarTrivial(ScriptCodeGen scg)
        {
            // reading or writing a property involves a function call however
            // so we need to check the triviality of the property functions
            if((getProp != null) && !getProp.IsFuncTrivial(scg))
                return false;
            if((setProp != null) && !setProp.IsFuncTrivial(scg))
                return false;

            // otherwise for variables it is a trivial access
            // and likewise for getting a delegate that points to a function
            return true;
        }

        /***************************\
         *  FUNCTION-only methods  *
        \***************************/

        private TokenName _funcNameSig;  // vars: null; funcs: function name including argumet signature, eg, "PrintStuff(list,string)"
        public TokenName funcNameSig
        {
            get
            {
                if(_funcNameSig == null)
                {
                    if(argDecl == null)
                        return null;
                    _funcNameSig = new TokenName(name, name.val + argDecl.GetArgSig());
                }
                return _funcNameSig;
            }
        }

        /**
         * @brief The bare function name, ie, without any signature info
         */
        public string GetSimpleName()
        {
            return name.val;
        }

        /**
         * @brief The function name as it appears in the object code,
         *        ie, script-defined type name if any,
         *        bare function name and argument signature, 
         *        eg, "MyClass.PrintStuff(string)"
         */
        public string GetObjCodeName()
        {
            if(sdtClass != null)
            {
                return sdtClass.longName.val + "." + funcNameSig.val;
            }
            return funcNameSig.val;
        }

        /**
         * @brief Get delegate type.
         *        This is the function's script-visible type,
         *        It includes return type and all script-visible argument types.
         * @returns null for vars; else delegate type for funcs
         */
        public TokenTypeSDTypeDelegate GetDelType()
        {
            if(argDecl == null)
                return null;
            if(_type == null)
            {
                if(tokenScript == null)
                {
                    // used during startup to define inline function delegate types
                    _type = new TokenTypeSDTypeDelegate(retType, argDecl.types);
                }
                else
                {
                    // used for normal script processing
                    _type = new TokenTypeSDTypeDelegate(this, retType, argDecl.types, tokenScript);
                }
            }
            if(_type is TokenTypeSDTypeDelegate TokenTypeSDTypeDelegate_type)
                return TokenTypeSDTypeDelegate_type;
            return null;
        }

        /**
         * @brief See if the function's code itself is trivial or not.
         *        If it contains any loops (calls to CheckRun()), it is not trivial.
         *        If it calls anything that is not trivial, it is not trivial.
         *        Otherwise it is trivial.
         */
        public bool IsFuncTrivial(ScriptCodeGen scg)
        {
             // If not really a function, assume it's a delegate.
             // And since we don't really know what functions it can point to, 
             // assume it can point to a non-trivial one.
            if(retType == null)
                return false;

             // All virtual functions are non-trivial because although a particular 
             // one might be trivial, it might be overidden with a non-trivial one.
            if((sdtFlags & (ScriptReduce.SDT_ABSTRACT | ScriptReduce.SDT_OVERRIDE |
                             ScriptReduce.SDT_VIRTUAL)) != 0)
            {
                return false;
            }

             // Check the triviality status of the function.
            switch(triviality)
            {
                 // Don't yet know if it is trivial.
                 // We know at this point it doesn't have any direct looping.
                 // But if it calls something that has loops, it isn't trivial.
                 // Otherwise it is trivial.
                case Triviality.unknown:
                    {
                         // Mark that we are analyzing this function now.  So if there are 
                         // any recursive call loops, that will show that the function is 
                         // non-trivial and the analysis will terminate at that point.
                        triviality = Triviality.analyzing;

                         // Check out everything else this function calls.  If any say they 
                         // aren't trivial, then we say this function isn't trivial.
                        foreach(TokenRValCall call in unknownTrivialityCalls)
                        {
                            if(!call.IsRValTrivial(scg, null))
                            {
                                triviality = Triviality.complex;
                                return false;
                            }
                        }

                         // All functions called by this function are trivial, and this 
                         // function's code doesn't have any loops, so we can mark this 
                         // function as being trivial.
                        triviality = Triviality.trivial;
                        return true;
                    }

                 // We already know that it is trivial.
                case Triviality.trivial:
                    {
                        return true;
                    }

                 // We either know it is complex or are trying to analyze it already.
                 // If we are already analyzing it, it means it has a recursive loop
                 // and we assume those are non-trivial.
                default:
                    return false;
            }
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            DebStringSDTFlags(sb);

            if(retType == null)
            {
                sb.Append(constant ? "constant" : type.ToString());
                sb.Append(' ');
                sb.Append(name.val);
                if(init != null)
                {
                    sb.Append(" = ");
                    init.DebString(sb);
                }
                sb.Append(';');
            }
            else
            {
                if(retType is not TokenTypeVoid)
                {
                    sb.Append(retType.ToString());
                    sb.Append(' ');
                }
                string namestr = name.val;
                if(namestr == "$ctor")
                    namestr = "constructor";
                sb.Append(namestr);
                sb.Append(" (");
                for(int i = 0; i < argDecl.vars.Length; i++)
                {
                    if(i > 0)
                        sb.Append(", ");
                    sb.Append(argDecl.vars[i].type.ToString());
                    sb.Append(' ');
                    sb.Append(argDecl.vars[i].name.val);
                }
                sb.Append(')');
                if(body == null)
                    sb.Append(';');
                else
                {
                    sb.Append(' ');
                    body.DebString(sb);
                }
            }
        }

        // debugging
        // - used to output contents of a $globalvarinit(), $instfieldinit() or $statisfieldinit() function
        //   as a series of variable declaration statements with initial value assignments
        //   so we get the initial value assignments done in same order as specified in script
        public void DebStringInitFields(StringBuilder sb)
        {
            if(retType is not TokenTypeVoid)
                throw new Exception("bad return type " + retType.GetType().Name);
            if(argDecl.vars.Length != 0)
                throw new Exception("has " + argDecl.vars.Length + " arg(s)");

            for(Token stmt = body.statements; stmt != null; stmt = stmt.nextToken)
            {
                 // Body of the function should all be arithmetic statements (not eg for loops, if statements etc).
                TokenRVal rval = ((TokenStmtRVal)stmt).rVal;

                 // And the opcode should be a simple assignment operator.
                TokenRValOpBin rvob = (TokenRValOpBin)rval;
                if(rvob.opcode is not TokenKwAssign)
                    throw new Exception("bad op type " + rvob.opcode.GetType().Name);

                 // Get field or variable being assigned to.
                TokenDeclVar tdvar = null;
                TokenRVal left = rvob.rValLeft;
                if(left is TokenLValIField ifield)
                {
                    TokenRValThis zhis = (TokenRValThis)ifield.baseRVal;
                    TokenDeclSDTypeClass sdt = zhis.sdtClass;
                    tdvar = sdt.members.FindExact(ifield.fieldName.val, null);
                }
                if(left is TokenLValName global)
                {
                    tdvar = global.stack.FindExact(global.name.val, null);
                }
                if(left is TokenLValSField sfield)
                {
                    TokenTypeSDTypeClass sdtc = (TokenTypeSDTypeClass)sfield.baseType;
                    TokenDeclSDTypeClass decl = sdtc.decl;
                    tdvar = decl.members.FindExact(sfield.fieldName.val, null);
                }
                if(tdvar == null)
                    throw new Exception("unknown var type " + left.GetType().Name);

                 // Output flags, type name and bare variable name.
                 // This should look like a declaration in the 'sb'
                 // as it is not enclosed in a function.
                tdvar.DebStringSDTFlags(sb);
                tdvar.type.DebString(sb);
                sb.Append(' ');
                sb.Append(tdvar.name.val);

                 // Maybe it has a non-default initialization value.
                if((tdvar.init != null) && tdvar.init is not TokenRValInitDef)
                {
                    sb.Append(" = ");
                    tdvar.init.DebString(sb);
                }

                 // End of declaration statement.
                sb.Append(';');
            }
        }

        private void DebStringSDTFlags(StringBuilder sb)
        {
            if((sdtFlags & ScriptReduce.SDT_PRIVATE) != 0)
                sb.Append("private ");
            if((sdtFlags & ScriptReduce.SDT_PROTECTED) != 0)
                sb.Append("protected ");
            if((sdtFlags & ScriptReduce.SDT_PUBLIC) != 0)
                sb.Append("public ");
            if((sdtFlags & ScriptReduce.SDT_ABSTRACT) != 0)
                sb.Append("abstract ");
            if((sdtFlags & ScriptReduce.SDT_FINAL) != 0)
                sb.Append("final ");
            if((sdtFlags & ScriptReduce.SDT_NEW) != 0)
                sb.Append("new ");
            if((sdtFlags & ScriptReduce.SDT_OVERRIDE) != 0)
                sb.Append("override ");
            if((sdtFlags & ScriptReduce.SDT_STATIC) != 0)
                sb.Append("static ");
            if((sdtFlags & ScriptReduce.SDT_VIRTUAL) != 0)
                sb.Append("virtual ");
        }
    }

    /**
     * @brief Indicates an interface type.method that is implemented by the function
     */
    public class TokenIntfImpl: Token
    {
        public TokenTypeSDTypeInterface intfType;
        public TokenName methName;  // simple name, no arg signature

        public TokenIntfImpl(TokenTypeSDTypeInterface intfType, TokenName methName) : base(intfType)
        {
            this.intfType = intfType;
            this.methName = methName;
        }
    }

    /**
     * @brief any expression that can go on left side of an "="
     */
    public abstract class TokenLVal: TokenRVal
    {
        public TokenLVal(Token original) : base(original) { }
        public abstract override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig);
        public abstract override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig);
    }

    /**
     * @brief an element of an array is an L-value
     */
    public class TokenLValArEle: TokenLVal
    {
        public TokenRVal baseRVal;
        public TokenRVal subRVal;

        public TokenLValArEle(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenType baseType = baseRVal.GetRValType(scg, null);

             // Maybe referencing element of a fixed-dimension array.
            if((baseType is TokenTypeSDTypeClass bbtype) && bbtype.decl.arrayOfType != null)
            {
                return bbtype.decl.arrayOfType;
            }

             // Maybe referencing $idxprop property of script-defined class or interface.
            if(baseType is TokenTypeSDTypeClass bc)
            {
                TokenDeclSDTypeClass sdtDecl = bc.decl;
                TokenDeclVar idxProp = scg.FindSingleMember(sdtDecl.members, new TokenName(this, "$idxprop"), null);
                if(idxProp != null)
                    return idxProp.type;
            }
            if(baseType is TokenTypeSDTypeInterface bi)
            {
                TokenDeclSDTypeInterface sdtDecl = bi.decl;
                TokenDeclVar idxProp = sdtDecl.FindIFaceMember(scg, new TokenName(this, "$idxprop"), null, out sdtDecl);
                if(idxProp != null)
                    return idxProp.type;
            }

             // Maybe referencing single character of a string.
            if((baseType is TokenTypeKey) || (baseType is TokenTypeStr))
            {
                return new TokenTypeChar(this);
            }

             // Assume XMR_Array element or extracting element from list.
            if((baseType is TokenTypeArray) || (baseType is TokenTypeList))
            {
                return new TokenTypeObject(this);
            }

            scg.ErrorMsg(this, "unknown array reference");
            return new TokenTypeVoid(this);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return baseRVal.IsRValTrivial(scg, null) && subRVal.IsRValTrivial(scg, null);
        }

        public override void DebString(StringBuilder sb)
        {
            baseRVal.DebString(sb);
            sb.Append('[');
            subRVal.DebString(sb);
            sb.Append(']');
        }
    }

    /**
     * @brief 'base.' being used to reference a field/method of the extended class.
     */
    public class TokenLValBaseField: TokenLVal
    {
        public TokenName fieldName;
        private TokenDeclSDTypeClass thisClass;

        public TokenLValBaseField(Token original, TokenName fieldName, TokenDeclSDTypeClass thisClass) : base(original)
        {
            this.fieldName = fieldName;
            this.thisClass = thisClass;
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenDeclVar var = scg.FindThisMember(thisClass.extends, fieldName, argsig);
            if(var != null)
                return var.type;
            scg.ErrorMsg(fieldName, "unknown member of " + thisClass.extends.ToString());
            return new TokenTypeVoid(fieldName);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenDeclVar var = scg.FindThisMember(thisClass.extends, fieldName, argsig);
            return (var != null) && var.IsVarTrivial(scg);
        }

        public override bool IsCallTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenDeclVar var = scg.FindThisMember(thisClass.extends, fieldName, argsig);
            return (var != null) && var.IsFuncTrivial(scg);
        }
    }

    /**
     * @brief a field within an struct is an L-value
     */
    public class TokenLValIField: TokenLVal
    {
        public TokenRVal baseRVal;
        public TokenName fieldName;

        public TokenLValIField(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenType baseType = baseRVal.GetRValType(scg, null);
            if(baseType is TokenTypeSDTypeClass TokenTypeSDTypeClassbaseType)
            {
                TokenDeclVar tvar = scg.FindThisMember(TokenTypeSDTypeClassbaseType, fieldName, argsig);
                if(tvar != null)
                    return tvar.type;
            }
            if(baseType is TokenTypeSDTypeInterface TokenTypeSDTypeInterfacebaseType)
            {
                TokenDeclSDTypeInterface baseIntfDecl = (TokenTypeSDTypeInterfacebaseType).decl;
                TokenDeclVar tvar = baseIntfDecl.FindIFaceMember(scg, fieldName, argsig, out baseIntfDecl);
                if(tvar != null)
                    return tvar.type;
            }
            if(baseType is TokenTypeArray)
            {
                return XMR_Array.GetRValType(fieldName);
            }
            if((baseType is TokenTypeRot) || (baseType is TokenTypeVec))
            {
                return new TokenTypeFloat(fieldName);
            }
            scg.ErrorMsg(fieldName, "unknown member of " + baseType.ToString());
            return new TokenTypeVoid(fieldName);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
             // If getting pointer to instance isn't trivial, then accessing the member isn't trivial either.
            if(!baseRVal.IsRValTrivial(scg, null))
                return false;

             // Accessing a member of a class depends on the member.
             // In the case of a method, this is accessing it as a delegate, not calling it, and 
             // argsig simply serves as selecting which of possibly overloaded methods to select.
             // The case of accessing a property, however, depends on the property implementation, 
             // as there could be looping inside the property code.
            TokenType baseType = baseRVal.GetRValType(scg, null);
            if(baseType is TokenTypeSDTypeClass TokenTypeSDTypeClassbaseType)
            {
                TokenDeclVar tvar = scg.FindThisMember(TokenTypeSDTypeClassbaseType, fieldName, argsig);
                return (tvar != null) && tvar.IsVarTrivial(scg);
            }

             // Accessing the members of anything else (arrays, rotations, vectors) is always trivial.
            return true;
        }

        /**
         * @brief Check to see if the case of calling an instance method of some object is trivial.
         * @param scg = script making the call
         * @param argsig = argument types of the call (used to select among overloads)
         * @returns true iff we can tell at compile time that the call will always call a trivial method
         */
        public override bool IsCallTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
             // If getting pointer to instance isn't trivial, then calling the method isn't trivial either.
            if(!baseRVal.IsRValTrivial(scg, null))
                return false;

             // Calling a method of a class depends on the method.
            TokenType baseType = baseRVal.GetRValType(scg, null);
            if(baseType is TokenTypeSDTypeClass TokenTypeSDTypeClassbaseType)
            {
                TokenDeclVar tvar = scg.FindThisMember(TokenTypeSDTypeClassbaseType, fieldName, argsig);
                return (tvar != null) && tvar.IsFuncTrivial(scg);
            }

             // Calling via a pointer to an interface instance is never trivial.
             // (It is really a pointer to an array of delegates).
             // We can't tell for this call site whether the actual method being called is trivial or not,
             // so we have to assume it isn't.
             // ??? We could theoretically check to see if *all* implementations of this method of 
             //     this interface are trivial, then we could conclude that this call is trivial.
            if(baseType is TokenTypeSDTypeInterface)
                return false;

             // Calling a method of anything else (arrays, rotations, vectors) is always trivial.
             // Even methods of delegates, such as ".GetArgTypes()" that we may someday do is trivial.
            return true;
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            baseRVal.DebString(sb);
            sb.Append('.');
            sb.Append(fieldName.val);
        }
    }

    /**
     * @brief a name is being used as an L-value
     */
    public class TokenLValName: TokenLVal
    {
        public TokenName name;
        public VarDict stack;

        public TokenLValName(TokenName name, VarDict stack) : base(name)
        {
             // Save name of variable/method/function/field.
            this.name = name;

             // Save where in the stack it can be looked up.
             // If the current stack is for locals, do not allow forward references.
             //     this allows idiocy like:
             //         list buttons = [ 1, 2, 3 ];
             //         x () {
             //             list buttons = llList2List (buttons, 0, 1);
             //             llOwnerSay (llList2CSV (buttons));
             //         }
             // If it is not locals, allow forward references.
             //     this allows function X() to call Y() and Y() to call X().
            this.stack = stack.FreezeLocals();
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenDeclVar var = scg.FindNamedVar(this, argsig);
            if(var != null)
                return var.type;
            scg.ErrorMsg(name, "undefined name " + name.val + ScriptCodeGen.ArgSigString(argsig));
            return new TokenTypeVoid(name);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenDeclVar var = scg.FindNamedVar(this, argsig);
            return (var != null) && var.IsVarTrivial(scg);
        }

        /**
         * @brief Check to see if the case of calling a global method is trivial.
         * @param scg = script making the call
         * @param argsig = argument types of the call (used to select among overloads)
         * @returns true iff we can tell at compile time that the call will always call a trivial method
         */
        public override bool IsCallTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenDeclVar var = scg.FindNamedVar(this, argsig);
            return (var != null) && var.IsFuncTrivial(scg);
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append(name.val);
        }
    }

    /**
     * @brief a static field within a struct is an L-value
     */
    public class TokenLValSField: TokenLVal
    {
        public TokenType baseType;
        public TokenName fieldName;

        public TokenLValSField(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            if(baseType is TokenTypeSDTypeClass TokenTypeSDTypeClassbaseType)
            {
                TokenDeclVar var = scg.FindThisMember(TokenTypeSDTypeClassbaseType, fieldName, argsig);
                if(var != null)
                    return var.type;
            }
            scg.ErrorMsg(fieldName, "unknown member of " + baseType.ToString());
            return new TokenTypeVoid(fieldName);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
             // Accessing a member of a class depends on the member.
             // In the case of a method, this is accessing it as a delegate, not calling it, and 
             // argsig simply serves as selecting which of possibly overloaded methods to select.
             // The case of accessing a property, however, depends on the property implementation, 
             // as there could be looping inside the property code.
            if(baseType is TokenTypeSDTypeClass TokenTypeSDTypeClassbaseType)
            {
                TokenDeclVar tvar = scg.FindThisMember(TokenTypeSDTypeClassbaseType, fieldName, argsig);
                return (tvar != null) && tvar.IsVarTrivial(scg);
            }

             // Accessing the fields/methods/properties of anything else (arrays, rotations, vectors) is always trivial.
            return true;
        }

        /**
         * @brief Check to see if the case of calling a class' static method is trivial.
         * @param scg = script making the call
         * @param argsig = argument types of the call (used to select among overloads)
         * @returns true iff we can tell at compile time that the call will always call a trivial method
         */
        public override bool IsCallTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
             // Calling a static method of a class depends on the method.
            if(baseType is TokenTypeSDTypeClass TokenTypeSDTypeClassbaseType)
            {
                TokenDeclVar tvar = scg.FindThisMember(TokenTypeSDTypeClassbaseType, fieldName, argsig);
                return (tvar != null) && tvar.IsFuncTrivial(scg);
            }

             // Calling a static method of anything else (arrays, rotations, vectors) is always trivial.
            return true;
        }

        public override void DebString(StringBuilder sb)
        {
            if(fieldName.val == "$new")
            {
                sb.Append("new ");
                baseType.DebString(sb);
            }
            else
            {
                baseType.DebString(sb);
                sb.Append('.');
                fieldName.DebString(sb);
            }
        }
    }

    /**
     * @brief any expression that can go on right side of "="
     */
    public delegate TokenRVal TCCLookup(TokenRVal rVal, ref bool didOne);
    public abstract class TokenRVal: Token
    {
        public TokenRVal(Token original) : base(original) { }

        /**
         * @brief Tell us the type of the expression.
         */
        public abstract TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig);

        /**
         * @brief Tell us if reading and writing the value is trivial.
         *
         * @param scg = script code generator of script making the access
         * @param argsig = argument types of the call (used to select among overloads)
         * @returns true: we can tell at compile time that reading/writing this location
         *                will always be trivial (no looping or CheckRun() calls possible).
         *         false: otherwise
         */
        public abstract bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig);

        /**
         * @brief Tell us if calling the method is trivial.
         *
         *        This is the default implementation that returns false.
         *        It is only used if the location is holding a delegate
         *        and the method that the delegate is pointing to is being
         *        called.  Since we can't tell if the actual runtime method
         *        is trivial or not, we assume it isn't.
         *
         *        For the more usual ways of calling functions, see the
         *        various overrides of IsCallTrivial().
         *
         * @param scg = script code generator of script making the call
         * @param argsig = argument types of the call (used to select among overloads)
         * @returns true: we can tell at compile time that this call will always
         *                be to a trivial function/method (no looping or CheckRun() 
         *                calls possible).
         *         false: otherwise
         */
        public virtual bool IsCallTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return false;
        }

        /**
         * @brief If the result of the expression is a constant,
         *        create a TokenRValConst equivalent, set didOne, and return that.
         *        Otherwise, just return the original without changing didOne.
         */
        public virtual TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            return lookup(this, ref didOne);
        }
    }

    /**
     * @brief a postfix operator and corresponding L-value
     */
    public class TokenRValAsnPost: TokenRVal
    {
        public TokenLVal lVal;
        public Token postfix;

        public TokenRValAsnPost(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return lVal.GetRValType(scg, argsig);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return lVal.IsRValTrivial(scg, null);
        }

        public override void DebString(StringBuilder sb)
        {
            lVal.DebString(sb);
            sb.Append(' ');
            postfix.DebString(sb);
        }
    }

    /**
     * @brief a prefix operator and corresponding L-value
     */
    public class TokenRValAsnPre: TokenRVal
    {
        public Token prefix;
        public TokenLVal lVal;

        public TokenRValAsnPre(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return lVal.GetRValType(scg, argsig);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return lVal.IsRValTrivial(scg, null);
        }

        public override void DebString(StringBuilder sb)
        {
            prefix.DebString(sb);
            sb.Append(' ');
            lVal.DebString(sb);
        }
    }

    /**
     * @brief calling a function or method, ie, may have side-effects
     */
    public class TokenRValCall: TokenRVal
    {

        public TokenRVal meth;  // points to the function to be called
                                // - might be a reference to a global function (TokenLValName)
                                // - or an instance method of a class (TokenLValIField)
                                // - or a static method of a class (TokenLValSField)
                                // - or a delegate stored in a variable (assumption for anything else)
        public TokenRVal args;  // null-terminated TokenRVal list
        public int nArgs;       // number of elements in args

        public TokenRValCall(Token original) : base(original) { }

        private TokenType[] myArgSig;

        /**
         * @brief The type of a call is the type of the return value.
         */
        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
             // Build type signature so we select correct overloaded function.
            if(myArgSig == null)
            {
                myArgSig = new TokenType[nArgs];
                int i = 0;
                for(Token t = args; t != null; t = t.nextToken)
                {
                    myArgSig[i++] = ((TokenRVal)t).GetRValType(scg, null);
                }
            }

             // Get the type of the method itself.  This should get us a delegate type.
            TokenType delType = meth.GetRValType(scg, myArgSig);
            if(delType is not TokenTypeSDTypeDelegate)
            {
                scg.ErrorMsg(meth, "must be function or method");
                return new TokenTypeVoid(meth);
            }

             // Get the return type from the delegate type.
            return ((TokenTypeSDTypeDelegate)delType).decl.GetRetType();
        }

        /**
         * @brief See if the call to the function/method is trivial.
         *        It is trivial if all the argument computations are trivial and
         *        the function is not being called via virtual table or delegate
         *        and the function body is trivial.
         */
        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
             // Build type signature so we select correct overloaded function.
            if(myArgSig == null)
            {
                myArgSig = new TokenType[nArgs];
                int i = 0;
                for(Token t = args; t != null; t = t.nextToken)
                {
                    myArgSig[i++] = ((TokenRVal)t).GetRValType(scg, null);
                }
            }

             // Make sure all arguments can be computed trivially.
            for(Token t = args; t != null; t = t.nextToken)
            {
                if(!((TokenRVal)t).IsRValTrivial(scg, null))
                    return false;
            }

             // See if the function call itself and the function body are trivial.
            return meth.IsCallTrivial(scg, myArgSig);
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            meth.DebString(sb);
            sb.Append(" (");
            bool first = true;
            for(Token t = args; t != null; t = t.nextToken)
            {
                if(!first)
                    sb.Append(", ");
                t.DebString(sb);
                first = false;
            }
            sb.Append(')');
        }
    }

    /**
     * @brief encapsulates a typecast, ie, (type)
     */
    public class TokenRValCast: TokenRVal
    {
        public TokenType castTo;
        public TokenRVal rVal;

        public TokenRValCast(TokenType type, TokenRVal value) : base(type)
        {
            castTo = type;
            rVal = value;
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return castTo;
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            if(castTo is TokenTypeSDTypeDelegate tokencastto)
                argsig = tokencastto.decl.GetArgTypes();
            else
                argsig = null;
            return rVal.IsRValTrivial(scg, argsig);
        }

        /**
         * @brief If operand is constant, maybe we can say the whole thing is a constant.
         */
        public override TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            rVal = rVal.TryComputeConstant(lookup, ref didOne);
            if(rVal is TokenRValConst TokenRValConstrVal)
            {
                try
                {
                    object val = TokenRValConstrVal.val;
                    object nval = null;
                    if(castTo is TokenTypeChar)
                    {
                        if(val is char)
                            return rVal;
                        if(val is int intval)
                            nval = (char)intval;
                    }
                    else if (castTo is TokenTypeFloat)
                    {
                        if(val is double)
                            return rVal;
                        if(val is int intval)
                            nval = (double)intval;
                        if(val is string sval)
                            nval = new LSL_Float(sval).value;
                    }
                    else if (castTo is TokenTypeInt)
                    {
                        if(val is int)
                            return rVal;
                        if(val is char charval)
                            nval = (int)charval;
                        if(val is double dval)
                            nval = (int)dval;
                        if(val is string sval)
                            nval = new LSL_Integer(sval).value;
                    }
                    else if (castTo is TokenTypeRot)
                    {
                        if(val is LSL_Rotation)
                            return rVal;
                        if(val is string sval)
                            nval = new LSL_Rotation(sval);
                    }
                    else if ((castTo is TokenTypeKey) || (castTo is TokenTypeStr))
                    {
                        if(val is string)
                            nval = val;  // in case of key/string conversion
                        if(val is char cval)
                            nval = TypeCast.CharToString(cval);
                        if(val is double dval)
                            nval = TypeCast.FloatToString(dval);
                        if(val is int ival)
                            nval = TypeCast.IntegerToString(ival);
                        if(val is LSL_Rotation lslrot)
                            nval = TypeCast.RotationToString(lslrot);
                        if(val is LSL_Vector lslvec)
                            nval = TypeCast.VectorToString(lslvec);
                    }
                    else if (castTo is TokenTypeVec)
                    {
                        if(val is LSL_Vector)
                            return rVal;
                        if(val is string sval)
                            nval = new LSL_Vector(sval);
                    }

                    if(nval != null)
                    {
                        TokenRVal rValConst = new TokenRValConst(castTo, nval);
                        didOne = true;
                        return rValConst;
                    }
                }
                catch
                {
                }
            }
            return this;
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append('(');
            castTo.DebString(sb);
            sb.Append(')');
            rVal.DebString(sb);
        }
    }

    /**
     * @brief Encapsulate a conditional expression:
     *        <condExpr> ? <trueExpr> : <falseExpr>
     */
    public class TokenRValCondExpr: TokenRVal
    {
        public TokenRVal condExpr;
        public TokenRVal trueExpr;
        public TokenRVal falseExpr;

        public TokenRValCondExpr(Token original) : base(original)
        {
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            TokenType trueType = trueExpr.GetRValType(scg, argsig);
            TokenType falseType = falseExpr.GetRValType(scg, argsig);
            if(trueType.ToString() != falseType.ToString())
            {
                scg.ErrorMsg(condExpr, "true & false expr types don't match");
            }
            return trueType;
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return condExpr.IsRValTrivial(scg, null) &&
                   trueExpr.IsRValTrivial(scg, argsig) &&
                  falseExpr.IsRValTrivial(scg, argsig);
        }

        /**
         * @brief If condition is constant, then the whole expression is constant
         *      iff the corresponding trueExpr or falseExpr is constant.
         */
        public override TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            TokenRVal rValCond = condExpr.TryComputeConstant(lookup, ref didOne);
            if(rValCond is TokenRValConst TokenRValConstrValCond)
            {
                didOne = true;
                return (TokenRValConstrValCond.IsConstBoolTrue() ? trueExpr : falseExpr).TryComputeConstant(lookup, ref didOne);
            }
            return this;
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            condExpr.DebString(sb);
            sb.Append(" ? ");
            trueExpr.DebString(sb);
            sb.Append(" : ");
            falseExpr.DebString(sb);
        }
    }

    /**
     * @brief all constants supposed to end up here
     */
    public enum TokenRValConstType: byte { CHAR = 0, FLOAT = 1, INT = 2, KEY = 3, STRING = 4 };
    public class TokenRValConst: TokenRVal
    {
        public object val;  // always a system type (char, int, double, string), never LSL-wrapped
        public TokenRValConstType type;
        public TokenType tokType;

        public TokenRValConst(Token original, object value) : base(original)
        {
            val = value;

            TokenType tt;
            if(val is char)
            {
                type = TokenRValConstType.CHAR;
                tt = new TokenTypeChar(this);
            }
            else if(val is int)
            {
                type = TokenRValConstType.INT;
                tt = new TokenTypeInt(this);
            }
            else if(val is double)
            {
                type = TokenRValConstType.FLOAT;
                tt = new TokenTypeFloat(this);
            }
            else if(val is string)
            {
                type = TokenRValConstType.STRING;
                tt = new TokenTypeStr(this);
            }
            else
            {
                throw new Exception("invalid constant type " + val.GetType());
            }

            tokType = (original is TokenType type1) ? type1 : tt;
            if(tokType is TokenTypeKey)
            {
                type = TokenRValConstType.KEY;
            }
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return tokType;
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return true;
        }

        public CompValu GetCompValu()
        {
            switch(type)
            {
                case TokenRValConstType.CHAR:
                    {
                        return new CompValuChar(tokType, (char)val);
                    }
                case TokenRValConstType.FLOAT:
                    {
                        return new CompValuFloat(tokType, (double)val);
                    }
                case TokenRValConstType.INT:
                    {
                        return new CompValuInteger(tokType, (int)val);
                    }
                case TokenRValConstType.KEY:
                case TokenRValConstType.STRING:
                    {
                        return new CompValuString(tokType, (string)val);
                    }
                default:
                    throw new Exception("unknown type");
            }
        }

        public override TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            // gotta end somewhere
            return this;
        }

        public bool IsConstBoolTrue()
        {
            switch(type)
            {
                case TokenRValConstType.CHAR:
                    {
                        return (char)val != 0;
                    }
                case TokenRValConstType.FLOAT:
                    {
                        return (double)val != 0;
                    }
                case TokenRValConstType.INT:
                    {
                        return (int)val != 0;
                    }
                case TokenRValConstType.KEY:
                    {
                        return (string)val != "" && (string)val != ScriptBaseClass.NULL_KEY;
                    }
                case TokenRValConstType.STRING:
                    {
                        return (string)val != "";
                    }
                default:
                    throw new Exception("unknown type");
            }
        }

        public override void DebString(StringBuilder sb)
        {
            if(val is char charval)
            {
                sb.Append('\'');
                EscapeQuotes(sb, new string(new char[] { charval }));
                sb.Append('\'');
            }
            else if(val is int intval)
            {
                sb.Append(intval);
            }
            else if(val is double dval)
            {
                string str = dval.ToString();
                sb.Append(str);
                if((str.IndexOf('.') < 0) &&
                    (str.IndexOf('E') < 0) &&
                    (str.IndexOf('e') < 0))
                {
                    sb.Append(".0");
                }
            }
            else if(val is string sval)
            {
                sb.Append('"');
                EscapeQuotes(sb, sval);
                sb.Append('"');
            }
            else
            {
                throw new Exception("invalid constant type " + val.GetType());
            }
        }
        private static void EscapeQuotes(StringBuilder sb, string s)
        {
            foreach(char c in s)
            {
                switch(c)
                {
                    case '\n':
                        {
                            sb.Append("\\n");
                            break;
                        }
                    case '\t':
                        {
                            sb.Append("\\t");
                            break;
                        }
                    case '\\':
                        {
                            sb.Append("\\\\");
                            break;
                        }
                    case '\'':
                        {
                            sb.Append("\\'");
                            break;
                        }
                    case '\"':
                        {
                            sb.Append("\\\"");
                            break;
                        }
                    default:
                        {
                            sb.Append(c);
                            break;
                        }
                }
            }
        }
    }

    /**
     * @brief Default initialization value for the corresponding variable.
     */
    public class TokenRValInitDef: TokenRVal
    {
        public TokenType type;

        public static TokenRValInitDef Construct(TokenDeclVar tokenDeclVar)
        {
            TokenRValInitDef zhis = new(tokenDeclVar)
            {
                type = tokenDeclVar.type
            };
            return zhis;
        }
        private TokenRValInitDef(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return type;
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            // it's always just a constant so it's always very trivial
            return true;
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("<default ");
            sb.Append(type.ToString());
            sb.Append('>');
        }
    }

    /**
     * @brief encapsulation of <rval> is <typeexp>
     */
    public class TokenRValIsType: TokenRVal
    {
        public TokenRVal rValExp;
        public TokenTypeExp typeExp;

        public TokenRValIsType(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return new TokenTypeBool(rValExp);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return rValExp.IsRValTrivial(scg, argsig);
        }
    }

    /**
     * @brief an R-value enclosed in brackets is an LSLList
     */
    public class TokenRValList: TokenRVal
    {

        public TokenRVal rVal;  // null-terminated list of TokenRVal objects
        public int nItems;

        public TokenRValList(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return new TokenTypeList(rVal);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            for(Token t = rVal; t != null; t = t.nextToken)
            {
                if(!((TokenRVal)t).IsRValTrivial(scg, null))
                    return false;
            }
            return true;
        }

        public override void DebString(StringBuilder sb)
        {
            bool first = true;
            sb.Append('[');
            for(Token t = rVal; t != null; t = t.nextToken)
            {
                if(!first)
                    sb.Append(',');
                sb.Append(' ');
                t.DebString(sb);
                first = false;
            }
            sb.Append(" ]");
        }
    }

    /**
     * @brief encapsulates '$new' arraytype '{' ... '}'
     */
    public class TokenRValNewArIni: TokenRVal
    {
        public TokenType arrayType;
        public TokenList valueList;  // TokenList : a sub-list
                                     // TokenKwComma : a default value
                                     // TokenRVal    : init expression

        public TokenRValNewArIni(Token original) : base(original)
        {
            valueList = new TokenList(original);
        }

        // type of the expression = the array type allocated by $new()
        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return arrayType;
        }

        // The expression is trivial if all the initializers are trivial.
        // An array's constructor is always trivial (no CheckRun() calls).
        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return ListIsTrivial(scg, valueList);
        }
        private bool ListIsTrivial(ScriptCodeGen scg, TokenList valList)
        {
            foreach(Token val in valList.tl)
            {
                if(val is TokenRVal TokenRValval)
                {
                    if(!TokenRValval.IsRValTrivial(scg, null))
                        return false;
                }
                if(val is TokenList TokenListval)
                {
                    if(!ListIsTrivial(scg, TokenListval))
                        return false;
                }
            }
            return true;
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("new ");
            arrayType.DebString(sb);
            sb.Append(' ');
            valueList.DebString(sb);
        }
    }
    public class TokenList: Token
    {
        public List<Token> tl = new();
        public TokenList(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append('{');
            bool first = true;
            foreach(Token t in tl)
            {
                if(!first)
                    sb.Append(", ");
                t.DebString(sb);
                first = false;
            }
            sb.Append('}');
        }
    }

    /**
     * @brief a binary operator and its two operands
     */
    public class TokenRValOpBin: TokenRVal
    {
        public TokenRVal rValLeft;
        public TokenKw opcode;
        public TokenRVal rValRight;

        public TokenRValOpBin(TokenRVal left, TokenKw op, TokenRVal right) : base(op)
        {
            rValLeft = left;
            opcode = op;
            rValRight = right;
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
             // Comparisons and the like always return bool.
            string opstr = opcode.ToString();
            if((opstr == "==") || (opstr == "!=") || (opstr == ">=") || (opstr == ">") ||
                (opstr == "&&") || (opstr == "||") || (opstr == "<=") || (opstr == "<") ||
                (opstr == "&&&") || (opstr == "|||"))
            {
                return new TokenTypeBool(opcode);
            }

             // Comma is always type of right-hand operand.
            if(opstr == ",")
                return rValRight.GetRValType(scg, argsig);

             // Assignments are always the type of the left-hand operand, 
             // including stuff like "+=".
            if(opstr.EndsWith("="))
            {
                return rValLeft.GetRValType(scg, argsig);
            }

             // string+something or something+string is always string.
             // except list+something or something+list is always a list.
            string lType = rValLeft.GetRValType(scg, argsig).ToString();
            string rType = rValRight.GetRValType(scg, argsig).ToString();
            if((opstr == "+") && ((lType == "list") || (rType == "list")))
            {
                return new TokenTypeList(opcode);
            }
            if((opstr == "+") && ((lType == "key") || (lType == "string") ||
                                   (rType == "key") || (rType == "string")))
            {
                return new TokenTypeStr(opcode);
            }

             // Everything else depends on both operands.
            string key = lType + opstr + rType;
            if(BinOpStr.defined.TryGetValue(key, out BinOpStr binOpStr))
            {
                return TokenType.FromSysType(opcode, binOpStr.outtype);
            }

            scg.ErrorMsg(opcode, "undefined operation " + key);
            return new TokenTypeVoid(opcode);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return rValLeft.IsRValTrivial(scg, null) && rValRight.IsRValTrivial(scg, null);
        }

        /**
         * @brief If both operands are constants, maybe we can say the whole thing is a constant.
         */
        public override TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            rValLeft = rValLeft.TryComputeConstant(lookup, ref didOne);
            rValRight = rValRight.TryComputeConstant(lookup, ref didOne);
            if((rValLeft is TokenRValConst TRValConstrValLeft) && (rValRight is TokenRValConst TRValConstrValRight))
            {
                //try
                {
                    object val = opcode.binOpConst(TRValConstrValLeft.val, TRValConstrValRight.val);
                    TokenRVal rValConst = new TokenRValConst(opcode, val);
                    didOne = true;
                    return rValConst;
                }
                //catch
                {
                }
            }
            return this;
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            rValLeft.DebString(sb);
            sb.Append(' ');
            sb.Append(opcode.ToString());
            sb.Append(' ');
            rValRight.DebString(sb);
        }
    }

    /**
     * @brief an unary operator and its one operand
     */
    public class TokenRValOpUn: TokenRVal
    {
        public TokenKw opcode;
        public TokenRVal rVal;

        public TokenRValOpUn(TokenKw op, TokenRVal right) : base(op)
        {
            opcode = op;
            rVal = right;
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            if(opcode is TokenKwExclam)
                return new TokenTypeInt(opcode);
            return rVal.GetRValType(scg, null);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return rVal.IsRValTrivial(scg, null);
        }

        /**
         * @brief If operand is constant, maybe we can say the whole thing is a constant.
         */
        public override TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            rVal = rVal.TryComputeConstant(lookup, ref didOne);
            if(rVal is TokenRValConst TokenRValConstrVal)
            {
                try
                {
                    object val = opcode.unOpConst(TokenRValConstrVal.val);
                    TokenRVal rValConst = new TokenRValConst(opcode, val);
                    didOne = true;
                    return rValConst;
                }
                catch
                {
                }
            }
            return this;
        }

        /**
         * @brief Serialization/Deserialization.
         */
        public TokenRValOpUn(Token original) : base(original) { }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append(opcode.ToString());
            rVal.DebString(sb);
        }
    }

    /**
     * @brief an R-value enclosed in parentheses
     */
    public class TokenRValParen: TokenRVal
    {

        public TokenRVal rVal;

        public TokenRValParen(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            // pass argsig through in this simple case, ie, let 
            // them do something like (llOwnerSay)("blabla...");
            return rVal.GetRValType(scg, argsig);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            // pass argsig through in this simple case, ie, let 
            // them do something like (llOwnerSay)("blabla...");
            return rVal.IsRValTrivial(scg, argsig);
        }

        /**
         * @brief If operand is constant, we can say the whole thing is a constant.
         */
        public override TokenRVal TryComputeConstant(TCCLookup lookup, ref bool didOne)
        {
            rVal = rVal.TryComputeConstant(lookup, ref didOne);
            if(rVal is TokenRValConst)
            {
                didOne = true;
                return rVal;
            }
            return this;
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append('(');
            rVal.DebString(sb);
            sb.Append(')');
        }
    }

    public class TokenRValRot: TokenRVal
    {

        public TokenRVal xRVal;
        public TokenRVal yRVal;
        public TokenRVal zRVal;
        public TokenRVal wRVal;

        public TokenRValRot(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return new TokenTypeRot(xRVal);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return xRVal.IsRValTrivial(scg, null) &&
                   yRVal.IsRValTrivial(scg, null) &&
                   zRVal.IsRValTrivial(scg, null) &&
                   wRVal.IsRValTrivial(scg, null);
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append('<');
            xRVal.DebString(sb);
            sb.Append(',');
            yRVal.DebString(sb);
            sb.Append(',');
            zRVal.DebString(sb);
            sb.Append(',');
            wRVal.DebString(sb);
            sb.Append('>');
        }
    }

    /**
     * @brief 'this' is being used as an rval inside an instance method.
     */
    public class TokenRValThis: TokenRVal
    {
        public Token original;
        public TokenDeclSDTypeClass sdtClass;

        public TokenRValThis(Token original, TokenDeclSDTypeClass sdtClass) : base(original)
        {
            this.original = original;
            this.sdtClass = sdtClass;
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return sdtClass.MakeRefToken(original);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return true;  // ldarg.0/starg.0 can't possibly loop
        }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append("this");
        }
    }

    /**
     * @brief the 'undef' keyword is being used as a value in an expression.
     *        It is the null object pointer and has type TokenTypeUndef.
     */
    public class TokenRValUndef: TokenRVal
    {
        readonly Token original;

        public TokenRValUndef(Token original) : base(original)
        {
            this.original = original;
        }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return new TokenTypeUndef(original);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return true;
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("undef");
        }
    }

    /**
     * @brief put 3 RVals together as a Vector value.
     */
    public class TokenRValVec: TokenRVal
    {

        public TokenRVal xRVal;
        public TokenRVal yRVal;
        public TokenRVal zRVal;

        public TokenRValVec(Token original) : base(original) { }

        public override TokenType GetRValType(ScriptCodeGen scg, TokenType[] argsig)
        {
            return new TokenTypeVec(xRVal);
        }

        public override bool IsRValTrivial(ScriptCodeGen scg, TokenType[] argsig)
        {
            return xRVal.IsRValTrivial(scg, null) &&
                   yRVal.IsRValTrivial(scg, null) &&
                   zRVal.IsRValTrivial(scg, null);
        }

        public override void DebString(StringBuilder sb)
        {
            sb.Append('<');
            xRVal.DebString(sb);
            sb.Append(',');
            yRVal.DebString(sb);
            sb.Append(',');
            zRVal.DebString(sb);
            sb.Append('>');
        }
    }

    /**
     * @brief encapsulates the whole script in a single token
     */
    public class TokenScript: Token
    {
        public int expiryDays = Int32.MaxValue;
        public TokenDeclState defaultState;
        public Dictionary<string, TokenDeclState> states = new();
        public VarDict variablesStack = new (false);  // initial one is used for global functions and variables
        public TokenDeclVar globalVarInit;                    // $globalvarinit function
                                                              // - performs explicit global var and static field inits

        private Dictionary<string, TokenDeclSDType> sdSrcTypes = new();
        private bool sdSrcTypesSealed = false;

        public TokenScript(Token original) : base(original) { }

        /*
         * Handle variable definition stack.
         * Generally a '{' pushes a new frame and a '}' pops the frame.
         * Function parameters are pushed in an additional frame (just outside the body's { ... } block)
         */
        public void PushVarFrame(bool locals)
        {
            PushVarFrame(new VarDict(locals));
        }
        public void PushVarFrame(VarDict newFrame)
        {
            newFrame.outerVarDict = variablesStack;
            variablesStack = newFrame;
        }
        public void PopVarFrame()
        {
            variablesStack = variablesStack.outerVarDict;
        }
        public bool AddVarEntry(TokenDeclVar var)
        {
            return variablesStack.AddEntry(var);
        }

        /*
         * Handle list of script-defined types.
         */
        public void sdSrcTypesSeal()
        {
            sdSrcTypesSealed = true;
        }
        public bool sdSrcTypesContainsKey(string key)
        {
            return sdSrcTypes.ContainsKey(key);
        }
        public bool sdSrcTypesTryGetValue(string key, out TokenDeclSDType value)
        {
            return sdSrcTypes.TryGetValue(key, out value);
        }
        public void sdSrcTypesAdd(string key, TokenDeclSDType value)
        {
            if(sdSrcTypesSealed)
                throw new Exception("sdSrcTypes is sealed");
            value.sdTypeIndex = sdSrcTypes.Count;
            sdSrcTypes.Add(key, value);
        }
        public void sdSrcTypesRep(string key, TokenDeclSDType value)
        {
            if(sdSrcTypesSealed)
                throw new Exception("sdSrcTypes is sealed");
            value.sdTypeIndex = sdSrcTypes[key].sdTypeIndex;
            sdSrcTypes[key] = value;
        }
        public void sdSrcTypesReplace(string key, TokenDeclSDType value)
        {
            if(sdSrcTypesSealed)
                throw new Exception("sdSrcTypes is sealed");
            sdSrcTypes[key] = value;
        }
        public Dictionary<string, TokenDeclSDType>.ValueCollection sdSrcTypesValues
        {
            get
            {
                return sdSrcTypes.Values;
            }
        }
        public int sdSrcTypesCount
        {
            get
            {
                return sdSrcTypes.Count;
            }
        }

        /**
         * @brief Debug output.
         */
        public override void DebString(StringBuilder sb)
        {
            /*
             * Script-defined types.
             */
            foreach(TokenDeclSDType srcType in sdSrcTypes.Values)
            {
                srcType.DebString(sb);
            }

            /*
             * Global constants.
             * Variables are handled by outputting the $globalvarinit function.
             */
            foreach(TokenDeclVar var in variablesStack)
            {
                if(var.constant)
                {
                    var.DebString(sb);
                }
            }

            /*
             * Global functions.
             */
            foreach(TokenDeclVar var in variablesStack)
            {
                if(var == globalVarInit)
                {
                    var.DebStringInitFields(sb);
                }
                else if(var.retType != null)
                {
                    var.DebString(sb);
                }
            }

            /*
             * States and their event handler functions.
             */
            defaultState.DebString(sb);
            foreach(TokenDeclState st in states.Values)
            {
                st.DebString(sb);
            }
        }
    }

    /**
     * @brief state body declaration
     */
    public class TokenStateBody: Token
    {

        public TokenDeclVar eventFuncs;

        public int index = -1;  // (codegen) row in ScriptHandlerEventTable (0=default)

        public TokenStateBody(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append(" { ");
            for(Token t = eventFuncs; t != null; t = t.nextToken)
            {
                t.DebString(sb);
            }
            sb.Append(" } ");
        }
    }

    /**
     * @brief a single statement, such as ending on a semicolon or enclosed in braces
     * TokenStmt includes the terminating semicolon or the enclosing braces
     * Also includes @label; for jump targets.
     * Also includes stray ; null statements.
     * Also includes local variable declarations with or without initialization value.
     */
    public class TokenStmt: Token
    {
        public TokenStmt(Token original) : base(original) { }
    }

    /**
     * @brief a group of statements enclosed in braces
     */
    public class TokenStmtBlock: TokenStmt
    {
        public Token statements;               // null-terminated list of statements, can also have TokenDeclVar's in here
        public TokenStmtBlock outerStmtBlock;  // next outer stmtBlock or null if top-level, ie, function definition
        public TokenDeclVar function;          // function it is part of
        public bool isTry;                     // true iff it's a try statement block
        public bool isCatch;                   // true iff it's a catch statement block
        public bool isFinally;                 // true iff it's a finally statement block
        public TokenStmtTry tryStmt;           // set iff isTry|isCatch|isFinally is set

        public TokenStmtBlock(Token original) : base(original) { }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            sb.Append("{ ");
            for(Token stmt = statements; stmt != null; stmt = stmt.nextToken)
            {
                stmt.DebString(sb);
            }
            sb.Append("} ");
        }
    }

    /**
     * @brief definition of branch target name
     */
    public class TokenStmtLabel: TokenStmt
    {
        public TokenName name;        // the label's name
        public TokenStmtBlock block;  // which block it is defined in
        public bool hasBkwdRefs = false;

        public bool labelTagged;      // code gen: location of label
        public ScriptMyLabel labelStruct;

        public TokenStmtLabel(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append('@');
            name.DebString(sb);
            sb.Append(';');
        }
    }

    /**
     * @brief those types of RVals with a semi-colon on the end
     *        that are allowed to stand alone as statements
     */
    public class TokenStmtRVal: TokenStmt
    {
        public TokenRVal rVal;

        public TokenStmtRVal(Token original) : base(original) { }

        // debugging
        public override void DebString(StringBuilder sb)
        {
            rVal.DebString(sb);
            sb.Append("; ");
        }
    }

    public class TokenStmtBreak: TokenStmt
    {
        public TokenStmtBreak(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("break;");
        }
    }

    public class TokenStmtCont: TokenStmt
    {
        public TokenStmtCont(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("continue;");
        }
    }

    /**
     * @brief "do" statement
     */
    public class TokenStmtDo: TokenStmt
    {

        public TokenStmt bodyStmt;
        public TokenRValParen testRVal;

        public TokenStmtDo(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("do ");
            bodyStmt.DebString(sb);
            sb.Append(" while ");
            testRVal.DebString(sb);
            sb.Append(';');
        }
    }

    /**
     * @brief "for" statement
     */
    public class TokenStmtFor: TokenStmt
    {

        public TokenStmt initStmt;  // there is always an init statement, though it may be a null statement
        public TokenRVal testRVal;  // there may or may not be a test (null if not)
        public TokenRVal incrRVal;  // there may or may not be an increment (null if not)
        public TokenStmt bodyStmt;  // there is always a body statement, though it may be a null statement

        public TokenStmtFor(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("for (");
            if(initStmt != null)
                initStmt.DebString(sb);
            else
                sb.Append(';');
            testRVal?.DebString(sb);
            sb.Append(';');
            incrRVal?.DebString(sb);
            sb.Append(") ");
            bodyStmt.DebString(sb);
        }
    }

    /**
     * @brief "foreach" statement
     */
    public class TokenStmtForEach: TokenStmt
    {

        public TokenLVal keyLVal;
        public TokenLVal valLVal;
        public TokenRVal arrayRVal;
        public TokenStmt bodyStmt;  // there is always a body statement, though it may be a null statement

        public TokenStmtForEach(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("foreach (");
            keyLVal?.DebString(sb);
            sb.Append(',');
            valLVal?.DebString(sb);
            sb.Append(" in ");
            arrayRVal.DebString(sb);
            sb.Append(')');
            bodyStmt.DebString(sb);
        }
    }

    public class TokenStmtIf: TokenStmt
    {

        public TokenRValParen testRVal;
        public TokenStmt trueStmt;
        public TokenStmt elseStmt;

        public TokenStmtIf(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("if ");
            testRVal.DebString(sb);
            sb.Append(' ');
            trueStmt.DebString(sb);
            if(elseStmt != null)
            {
                sb.Append(" else ");
                elseStmt.DebString(sb);
            }
        }
    }

    public class TokenStmtJump: TokenStmt
    {

        public TokenName label;

        public TokenStmtJump(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("jump ");
            label.DebString(sb);
            sb.Append(';');
        }
    }

    public class TokenStmtNull: TokenStmt
    {
        public TokenStmtNull(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append(';');
        }
    }

    public class TokenStmtRet: TokenStmt
    {
        public TokenRVal rVal;  // null if void

        public TokenStmtRet(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("return");
            if(rVal != null)
            {
                sb.Append(' ');
                rVal.DebString(sb);
            }
            sb.Append(';');
        }
    }

    /**
     * @brief statement that changes the current state.
     */
    public class TokenStmtState: TokenStmt
    {
        public TokenName state;  // null for default

        public TokenStmtState(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("state ");
            sb.Append((state == null) ? "default" : state.val);
            sb.Append(';');
        }
    }

    /**
     * @brief Encapsulates a whole switch statement including the body and all cases.
     */
    public class TokenStmtSwitch: TokenStmt
    {
        public TokenRValParen testRVal;          // the integer index expression
        public TokenSwitchCase cases = null;     // list of all cases, linked by .nextCase
        public TokenSwitchCase lastCase = null;  // used during reduce to point to last in 'cases' list

        public TokenStmtSwitch(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("switch ");
            testRVal.DebString(sb);
            sb.Append('{');
            for(TokenSwitchCase kase = cases; kase != null; kase = kase.nextCase)
            {
                kase.DebString(sb);
            }
            sb.Append('}');
        }
    }

    /**
     * @brief Encapsulates a case/default clause from a switch statement including the
     *        two values and the corresponding body statements.
     */
    public class TokenSwitchCase: Token
    {
        public TokenSwitchCase nextCase;  // next case in source-code order
        public TokenRVal rVal1;           // null means 'default', else 'case'
        public TokenRVal rVal2;           // null means 'case expr:', else 'case expr ... expr:'
        public TokenStmt stmts;           // statements associated with the case
        public TokenStmt lastStmt;        // used during reduce for building statement list

        public int val1;                        // codegen: value of rVal1 here
        public int val2;                        // codegen: value of rVal2 here
        public ScriptMyLabel label;             // codegen: target label here
        public TokenSwitchCase nextSortedCase;  // codegen: next case in ascending val order

        public string str1;
        public string str2;
        public TokenSwitchCase lowerCase;
        public TokenSwitchCase higherCase;

        public TokenSwitchCase(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            if(rVal1 == null)
            {
                sb.Append("default: ");
            }
            else
            {
                sb.Append("case ");
                rVal1.DebString(sb);
                if(rVal2 != null)
                {
                    sb.Append(" ... ");
                    rVal2.DebString(sb);
                }
                sb.Append(": ");
            }
            for(Token t = stmts; t != null; t = t.nextToken)
            {
                t.DebString(sb);
            }
        }
    }

    public class TokenStmtThrow: TokenStmt
    {
        public TokenRVal rVal;  // null if rethrow style

        public TokenStmtThrow(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("throw ");
            rVal.DebString(sb);
            sb.Append(';');
        }
    }

    /**
     * @brief Encapsulates related try, catch and finally statements.
     */
    public class TokenStmtTry: TokenStmt
    {
        public TokenStmtBlock tryStmt;
        public TokenDeclVar catchVar;       // null iff catchStmt is null
        public TokenStmtBlock catchStmt;    // can be null
        public TokenStmtBlock finallyStmt;  // can be null
        public Dictionary<string, IntermediateLeave> iLeaves = new();

        public TokenStmtTry(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("try ");
            tryStmt.DebString(sb);
            if(catchStmt != null)
            {
                sb.Append("catch (");
                sb.Append(catchVar.type.ToString());
                sb.Append(' ');
                sb.Append(catchVar.name.val);
                sb.Append(") ");
                catchStmt.DebString(sb);
            }
            if(finallyStmt != null)
            {
                sb.Append("finally ");
                finallyStmt.DebString(sb);
            }
        }
    }

    public class IntermediateLeave
    {
        public ScriptMyLabel jumpIntoLabel;
        public ScriptMyLabel jumpAwayLabel;
    }

    public class TokenStmtVarIniDef: TokenStmt
    {
        public TokenLVal var;
        public TokenStmtVarIniDef(Token original) : base(original) { }
    }

    public class TokenStmtWhile: TokenStmt
    {
        public TokenRValParen testRVal;
        public TokenStmt bodyStmt;

        public TokenStmtWhile(Token original) : base(original) { }

        public override void DebString(StringBuilder sb)
        {
            sb.Append("while ");
            testRVal.DebString(sb);
            sb.Append(' ');
            bodyStmt.DebString(sb);
        }
    }

    /**
     * @brief type expressions (right-hand of 'is' keyword).
     */
    public class TokenTypeExp: Token
    {
        public TokenTypeExp(Token original) : base(original) { }
    }

    public class TokenTypeExpBinOp: TokenTypeExp
    {
        public TokenTypeExp leftOp;
        public Token binOp;
        public TokenTypeExp rightOp;

        public TokenTypeExpBinOp(Token original) : base(original) { }
    }

    public class TokenTypeExpNot: TokenTypeExp
    {
        public TokenTypeExp typeExp;

        public TokenTypeExpNot(Token original) : base(original) { }
    }

    public class TokenTypeExpPar: TokenTypeExp
    {
        public TokenTypeExp typeExp;

        public TokenTypeExpPar(Token original) : base(original) { }
    }

    public class TokenTypeExpType: TokenTypeExp
    {
        public TokenType typeToken;

        public TokenTypeExpType(Token original) : base(original) { }
    }

    public class TokenTypeExpUndef: TokenTypeExp
    {
        public TokenTypeExpUndef(Token original) : base(original) { }
    }
}
