local tokens     = require 'parser.tokens'
local guide      = require 'parser.guide'

local sbyte      = string.byte
local sfind      = string.find
local smatch     = string.match
local sgsub      = string.gsub
local ssub       = string.sub
local schar      = string.char
local supper     = string.upper
local uchar      = utf8.char
local tconcat    = table.concat
local tinsert    = table.insert
local tointeger  = math.tointeger
local tonumber   = tonumber
local maxinteger = math.maxinteger
local assert     = assert

_ENV = nil

---@alias parser.position integer

---@param str string
---@return table<integer, boolean>
local function stringToCharMap(str)
    local map = {}
    local pos = 1
    while pos <= #str do
        local byte = sbyte(str, pos, pos)
        map[schar(byte)] = true
        pos = pos + 1
        if  ssub(str, pos, pos) == '-'
        and pos < #str then
            pos = pos + 1
            local byte2 = sbyte(str, pos, pos)
            assert(byte < byte2)
            for b = byte + 1, byte2 do
                map[schar(b)] = true
            end
            pos = pos + 1
        end
    end
    return map
end

local CharMapNumber  = stringToCharMap '0-9'
local CharMapN16     = stringToCharMap 'xX'
local CharMapN2      = stringToCharMap 'bB'
local CharMapE10     = stringToCharMap 'eE'
local CharMapE16     = stringToCharMap 'pP'
local CharMapSign    = stringToCharMap '+-'
local CharMapSB      = stringToCharMap 'ao|~&=<>.*/%^+-'
local CharMapSU      = stringToCharMap 'n#~!-'
local CharMapSimple  = stringToCharMap '.:([\'"{'
local CharMapStrSH   = stringToCharMap '\'"`'
local CharMapStrLH   = stringToCharMap '['
local CharMapTSep    = stringToCharMap ',;'
local CharMapWord    = stringToCharMap '_a-zA-Z\x80-\xff'

local EscMap = {
    ['a']  = '\a',
    ['b']  = '\b',
    ['f']  = '\f',
    ['n']  = '\n',
    ['r']  = '\r',
    ['t']  = '\t',
    ['v']  = '\v',
    ['\\'] = '\\',
    ['\''] = '\'',
    ['\"'] = '\"',
}

local NLMap = {
    ['\n']   = true,
    ['\r']   = true,
    ['\r\n'] = true,
}

local LineMulti = 10000

-- goto 单独处理
local KeyWord = {
    ['and']      = true,
    ['break']    = true,
    ['do']       = true,
    ['else']     = true,
    ['elseif']   = true,
    ['end']      = true,
    ['false']    = true,
    ['for']      = true,
    ['function'] = true,
    ['if']       = true,
    ['in']       = true,
    ['local']    = true,
    ['nil']      = true,
    ['not']      = true,
    ['or']       = true,
    ['repeat']   = true,
    ['return']   = true,
    ['then']     = true,
    ['true']     = true,
    ['until']    = true,
    ['while']    = true,
}

local Specials = {
    ['_G']           = true,
    ['rawset']       = true,
    ['rawget']       = true,
    ['setmetatable'] = true,
    ['require']      = true,
    ['dofile']       = true,
    ['loadfile']     = true,
    ['pcall']        = true,
    ['xpcall']       = true,
    ['pairs']        = true,
    ['ipairs']       = true,
    ['assert']       = true,
    ['error']        = true,
    ['type']         = true,
    ['os.exit']      = true,
}

local UnarySymbol = {
    ['not'] = 11,
    ['#']   = 11,
    ['~']   = 11,
    ['-']   = 11,
}

local BinarySymbol = {
    ['or']  = 1,
    ['and'] = 2,
    ['<=']  = 3,
    ['>=']  = 3,
    ['<']   = 3,
    ['>']   = 3,
    ['~=']  = 3,
    ['==']  = 3,
    ['|']   = 4,
    ['~']   = 5,
    ['&']   = 6,
    ['<<']  = 7,
    ['>>']  = 7,
    ['..']  = 8,
    ['+']   = 9,
    ['-']   = 9,
    ['*']   = 10,
    ['//']  = 10,
    ['/']   = 10,
    ['%']   = 10,
    ['^']   = 12,
}

local BinaryAlias = {
    ['&&'] = 'and',
    ['||'] = 'or',
    ['!='] = '~=',
}

local BinaryActionAlias = {
    ['='] = '==',
}

local UnaryAlias = {
    ['!'] = 'not',
}

local SymbolForward = {
    [01]  = true,
    [02]  = true,
    [03]  = true,
    [04]  = true,
    [05]  = true,
    [06]  = true,
    [07]  = true,
    [08]  = false,
    [09]  = true,
    [10]  = true,
    [11]  = true,
    [12]  = false,
}

local GetToSetMap = {
    ['getglobal'] = 'setglobal',
    ['getlocal']  = 'setlocal',
    ['getfield']  = 'setfield',
    ['getindex']  = 'setindex',
    ['getmethod'] = 'setmethod',
}

local ChunkFinishMap = {
    ['end']    = true,
    ['else']   = true,
    ['elseif'] = true,
    ['in']     = true,
    ['then']   = true,
    ['until']  = true,
    [';']      = true,
    [']']      = true,
    [')']      = true,
    ['}']      = true,
}

local ChunkStartMap = {
    ['do']       = true,
    ['else']     = true,
    ['elseif']   = true,
    ['for']      = true,
    ['function'] = true,
    ['if']       = true,
    ['local']    = true,
    ['repeat']   = true,
    ['return']   = true,
    ['then']     = true,
    ['until']    = true,
    ['while']    = true,
}

local ListFinishMap = {
    ['end']      = true,
    ['else']     = true,
    ['elseif']   = true,
    ['in']       = true,
    ['then']     = true,
    ['do']       = true,
    ['until']    = true,
    ['for']      = true,
    ['if']       = true,
    ['local']    = true,
    ['repeat']   = true,
    ['return']   = true,
    ['while']    = true,
}

local State, Lua, Line, LineOffset, Chunk, Tokens, Index, LastTokenFinish, Mode, LocalCount, LocalLimited

local LocalLimit = 200

local parseExp, parseAction

---@class parser.state.err
---@field type string
---@field start? parser.position
---@field finish? parser.position
---@field info? table
---@field fix? table
---@field version? string[]|string
---@field level? string | 'Error' | 'Warning'

---@type fun(err:parser.state.err):parser.state.err|nil
local pushError

local function addSpecial(name, obj)
    if not State.specials then
        State.specials = {}
    end
    if not State.specials[name] then
        State.specials[name] = {}
    end
    State.specials[name][#State.specials[name]+1] = obj
    obj.special = name
end

---@param offset integer
---@param leftOrRight '"left"'|'"right"'
local function getPosition(offset, leftOrRight)
    if not offset or offset > #Lua then
        return LineMulti * Line + #Lua - LineOffset + 1
    end
    if leftOrRight == 'left' then
        return LineMulti * Line + offset - LineOffset
    else
        return LineMulti * Line + offset - LineOffset + 1
    end
end

---@return string?          word
---@return parser.position? startPosition
---@return parser.position? finishPosition
local function peekWord()
    local word = Tokens[Index + 1]
    if not word then
        return nil
    end
    if not CharMapWord[ssub(word, 1, 1)] then
        return nil
    end
    local startPos  = getPosition(Tokens[Index] , 'left')
    local finishPos = getPosition(Tokens[Index] + #word - 1, 'right')
    return word, startPos, finishPos
end

local function lastRightPosition()
    if Index < 2 then
        return 0
    end
    local token = Tokens[Index - 1]
    if NLMap[token] then
        return LastTokenFinish
    elseif token then
        return getPosition(Tokens[Index - 2] + #token - 1, 'right')
    else
        return getPosition(#Lua, 'right')
    end
end

local function missSymbol(symbol, start, finish)
    pushError {
        type   = 'MISS_SYMBOL',
        start  = start or lastRightPosition(),
        finish = finish or start or lastRightPosition(),
        info = {
            symbol = symbol,
        }
    }
end

local function missExp()
    pushError {
        type   = 'MISS_EXP',
        start  = lastRightPosition(),
        finish = lastRightPosition(),
    }
end

local function missName(pos)
    pushError {
        type   = 'MISS_NAME',
        start  = pos or lastRightPosition(),
        finish = pos or lastRightPosition(),
    }
end

local function missEnd(relatedStart, relatedFinish)
    pushError {
        type   = 'MISS_SYMBOL',
        start  = lastRightPosition(),
        finish = lastRightPosition(),
        info = {
            symbol  = 'end',
            related = {
                {
                    start  = relatedStart,
                    finish = relatedFinish,
                }
            }
        }
    }
    pushError {
        type   = 'MISS_END',
        start  = relatedStart,
        finish = relatedFinish,
    }
end

local function unknownSymbol(start, finish, word)
    local token = word or Tokens[Index + 1]
    if not token then
        return false
    end
    pushError {
        type   = 'UNKNOWN_SYMBOL',
        start  = start  or getPosition(Tokens[Index], 'left'),
        finish = finish or getPosition(Tokens[Index] + #token - 1, 'right'),
        info   = {
            symbol = token,
        }
    }
    return true
end

local function skipUnknownSymbol()
    if unknownSymbol() then
        Index = Index + 2
        return true
    end
    return false
end

local function skipNL()
    local token = Tokens[Index + 1]
    if NLMap[token] then
        if Index >= 2 and not NLMap[Tokens[Index - 1]] then
            LastTokenFinish = getPosition(Tokens[Index - 2] + #Tokens[Index - 1] - 1, 'right')
        end
        Line       = Line + 1
        LineOffset = Tokens[Index] + #token
        Index = Index + 2
        State.lines[Line] = LineOffset
        return true
    end
    return false
end

local function getSavePoint()
    local index = Index
    local line  = Line
    local lineOffset = LineOffset
    local errs  = State.errs
    local errCount = #errs
    return function ()
        Index = index
        Line  = line
        LineOffset = lineOffset
        for i = errCount + 1, #errs do
            errs[i] = nil
        end
    end
end

local function fastForwardToken(offset)
    while true do
        local myOffset = Tokens[Index]
        if not myOffset
        or myOffset >= offset then
            break
        end
        local token = Tokens[Index + 1]
        if NLMap[token] then
            Line       = Line + 1
            LineOffset = Tokens[Index] + #token
            State.lines[Line] = LineOffset
        end
        Index = Index + 2
    end
end

local function resolveLongString(finishMark)
    skipNL()
    local miss
    local start        = Tokens[Index]
    local finishOffset = sfind(Lua, finishMark, start, true)
    if not finishOffset then
        finishOffset = #Lua + 1
        miss = true
    end
    local stringResult = start and ssub(Lua, start, finishOffset - 1) or ''
    local lastLN = stringResult:find '[\r\n][^\r\n]*$'
    if lastLN then
        local result = stringResult
            : gsub('\r\n?', '\n')
        stringResult = result
    end
    if finishMark == ']]' and State.version == 'Lua 5.1' then
        local nestOffset = sfind(Lua, '[[', start, true)
        if nestOffset and nestOffset < finishOffset then
            fastForwardToken(nestOffset)
            local nestStartPos = getPosition(nestOffset, 'left')
            local nestFinishPos = getPosition(nestOffset + 1, 'right')
            pushError {
                type   = 'NESTING_LONG_MARK',
                start  = nestStartPos,
                finish = nestFinishPos,
            }
        end
    end
    fastForwardToken(finishOffset + #finishMark)
    if miss then
        local pos = getPosition(finishOffset - 1, 'right')
        pushError {
            type   = 'MISS_SYMBOL',
            start  = pos,
            finish = pos,
            info   = {
                symbol = finishMark,
            },
            fix    = {
                title = 'ADD_LSTRING_END',
                {
                    start  = pos,
                    finish = pos,
                    text   = finishMark,
                }
            },
        }
    end
    return stringResult, getPosition(finishOffset + #finishMark - 1, 'right')
end

local function parseLongString()
    local start, finish, mark = sfind(Lua, '^(%[%=*%[)', Tokens[Index])
    if not start then
        return nil
    end
    fastForwardToken(finish + 1)
    local startPos     = getPosition(start, 'left')
    local finishMark   = sgsub(mark, '%[', ']')
    local stringResult, finishPos = resolveLongString(finishMark)
    return {
        type   = 'string',
        start  = startPos,
        finish = finishPos,
        [1]    = stringResult,
        [2]    = mark,
    }
end

local function pushCommentHeadError(left)
    if State.options.nonstandardSymbol['//'] then
        return
    end
    pushError {
        type   = 'ERR_COMMENT_PREFIX',
        start  = left,
        finish = left + 2,
        fix    = {
            title = 'FIX_COMMENT_PREFIX',
            {
                start  = left,
                finish = left + 2,
                text   = '--',
            },
        }
    }
end

local function pushLongCommentError(left, right)
    if State.options.nonstandardSymbol['/**/'] then
        return
    end
    pushError {
        type   = 'ERR_C_LONG_COMMENT',
        start  = left,
        finish = right,
        fix    = {
            title = 'FIX_C_LONG_COMMENT',
            {
                start  = left,
                finish = left + 2,
                text   = '--[[',
            },
            {
                start  = right - 2,
                finish = right,
                text   =  '--]]'
            },
        }
    }
end

local function skipComment(isAction)
    local token = Tokens[Index + 1]
    if token == '--'
    or (
        token == '//'
        and (
            isAction
            or State.options.nonstandardSymbol['//']
        )
    ) then
        local start = Tokens[Index]
        local left = getPosition(start, 'left')
        local chead = false
        if token == '//' then
            chead = true
            pushCommentHeadError(left)
        end
        Index = Index + 2
        local longComment = start + 2 == Tokens[Index] and parseLongString()
        if longComment then
            longComment.type = 'comment.long'
            longComment.text = longComment[1]
            longComment.mark = longComment[2]
            longComment[1]   = nil
            longComment[2]   = nil
            State.comms[#State.comms+1] = longComment
            return true
        end
        while true do
            local nl = Tokens[Index + 1]
            if not nl or NLMap[nl] then
                break
            end
            Index = Index + 2
        end
        local right = Tokens[Index] and (Tokens[Index] - 1) or #Lua
        State.comms[#State.comms+1] = {
            type   = chead and 'comment.cshort' or 'comment.short',
            start  = left,
            finish = getPosition(right, 'right'),
            text   = ssub(Lua, start + 2, right),
        }
        return true
    end
    if token == '/*' then
        local start = Tokens[Index]
        local left = getPosition(start, 'left')
        Index = Index + 2
        local result, right = resolveLongString '*/'
        pushLongCommentError(left, right)
        State.comms[#State.comms+1] = {
            type   = 'comment.clong',
            start  = left,
            finish = right,
            text   = result,
        }
        return true
    end
    return false
end

local function skipSpace(isAction)
    repeat until not skipNL()
            and  not skipComment(isAction)
end

local function expectAssign(isAction)
    local token = Tokens[Index + 1]
    if token == '=' then
        Index = Index + 2
        return true
    end
    if token == '==' then
        local left  = getPosition(Tokens[Index], 'left')
        local right = getPosition(Tokens[Index] + #token - 1, 'right')
        pushError {
            type   = 'ERR_ASSIGN_AS_EQ',
            start  = left,
            finish = right,
            fix    = {
                title = 'FIX_ASSIGN_AS_EQ',
                {
                    start  = left,
                    finish = right,
                    text   = '=',
                }
            }
        }
        Index = Index + 2
        return true
    end
    if isAction then
        if token == '+='
        or token == '-='
        or token == '*='
        or token == '/='
        or token == '%='
        or token == '^='
        or token == '//='
        or token == '|='
        or token == '&='
        or token == '>>='
        or token == '<<=' then
            if not State.options.nonstandardSymbol[token] then
                unknownSymbol()
            end
            Index = Index + 2
            return true
        end
    end
    return false
end

---@param kind? '"prefix"'|'"suffix"'
local function parseLocalAttrs(kind)
    local attrs
    while true do
        skipSpace()
        local token = Tokens[Index + 1]
        if token ~= '<' then
            break
        end
        if not attrs then
            attrs = {
                type = 'localattrs',
                start = getPosition(Tokens[Index], 'left'),
                finish = getPosition(Tokens[Index], 'right'),
            }
        end
        local attr = {
            type   = 'localattr',
            parent = attrs,
            start  = getPosition(Tokens[Index], 'left'),
            finish = getPosition(Tokens[Index], 'right'),
            kind   = kind or 'suffix',
        }
        attrs[#attrs+1] = attr
        Index = Index + 2
        skipSpace()
        local word, wstart, wfinish = peekWord()
        if word then
            attr[1] = word
            attr.finish = wfinish
            Index = Index + 2
            if  word ~= 'const'
            and word ~= 'close' then
                pushError {
                    type   = 'UNKNOWN_ATTRIBUTE',
                    start  = wstart,
                    finish = wfinish,
                }
            end
        else
            missName()
        end
        attr.finish = lastRightPosition()
        skipSpace()
        if Tokens[Index + 1] == '>' then
            attr.finish = getPosition(Tokens[Index], 'right')
            Index = Index + 2
        elseif Tokens[Index + 1] == '>=' then
            attr.finish = getPosition(Tokens[Index], 'right')
            pushError {
                type   = 'MISS_SPACE_BETWEEN',
                start  = getPosition(Tokens[Index], 'left'),
                finish = getPosition(Tokens[Index] + 1, 'right'),
            }
            Index = Index + 2
        else
            missSymbol '>'
        end
        if  State.version ~= 'Lua 5.4'
        and State.version ~= 'Lua 5.5' then
            pushError {
                type    = 'UNSUPPORT_SYMBOL',
                start   = attr.start,
                finish  = attr.finish,
                version = {'Lua 5.4', 'Lua 5.5'},
                info    = {
                    version = State.version
                }
            }
        end
        attrs.finish = attr.finish
    end
    return attrs
end

local function mergeLocalAttrs(attrsBefore, attrsAfter)
    if not attrsBefore then
        return attrsAfter
    end
    if not attrsAfter then
        return attrsBefore
    end
    for i = 1, #attrsAfter do
        attrsBefore[#attrsBefore + 1] = attrsAfter[i]
    end
    attrsBefore.finish = attrsAfter[#attrsAfter].finish
    return attrsBefore
end

---@param obj table
local function createLocal(obj, attrs)
    obj.type   = 'local'
    obj.effect = obj.finish

    if attrs then
        obj.attrs = attrs
        attrs.parent = obj
    end

    local chunk = Chunk[#Chunk]
    if chunk then
        local locals = chunk.locals
        if not locals then
            locals = {}
            chunk.locals = locals
        end
        locals[#locals+1] = obj
        LocalCount = LocalCount + 1
        if not LocalLimited and LocalCount > LocalLimit then
            LocalLimited = true
            pushError {
                type   = 'LOCAL_LIMIT',
                start  = obj.start,
                finish = obj.finish,
            }
        end
    end
    return obj
end

local function getLocal(name, pos)
    for i = #Chunk, 1, -1 do
        local chunk  = Chunk[i]
        local locals = chunk.locals
        if locals then
            local res
            for n = 1, #locals do
                local loc = locals[n]
                if loc.effect > pos then
                    break
                end
                if loc[1] == name then
                    if not res or res.effect < loc.effect then
                        res = loc
                    end
                end
            end
            if res then
                return res
            end
        end
    end
end

local function getVariable(name, pos)
    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        local resLocal
        local resGlobal

        -- Find most recent local in this chunk
        local locals = chunk.locals
        if locals then
            for n = 1, #locals do
                local loc = locals[n]
                if loc.effect > pos then
                    break
                end
                if loc[1] == name then
                    if not resLocal or resLocal.effect < loc.effect then
                        resLocal = loc
                    end
                end
            end
        end

        -- Find global in this chunk (globals don't have effect time, just find the last one)
        local globals = chunk.globals
        if globals then
            for n = 1, #globals do
                local glob = globals[n]
                if glob.effect > pos then
                    break
                end
                if glob[1] == name then
                    if not resGlobal or resGlobal.effect < glob.effect then
                        resGlobal = glob
                    end
                end
            end
        end

        -- Return the one declared later (compare by start position)
        if resLocal and resGlobal then
            if resLocal.start > resGlobal.start then
                return resLocal
            else
                return resGlobal
            end
        end
        if resLocal then
            return resLocal
        end
        if resGlobal then
            return resGlobal
        end
    end

    -- If not found, look for global *
    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        local globals = chunk.globals
        if globals then
            for n = #globals, 1, -1 do
                local glob = globals[n]
                if glob[1] == '*' then
                    return glob
                end
            end
        end
    end

    return nil
end

local function linkGlobalToEnv(node, var)
    -- Lua 5.5: Check VARIABLE_NOT_DECLARED
    if State.version == 'Lua 5.5' and not var then
        -- Check if there's any global declaration in scope
        local hasAnyGlobal = false
        for i = #Chunk, 1, -1 do
            local chunk = Chunk[i]
            if chunk.globals and #chunk.globals > 0 then
                hasAnyGlobal = true
                break
            end
        end

        if hasAnyGlobal then
            pushError {
                type   = 'VARIABLE_NOT_DECLARED',
                start  = node.start,
                finish = node.finish,
                info   = {
                    name = node[1],
                }
            }
        end
    end

    -- Lua 5.5: Use getVariable to find _ENV, check if it's global
    local env
    if State.version == 'Lua 5.5' then
        env = getVariable(State.ENVMode, node.start)
        if env and env.type == 'setglobal' then
            pushError {
                type   = 'ENV_IS_GLOBAL',
                start  = node.start,
                finish = node.finish,
                info   = {
                    name = node[1],
                }
            }
        end
        if env and env.type == 'local' then
            node.node = env
            if not env.ref then
                env.ref = {}
            end
            env.ref[#env.ref+1] = node
        end
    else
        env = getLocal(State.ENVMode, node.start)
        if env then
            node.node = env
            if not env.ref then
                env.ref = {}
            end
            env.ref[#env.ref+1] = node
        end
    end
end

---@param obj table
local function createGlobalDeclare(obj, attrs)
    obj.type = 'setglobal'
    obj.declare = true
    obj.effect = obj.finish

    if attrs then
        obj.attrs = attrs
        attrs.parent = obj
    end

    local chunk = Chunk[#Chunk]
    if chunk then
        local globals = chunk.globals
        if not globals then
            globals = {}
            chunk.globals = globals
        end
        globals[#globals+1] = obj
    end

    linkGlobalToEnv(obj, obj)

    return obj
end

local function pushChunk(chunk)
    Chunk[#Chunk+1] = chunk
end

local function hasAttr(attrs, attrName)
    if not attrs then
        return false
    end
    for i = 1, #attrs do
        if attrs[i][1] == attrName then
            return true
        end
    end
    return false
end

local function hasAttrKind(attrs, attrName, attrKind)
    if not attrs then
        return false
    end
    for i = 1, #attrs do
        if attrs[i][1] == attrName and attrs[i].kind == attrKind then
            return true
        end
    end
    return false
end

local function checkLocalCloseList(n1, n2, nrest)
    if State.version ~= 'Lua 5.4' and State.version ~= 'Lua 5.5' then
        return
    end

    if State.version == 'Lua 5.5' and hasAttrKind(n1 and n1.attrs, 'close', 'prefix') then
        local extra
        if n2 and not n2.attrs then
            extra = n2
        elseif nrest then
            for i = 1, #nrest do
                if not nrest[i].attrs then
                    extra = nrest[i]
                    break
                end
            end
        end
        if extra then
            pushError {
                type   = 'MULTI_CLOSE',
                start  = extra.start,
                finish = extra.finish,
            }
            return
        end
    end

    local function collectCloseAttrs(node, list)
        local attrs = node and node.attrs
        if not attrs then
            return
        end
        for i = 1, #attrs do
            local a = attrs[i]
            if a[1] == 'close' then
                list[#list + 1] = a
            end
        end
    end

    local closeList = {}
    collectCloseAttrs(n1, closeList)
    if n2 then
        collectCloseAttrs(n2, closeList)
    end
    if nrest then
        for i = 1, #nrest do
            collectCloseAttrs(nrest[i], closeList)
        end
    end

    if #closeList > 1 then
        local second = closeList[2]
        pushError {
            type   = 'MULTI_CLOSE',
            start  = second.start,
            finish = second.finish,
        }
    end
end

local function resolveLable(label, obj)
    if not label.ref then
        label.ref = {}
    end
    label.ref[#label.ref+1] = obj
    obj.node = label

    -- 如果有局部变量在 goto 与 label 之间声明，
    -- 并在 label 之后使用，则算作语法错误

    -- 如果 label 在 goto 之前声明，那么不会有中间声明的局部变量
    if obj.start > label.start then
        return
    end

    local block = guide.getBlock(obj)
    local locals = block and block.locals
    if not locals then
        return
    end

    for i = 1, #locals do
        local loc = locals[i]
        -- 检查局部变量声明位置为 goto 与 label 之间
        if loc.start < obj.start or loc.finish > label.finish then
            goto CONTINUE
        end
        -- 检查局部变量的使用位置在 label 之后
        local refs = loc.ref
        if not refs then
            goto CONTINUE
        end
        for j = 1, #refs do
            local ref = refs[j]
            if ref.finish > label.finish then
                pushError {
                    type   = 'JUMP_LOCAL_SCOPE',
                    start  = obj.start,
                    finish = obj.finish,
                    info   = {
                        loc = loc[1],
                    },
                    relative = {
                        {
                            start  = label.start,
                            finish = label.finish,
                        },
                        {
                            start  = loc.start,
                            finish = loc.finish,
                        }
                    },
                }
                return
            end
        end
        ::CONTINUE::
    end
end

local function resolveGoTo(gotos)
    for i = 1, #gotos do
        local action = gotos[i]
        local label  = guide.getLabel(action, action[1])
        if label then
            resolveLable(label, action)
        else
            pushError {
                type   = 'NO_VISIBLE_LABEL',
                start  = action.start,
                finish = action.finish,
                info   = {
                    label = action[1],
                }
            }
        end
    end
end

local function popChunk()
    local chunk = Chunk[#Chunk]
    if chunk.gotos then
        resolveGoTo(chunk.gotos)
        chunk.gotos = nil
    end
    local lastAction = chunk[#chunk]
    if lastAction then
        chunk.finish = lastAction.finish
    end
    Chunk[#Chunk] = nil
end

local function parseNil()
    if Tokens[Index + 1] ~= 'nil' then
        return nil
    end
    local offset = Tokens[Index]
    Index = Index + 2
    return {
        type   = 'nil',
        start  = getPosition(offset, 'left'),
        finish = getPosition(offset + 2, 'right'),
    }
end

local function parseBoolean()
    local word = Tokens[Index+1]
    if  word ~= 'true'
    and word ~= 'false' then
        return nil
    end
    local start  = getPosition(Tokens[Index], 'left')
    local finish = getPosition(Tokens[Index] + #word - 1, 'right')
    Index = Index + 2
    return {
        type   = 'boolean',
        start  = start,
        finish = finish,
        [1]    = word == 'true' and true or false,
    }
end

local function parseStringUnicode()
    local offset = Tokens[Index] + 1
    if ssub(Lua, offset, offset) ~= '{' then
        local pos  = getPosition(offset, 'left')
        missSymbol('{', pos)
        return nil, offset
    end
    local leftPos  = getPosition(offset, 'left')
    local x16      = smatch(Lua, '^%w*', offset + 1)
    local rightPos = getPosition(offset + #x16, 'right')
    offset = offset + #x16 + 1
    if ssub(Lua, offset, offset) == '}' then
        offset   = offset + 1
        rightPos = rightPos + 1
    else
        missSymbol('}', rightPos)
    end
    offset = offset + 1
    if #x16 == 0 then
        pushError {
            type   = 'UTF8_SMALL',
            start  = leftPos,
            finish = rightPos,
        }
        return '', offset
    end
    if  State.version ~= 'Lua 5.3'
    and State.version ~= 'Lua 5.4'
    and State.version ~= 'Lua 5.5'
    and State.version ~= 'LuaJIT'
    then
        pushError {
            type    = 'ERR_ESC',
            start   = leftPos - 2,
            finish  = rightPos,
            version = {'Lua 5.3', 'Lua 5.4', 'Lua 5.5', 'LuaJIT'},
            info = {
                version = State.version,
            }
        }
        return nil, offset
    end
    local byte = tonumber(x16, 16)
    if not byte then
        for i = 1, #x16 do
            if not tonumber(ssub(x16, i, i), 16) then
                pushError {
                    type   = 'MUST_X16',
                    start  = leftPos + i,
                    finish = leftPos + i + 1,
                }
            end
        end
        return nil, offset
    end
    if State.version == 'Lua 5.4' or State.version == 'Lua 5.5' then
        if byte < 0 or byte > 0x7FFFFFFF then
            pushError {
                type   = 'UTF8_MAX',
                start  = leftPos,
                finish = rightPos,
                info   = {
                    min = '00000000',
                    max = '7FFFFFFF',
                }
            }
            return nil, offset
        end
    else
        if byte < 0 or byte > 0x10FFFF then
            pushError {
                type    = 'UTF8_MAX',
                start   = leftPos,
                finish  = rightPos,
                version = byte <= 0x7FFFFFFF and {'Lua 5.4', 'Lua 5.5'} or nil,
                info = {
                    min = '000000',
                    max = '10FFFF',
                }
            }
        end
    end
    if byte >= 0 and byte <= 0x10FFFF then
        return uchar(byte), offset
    end
    return '', offset
end

local stringPool = {}
local function parseShortString()
    local mark        = Tokens[Index+1]
    local startOffset = Tokens[Index]
    local startPos    = getPosition(startOffset, 'left')
    Index             = Index + 2
    local stringIndex = 0
    local currentOffset = startOffset + 1
    local escs        = {}
    while true do
        local token = Tokens[Index + 1]
        if token == mark then
            stringIndex = stringIndex + 1
            stringPool[stringIndex] = ssub(Lua, currentOffset, Tokens[Index] - 1)
            Index = Index + 2
            break
        end
        if NLMap[token] then
            stringIndex = stringIndex + 1
            stringPool[stringIndex] = ssub(Lua, currentOffset, Tokens[Index] - 1)
            missSymbol(mark)
            break
        end
        if not token then
            stringIndex = stringIndex + 1
            stringPool[stringIndex] = ssub(Lua, currentOffset or -1)
            missSymbol(mark)
            break
        end
        if token == '\\' then
            stringIndex = stringIndex + 1
            stringPool[stringIndex] = ssub(Lua, currentOffset, Tokens[Index] - 1)
            currentOffset = Tokens[Index]
            Index = Index + 2
            if not Tokens[Index] then
                goto CONTINUE
            end
            local escLeft = getPosition(currentOffset, 'left')
            -- has space?
            if Tokens[Index] - currentOffset > 1 then
                local right = getPosition(currentOffset + 1, 'right')
                pushError {
                    type   = 'ERR_ESC',
                    start  = escLeft,
                    finish = right,
                }
                escs[#escs+1] = escLeft
                escs[#escs+1] = right
                escs[#escs+1] = 'err'
                goto CONTINUE
            end
            local nextToken = ssub(Tokens[Index + 1], 1, 1)
            if EscMap[nextToken] then
                stringIndex = stringIndex + 1
                stringPool[stringIndex] = EscMap[nextToken]
                currentOffset = Tokens[Index] + #nextToken
                Index = Index + 2
                escs[#escs+1] = escLeft
                escs[#escs+1] = escLeft + 2
                escs[#escs+1] = 'normal'
                goto CONTINUE
            end
            if nextToken == mark then
                stringIndex = stringIndex + 1
                stringPool[stringIndex] = mark
                currentOffset = Tokens[Index] + #nextToken
                Index = Index + 2
                escs[#escs+1] = escLeft
                escs[#escs+1] = escLeft + 2
                escs[#escs+1] = 'normal'
                goto CONTINUE
            end
            if nextToken == 'z' then
                Index = Index + 2
                repeat until not skipNL()
                currentOffset = Tokens[Index]
                escs[#escs+1] = escLeft
                escs[#escs+1] = escLeft + 2
                escs[#escs+1] = 'normal'
                goto CONTINUE
            end
            if CharMapNumber[nextToken] then
                local numbers = smatch(Tokens[Index + 1], '^%d+')
                if #numbers > 3 then
                    numbers = ssub(numbers, 1, 3)
                end
                currentOffset = Tokens[Index] + #numbers
                fastForwardToken(currentOffset)
                local right = getPosition(currentOffset - 1, 'right')
                local byte = tointeger(numbers)
                if byte and byte <= 255 then
                    stringIndex = stringIndex + 1
                    stringPool[stringIndex] = schar(byte)
                else
                    pushError {
                        type   = 'ERR_ESC',
                        start  = escLeft,
                        finish = right,
                    }
                end
                escs[#escs+1] = escLeft
                escs[#escs+1] = right
                escs[#escs+1] = 'byte'
                goto CONTINUE
            end
            if nextToken == 'x' then
                local left = getPosition(Tokens[Index] - 1, 'left')
                local x16  = ssub(Tokens[Index + 1], 2, 3)
                local byte = tonumber(x16, 16)
                if byte then
                    currentOffset = Tokens[Index] + 3
                    stringIndex = stringIndex + 1
                    stringPool[stringIndex] = schar(byte)
                else
                    currentOffset = Tokens[Index] + 1
                    pushError {
                        type   = 'MISS_ESC_X',
                        start  = getPosition(currentOffset, 'left'),
                        finish = getPosition(currentOffset + 1, 'right'),
                    }
                end
                local right = getPosition(currentOffset + 1, 'right')
                escs[#escs+1] = escLeft
                escs[#escs+1] = right
                escs[#escs+1] = 'byte'
                if State.version == 'Lua 5.1' then
                    pushError {
                        type    = 'ERR_ESC',
                        start   = left,
                        finish  = left + 4,
                        version = {'Lua 5.2', 'Lua 5.3', 'Lua 5.4', 'Lua 5.5', 'LuaJIT'},
                        info = {
                            version = State.version,
                        }
                    }
                end
                Index = Index + 2
                goto CONTINUE
            end
            if nextToken == 'u' then
                local str, newOffset = parseStringUnicode()
                if str then
                    stringIndex = stringIndex + 1
                    stringPool[stringIndex] = str
                end
                currentOffset = newOffset
                fastForwardToken(currentOffset - 1)
                local right = getPosition(currentOffset + 1, 'right')
                escs[#escs+1] = escLeft
                escs[#escs+1] = right
                escs[#escs+1] = 'unicode'
                goto CONTINUE
            end
            if NLMap[nextToken] then
                stringIndex = stringIndex + 1
                stringPool[stringIndex] = '\n'
                currentOffset = Tokens[Index] + #nextToken
                skipNL()
                escs[#escs+1] = escLeft
                escs[#escs+1] = escLeft + 1
                escs[#escs+1] = 'normal'
                goto CONTINUE
            end
            local right = getPosition(currentOffset + 1, 'right')
            pushError {
                type   = 'ERR_ESC',
                start  = escLeft,
                finish = right,
            }
            escs[#escs+1] = escLeft
            escs[#escs+1] = right
            escs[#escs+1] = 'err'
        end
        Index = Index + 2
        ::CONTINUE::
    end
    local stringResult = tconcat(stringPool, '', 1, stringIndex)
    local str = {
        type   = 'string',
        start  = startPos,
        finish = lastRightPosition(),
        escs   = #escs > 0 and escs or nil,
        [1]    = stringResult,
        [2]    = mark,
    }
    if mark == '`' then
        if not State.options.nonstandardSymbol[mark] then
            pushError {
                type   = 'ERR_NONSTANDARD_SYMBOL',
                start  = startPos,
                finish = str.finish,
                info   = {
                    symbol = '"',
                },
                fix    = {
                    title  = 'FIX_NONSTANDARD_SYMBOL',
                    symbol = '"',
                    {
                        start  = startPos,
                        finish = startPos + 1,
                        text   = '"',
                    },
                    {
                        start  = str.finish - 1,
                        finish = str.finish,
                        text   = '"',
                    },
                }
            }
        end
    end
    return str
end

local function parseString()
    local c = Tokens[Index + 1]
    if CharMapStrSH[c] then
        return parseShortString()
    end
    if CharMapStrLH[c] then
        return parseLongString()
    end
    return nil
end

local function parseNumber10(start)
    local integer = true
    local integerPart = smatch(Lua, '^%d*', start)
    local offset = start + #integerPart
    -- float part
    if ssub(Lua, offset, offset) == '.' then
        local floatPart = smatch(Lua, '^%d*', offset + 1)
        integer = false
        offset = offset + #floatPart + 1
    end
    -- exp part
    local echar = ssub(Lua, offset, offset)
    if CharMapE10[echar] then
        integer = false
        offset = offset + 1
        local nextChar = ssub(Lua, offset, offset)
        if CharMapSign[nextChar] then
            offset = offset + 1
        end
        local exp = smatch(Lua, '^%d*', offset)
        offset = offset + #exp
        if #exp == 0 then
            pushError {
                type   = 'MISS_EXPONENT',
                start  = getPosition(offset - 1, 'right'),
                finish = getPosition(offset - 1, 'right'),
            }
        end
    end
    return tonumber(ssub(Lua, start, offset - 1)), offset, integer
end

local function parseNumber16(start)
    local integerPart = smatch(Lua, '^[%da-fA-F]*', start)
    local offset = start + #integerPart
    local integer = true
    -- float part
    if ssub(Lua, offset, offset) == '.' then
        local floatPart = smatch(Lua, '^[%da-fA-F]*', offset + 1)
        integer = false
        offset = offset + #floatPart + 1
        if #integerPart == 0 and #floatPart == 0 then
            pushError {
                type   = 'MUST_X16',
                start  = getPosition(offset - 1, 'right'),
                finish = getPosition(offset - 1, 'right'),
            }
        end
    else
        if #integerPart == 0 then
            pushError {
                type   = 'MUST_X16',
                start  = getPosition(offset - 1, 'right'),
                finish = getPosition(offset - 1, 'right'),
            }
            return 0, offset
        end
    end
    -- exp part
    local echar = ssub(Lua, offset, offset)
    if CharMapE16[echar] then
        integer = false
        offset = offset + 1
        local nextChar = ssub(Lua, offset, offset)
        if CharMapSign[nextChar] then
            offset = offset + 1
        end
        local exp = smatch(Lua, '^%d*', offset)
        offset = offset + #exp
    end
    local n = tonumber(ssub(Lua, start - 2, offset - 1))
    return n, offset, integer
end

local function parseNumber2(start)
    local bins = smatch(Lua, '^[01]*', start)
    local offset = start + #bins
    if State.version ~= 'LuaJIT' then
        pushError {
            type    = 'UNSUPPORT_SYMBOL',
            start   = getPosition(start - 2, 'left'),
            finish  = getPosition(offset - 1, 'right'),
            version = 'LuaJIT',
            info    = {
                version = State.version,
            }
        }
    end
    return tonumber(bins, 2), offset
end

local function dropNumberTail(offset, integer)
    local _, finish, word = sfind(Lua, '^([%.%w_\x80-\xff]+)', offset)
    if not finish then
        return offset
    end
    if integer then
        if     supper(ssub(word, 1, 2)) == 'LL' then
            if State.version ~= 'LuaJIT' then
                pushError {
                    type    = 'UNSUPPORT_SYMBOL',
                    start   = getPosition(offset, 'left'),
                    finish  = getPosition(offset + 1, 'right'),
                    version = 'LuaJIT',
                    info    = {
                        version = State.version,
                    }
                }
            end
            offset = offset + 2
            word   = ssub(word, offset)
        elseif supper(ssub(word, 1, 3)) == 'ULL' then
            if State.version ~= 'LuaJIT' then
                pushError {
                    type    = 'UNSUPPORT_SYMBOL',
                    start   = getPosition(offset, 'left'),
                    finish  = getPosition(offset + 2, 'right'),
                    version = 'LuaJIT',
                    info    = {
                        version = State.version,
                    }
                }
            end
            offset = offset + 3
            word   = ssub(word, offset)
        end
    end
    if supper(ssub(word, 1, 1)) == 'I' then
        if State.version ~= 'LuaJIT' then
            pushError {
                type    = 'UNSUPPORT_SYMBOL',
                start   = getPosition(offset, 'left'),
                finish  = getPosition(offset, 'right'),
                version = 'LuaJIT',
                info    = {
                    version = State.version,
                }
            }
        end
        offset = offset + 1
        word   = ssub(word, offset)
    end
    if #word > 0 then
        pushError {
            type   = 'MALFORMED_NUMBER',
            start  = getPosition(offset, 'left'),
            finish = getPosition(finish, 'right'),
        }
    end
    return finish + 1
end

local function parseNumber()
    local offset = Tokens[Index]
    if not offset then
        return nil
    end
    local startPos = getPosition(offset, 'left')
    local neg
    if ssub(Lua, offset, offset) == '-' then
        neg = true
        offset = offset + 1
    end
    local number, integer
    local firstChar = ssub(Lua, offset, offset)
    if     firstChar == '.' then
        number, offset = parseNumber10(offset)
        integer = false
    elseif firstChar == '0' then
        local nextChar = ssub(Lua, offset + 1, offset + 1)
        if CharMapN16[nextChar] then
            number, offset, integer = parseNumber16(offset + 2)
        elseif CharMapN2[nextChar] then
            number, offset = parseNumber2(offset + 2)
            integer = true
        else
            number, offset, integer = parseNumber10(offset)
        end
    elseif CharMapNumber[firstChar] then
        number, offset, integer = parseNumber10(offset)
    else
        return nil
    end
    if not number then
        number = 0
    end
    if neg then
        number = - number
    end
    local result = {
        type   = integer and 'integer' or 'number',
        start  = startPos,
        finish = getPosition(offset - 1, 'right'),
        [1]    = number,
    }
    offset = dropNumberTail(offset, integer)
    fastForwardToken(offset)
    return result
end

local function isKeyWord(word, nextToken)
    if KeyWord[word] then
        return true
    end
    if word == 'goto' then
        if State.version == 'Lua 5.1' then
            return false
        end
        if State.version == 'LuaJIT' then
            if not nextToken then
                return false
            end
            if CharMapWord[ssub(nextToken, 1, 1)] then
                return true
            end
            return false
        end
        return true
    end
    if word == 'global' then
        return false
    end
    return false
end

local function isGlobalActionStart(nextToken)
    if State.version ~= 'Lua 5.5' then
        return false
    end
    if not nextToken
    or nextToken == '*'
    or nextToken == '<' then
        return true
    end
    if CharMapWord[ssub(nextToken, 1, 1)] then
        return true
    end
    return false
end

local function parseName(asAction)
    local word = peekWord()
    if not word then
        return nil
    end
    if ChunkFinishMap[word] then
        return nil
    end
    if asAction and ChunkStartMap[word] then
        return nil
    end
    local startPos  = getPosition(Tokens[Index], 'left')
    local finishPos = getPosition(Tokens[Index] + #word - 1, 'right')
    Index = Index + 2
    if not State.options.unicodeName and word:find '[\x80-\xff]' then
        pushError {
            type   = 'UNICODE_NAME',
            start  = startPos,
            finish = finishPos,
        }
    end
    if isKeyWord(word, Tokens[Index + 1]) then
        pushError {
            type   = 'KEYWORD',
            start  = startPos,
            finish = finishPos,
        }
    end
    return {
        type   = 'name',
        start  = startPos,
        finish = finishPos,
        [1]    = word,
    }
end

local function parseNameOrList(parent)
    local first = parseName()
    if not first then
        return nil
    end
    skipSpace()
    local list
    while true do
        if Tokens[Index + 1] ~= ',' then
            break
        end
        Index = Index + 2
        skipSpace()
        local name = parseName(true)
        if not name then
            missName()
            break
        end
        if not list then
            list = {
                type   = 'list',
                start  = first.start,
                finish = first.finish,
                parent = parent,
                [1]    = first
            }
        end
        list[#list+1] = name
        list.finish = name.finish
    end
    return list or first
end

local function parseExpList(mini)
    local list
    local wantSep = false
    while true do
        skipSpace()
        local token = Tokens[Index + 1]
        if not token then
            break
        end
        if ListFinishMap[token] then
            break
        end
        if token == ',' then
            local sepPos = getPosition(Tokens[Index], 'right')
            if not wantSep then
                pushError {
                    type   = 'UNEXPECT_SYMBOL',
                    start  = getPosition(Tokens[Index], 'left'),
                    finish = sepPos,
                    info = {
                        symbol = ',',
                    }
                }
            end
            wantSep = false
            Index = Index + 2
            goto CONTINUE
        else
            if mini then
                if wantSep then
                    break
                end
                local nextToken = peekWord()
                if  isKeyWord(nextToken, Tokens[Index + 2])
                and nextToken ~= 'function'
                and nextToken ~= 'true'
                and nextToken ~= 'false'
                and nextToken ~= 'nil'
                and nextToken ~= 'not' then
                    break
                end
            end
            local exp = parseExp()
            if not exp then
                break
            end
            if wantSep then
                missSymbol(',', list[#list].finish, exp.start)
            end
            wantSep = true
            if not list then
                list = {
                    type   = 'list',
                    start  = exp.start,
                }
            end
            list[#list+1] = exp
            list.finish   = exp.finish
            exp.parent    = list
        end
        ::CONTINUE::
    end
    if not list then
        return nil
    end
    if not wantSep then
        missExp()
    end
    return list
end

local function parseIndex()
    local start = getPosition(Tokens[Index], 'left')
    Index = Index + 2
    skipSpace()
    local exp = parseExp()
    local index = {
        type   = 'index',
        start  = start,
        finish = exp and exp.finish or (start + 1),
        index  = exp
    }
    if exp then
        exp.parent = index
    else
        missExp()
    end
    skipSpace()
    if Tokens[Index + 1] == ']' then
        index.finish = getPosition(Tokens[Index], 'right')
        Index = Index + 2
    else
        missSymbol ']'
    end
    return index
end

local function parseTable()
    local tbl = {
        type   = 'table',
        start  = getPosition(Tokens[Index], 'left'),
        finish = getPosition(Tokens[Index], 'right'),
    }
    tbl.bstart = tbl.finish
    Index = Index + 2
    local index = 0
    local tindex = 0
    local wantSep = false
    while true do
        skipSpace(true)
        local token = Tokens[Index + 1]
        if token == '}' then
            tbl.bfinish = getPosition(Tokens[Index], 'left')
            Index = Index + 2
            break
        end
        if CharMapTSep[token] then
            if not wantSep then
                missExp()
            end
            wantSep = false
            Index = Index + 2
            goto CONTINUE
        end
        local lastRight = lastRightPosition()

        if peekWord() then
            local savePoint = getSavePoint()
            local name = parseName()
            if name then
                skipSpace()
                if Tokens[Index + 1] == '=' then
                    Index = Index + 2
                    if wantSep then
                        pushError {
                            type   = 'MISS_SEP_IN_TABLE',
                            start  = lastRight,
                            finish = getPosition(Tokens[Index], 'left'),
                        }
                    end
                    wantSep = true
                    skipSpace()
                    local fvalue = parseExp()
                    local tfield = {
                        type   = 'tablefield',
                        start  = name.start,
                        finish = name.finish,
                        range  = fvalue and fvalue.finish,
                        node   = tbl,
                        parent = tbl,
                        field  = name,
                        value  = fvalue,
                    }
                    name.type   = 'field'
                    name.parent = tfield
                    if fvalue then
                        fvalue.parent = tfield
                    else
                        missExp()
                    end
                    index = index + 1
                    tbl[index] = tfield
                    goto CONTINUE
                end
            end
            savePoint()
        end

        local exp = parseExp(true)
        if exp then
            if wantSep then
                pushError {
                    type   = 'MISS_SEP_IN_TABLE',
                    start  = lastRight,
                    finish = exp.start,
                }
            end
            wantSep = true
            if exp.type == 'varargs' then
                index = index + 1
                tbl[index] = exp
                exp.parent = tbl
                goto CONTINUE
            end
            index = index + 1
            tindex = tindex + 1
            local texp = {
                type   = 'tableexp',
                start  = exp.start,
                finish = exp.finish,
                tindex = tindex,
                parent = tbl,
                value  = exp,
            }
            exp.parent = texp
            tbl[index] = texp
            goto CONTINUE
        end

        if token == '[' then
            if wantSep then
                pushError {
                    type   = 'MISS_SEP_IN_TABLE',
                    start  = lastRight,
                    finish = getPosition(Tokens[Index], 'left'),
                }
            end
            wantSep = true
            local tindex = parseIndex()
            skipSpace()
            tindex.type   = 'tableindex'
            tindex.node   = tbl
            tindex.parent = tbl
            index = index + 1
            tbl[index] = tindex
            if expectAssign() then
                skipSpace()
                local ivalue = parseExp()
                if ivalue then
                    ivalue.parent = tindex
                    tindex.range  = ivalue.finish
                    tindex.value  = ivalue
                else
                    missExp()
                end
            else
                missSymbol '='
            end
            goto CONTINUE
        end

        missSymbol '}'
        skipSpace()
        tbl.bfinish = getPosition(Tokens[Index], 'left')
        break
        ::CONTINUE::
    end
    tbl.finish = lastRightPosition()
    return tbl
end

local function addDummySelf(node, call)
    if not node then
        return
    end
    if node.type ~= 'getmethod' then
        return
    end
    -- dummy param `self`
    if not call.args then
        call.args = {
            type   = 'callargs',
            start  = call.start,
            finish = call.finish,
            parent = call,
        }
    end
    local self = {
        type   = 'self',
        start  = node.colon.start,
        finish = node.colon.finish,
        parent = call.args,
        [1]    = 'self',
    }
    tinsert(call.args, 1, self)
end

local function checkAmbiguityCall(call, parenPos)
    if State.version ~= 'Lua 5.1' then
        return
    end
    local node = call.node
    if not node then
        return
    end
    local nodeRow = guide.rowColOf(node.finish)
    local callRow = guide.rowColOf(parenPos)
    if nodeRow == callRow then
        return
    end
    pushError {
        type   = 'AMBIGUOUS_SYNTAX',
        start  = parenPos,
        finish = call.finish,
    }
end

local function bindSpecial(source, name)
    if Specials[name] then
        addSpecial(name, source)
    else
        local ospeicals = State.options.special
        if ospeicals and ospeicals[name] then
            addSpecial(ospeicals[name], source)
        end
    end
end

local function parseSimple(node, funcName)
    local currentName
    if node.type == 'getglobal'
    or node.type == 'getlocal' then
        currentName = node[1]
    end
    local lastMethod
    while true do
        if lastMethod and node.node == lastMethod then
            if node.type ~= 'call' then
                missSymbol('(', node.node.finish, node.node.finish)
            end
            lastMethod = nil
        end
        skipSpace()
        local token = Tokens[Index + 1]
        if token == '.' then
            local dot = {
                type   = token,
                start  = getPosition(Tokens[Index], 'left'),
                finish = getPosition(Tokens[Index], 'right'),
            }
            Index = Index + 2
            skipSpace()
            local field = parseName(true)
            local getfield = {
                type   = 'getfield',
                start  = node.start,
                finish = lastRightPosition(),
                node   = node,
                dot    = dot,
                field  = field
            }
            if field then
                field.parent = getfield
                field.type   = 'field'
                if currentName then
                    if node.type == 'getlocal'
                    or node.type == 'getglobal'
                    or node.type == 'getfield' then
                        currentName = currentName .. '.' .. field[1]
                        bindSpecial(getfield, currentName)
                    else
                        currentName = nil
                    end
                end
            else
                pushError {
                    type   = 'MISS_FIELD',
                    start  = lastRightPosition(),
                    finish = lastRightPosition(),
                }
            end
            node.parent = getfield
            node.next   = getfield
            node        = getfield
        elseif token == ':' then
            local colon = {
                type   = token,
                start  = getPosition(Tokens[Index], 'left'),
                finish = getPosition(Tokens[Index], 'right'),
            }
            Index = Index + 2
            skipSpace()
            local method = parseName(true)
            local getmethod = {
                type   = 'getmethod',
                start  = node.start,
                finish = lastRightPosition(),
                node   = node,
                colon  = colon,
                method = method
            }
            if method then
                method.parent = getmethod
                method.type   = 'method'
            else
                pushError {
                    type   = 'MISS_METHOD',
                    start  = lastRightPosition(),
                    finish = lastRightPosition(),
                }
            end
            node.parent = getmethod
            node.next   = getmethod
            node        = getmethod
            if lastMethod then
                missSymbol('(', node.node.finish, node.node.finish)
            end
            lastMethod = getmethod
        elseif token == '(' then
            if funcName then
                break
            end
            local startPos = getPosition(Tokens[Index], 'left')
            local call = {
                type   = 'call',
                start  = node.start,
                node   = node,
            }
            Index = Index + 2
            local args = parseExpList()
            if Tokens[Index + 1] == ')' then
                call.finish = getPosition(Tokens[Index], 'right')
                Index = Index + 2
            else
                call.finish = lastRightPosition()
                missSymbol ')'
            end
            if args then
                args.type   = 'callargs'
                args.start  = startPos
                args.finish = call.finish
                args.parent = call
                call.args   = args
            end
            addDummySelf(node, call)
            checkAmbiguityCall(call, startPos)
            node.parent = call
            node = call
        elseif token == '{' then
            if funcName then
                break
            end
            local tbl = parseTable()
            local call = {
                type   = 'call',
                start  = node.start,
                finish = tbl.finish,
                node   = node,
            }
            local args = {
                type   = 'callargs',
                start  = tbl.start,
                finish = tbl.finish,
                parent = call,
                [1]    = tbl,
            }
            call.args   = args
            addDummySelf(node, call)
            tbl.parent  = args
            node.parent = call
            node        = call
        elseif CharMapStrSH[token] then
            if funcName then
                break
            end
            local str = parseShortString()
            local call = {
                type   = 'call',
                start  = node.start,
                finish = str.finish,
                node   = node,
            }
            local args = {
                type   = 'callargs',
                start  = str.start,
                finish = str.finish,
                parent = call,
                [1]    = str,
            }
            call.args   = args
            addDummySelf(node, call)
            str.parent  = args
            node.parent = call
            node        = call
        elseif CharMapStrLH[token] then
            local str = parseLongString()
            if str then
                if funcName then
                    break
                end
                local call = {
                    type   = 'call',
                    start  = node.start,
                    finish = str.finish,
                    node   = node,
                }
                local args = {
                    type   = 'callargs',
                    start  = str.start,
                    finish = str.finish,
                    parent = call,
                    [1]    = str,
                }
                call.args   = args
                addDummySelf(node, call)
                str.parent  = args
                node.parent = call
                node        = call
            else
                local index  = parseIndex()
                local bstart = index.start
                index.type   = 'getindex'
                index.start  = node.start
                index.node   = node
                node.next    = index
                node.parent  = index
                node         = index
                if funcName then
                    pushError {
                        type   = 'INDEX_IN_FUNC_NAME',
                        start  = bstart,
                        finish = index.finish,
                    }
                end
            end
        else
            break
        end
    end
    if  node.type == 'call'
    and node.node == lastMethod then
        lastMethod = nil
    end
    if node.type == 'call' then
        if node.node.special == 'error'
        or node.node.special == 'os.exit' then
            node.hasExit = true
        end
    end
    if node == lastMethod then
        if funcName then
            lastMethod = nil
        end
    end
    if lastMethod then
        missSymbol('(', lastMethod.finish)
    end
    return node
end

local function parseVarargs()
    local varargs = {
        type   = 'varargs',
        start  = getPosition(Tokens[Index], 'left'),
        finish = getPosition(Tokens[Index] + 2, 'right'),
    }
    Index = Index + 2
    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        if chunk.vararg then
            if not chunk.vararg.ref then
                chunk.vararg.ref = {}
            end
            chunk.vararg.ref[#chunk.vararg.ref+1] = varargs
            varargs.node = chunk.vararg
            break
        end
        if chunk.type == 'main' then
            break
        end
        if chunk.type == 'function' then
            pushError {
                type   = 'UNEXPECT_DOTS',
                start  = varargs.start,
                finish = varargs.finish,
            }
            break
        end
    end
    return varargs
end

local function parseParen()
    local pl = Tokens[Index]
    local paren = {
        type   = 'paren',
        start  = getPosition(pl, 'left'),
        finish = getPosition(pl, 'right')
    }
    Index = Index + 2
    skipSpace()
    local exp = parseExp()
    if exp then
        paren.exp    = exp
        paren.finish = exp.finish
        exp.parent   = paren
    else
        missExp()
    end
    skipSpace()
    if Tokens[Index + 1] == ')' then
        paren.finish = getPosition(Tokens[Index], 'right')
        Index = Index + 2
    else
        missSymbol ')'
    end
    return paren
end

local function resolveName(node)
    if not node then
        return nil
    end
    local var = getVariable(node[1], node.start)
    if var and (var.type == 'local' or var.type == 'self') then
        node.type = 'getlocal'
        node.node = var
        if not var.ref then
            var.ref = {}
        end
        var.ref[#var.ref+1] = node
        if var.special then
            addSpecial(var.special, node)
        end
    else
        node.type = 'getglobal'
        node.var = var

        linkGlobalToEnv(node, var)
    end
    local name = node[1]
    bindSpecial(node, name)
    return node
end

local function isChunkFinishToken(token)
    local currentChunk = Chunk[#Chunk]
    if not currentChunk then
        return false
    end
    local tp = currentChunk.type
    if tp == 'main' then
        return false
    end
    if tp == 'for'
    or tp == 'in'
    or tp == 'loop'
    or tp == 'function' then
        return token == 'end'
    end
    if tp == 'if'
    or tp == 'ifblock'
    or tp == 'elseifblock'
    or tp == 'elseblock' then
        return token == 'then'
            or token == 'end'
            or token == 'else'
            or token == 'elseif'
    end
    if tp == 'repeat' then
        return token == 'until'
    end
    return true
end

local function parseActions()
    local rtn, last
    while true do
        skipSpace(true)
        local token = Tokens[Index + 1]
        if token == ';' then
            Index = Index + 2
            goto CONTINUE
        end
        if  ChunkFinishMap[token]
        and isChunkFinishToken(token) then
            break
        end
        local action, failed = parseAction()
        if failed then
            if not skipUnknownSymbol() then
                break
            end
        end
        if action then
            if not rtn and action.type == 'return' then
                rtn = action
            end
            last = action
        end
        ::CONTINUE::
    end
    if rtn and rtn ~= last then
        pushError {
            type   = 'ACTION_AFTER_RETURN',
            start  = rtn.start,
            finish = rtn.finish,
        }
    end
end

local function parseParams(params, isLambda)
    local lastSep
    local hasDots
    local endToken = isLambda and '|' or ')'
    while true do
        skipSpace()
        local token = Tokens[Index + 1]
        if not token or token == endToken then
            if lastSep then
                missName()
            end
            break
        end
        if token == ',' then
            if lastSep or lastSep == nil then
                missName()
            else
                lastSep = true
            end
            Index = Index + 2
            goto CONTINUE
        end
        if token == '...' then
            if lastSep == false then
                missSymbol ','
            end
            lastSep = false
            if not params then
                params = {}
            end
            local vararg = {
                type   = '...',
                start  = getPosition(Tokens[Index], 'left'),
                finish = getPosition(Tokens[Index] + 2, 'right'),
                parent = params,
                [1]    = '...',
            }
            local chunk = Chunk[#Chunk]
            chunk.vararg = vararg
            params[#params+1] = vararg
            if hasDots then
                pushError {
                    type   = 'ARGS_AFTER_DOTS',
                    start  = getPosition(Tokens[Index], 'left'),
                    finish = getPosition(Tokens[Index] + 2, 'right'),
                }
            end
            hasDots = true
            Index = Index + 2

            -- Lua 5.5: check for (...args) syntax
            skipSpace()
            local nextToken = Tokens[Index + 1]
            if nextToken and CharMapWord[ssub(nextToken, 1, 1)] then
                if State.version ~= 'Lua 5.5' then
                    pushError {
                        type   = 'UNSUPPORT_NAMED_VARARG',
                        start  = getPosition(Tokens[Index], 'left'),
                        finish = getPosition(Tokens[Index] + #nextToken - 1, 'right'),
                        version = 'Lua 5.5',
                    }
                end
                -- Create local variable for vararg
                local varargName = createLocal {
                    start  = getPosition(Tokens[Index], 'left'),
                    finish = getPosition(Tokens[Index] + #nextToken - 1, 'right'),
                    parent = params,
                    [1]    = nextToken,
                }
                varargName.varargRef = vararg
                vararg.name = varargName
                vararg.finish = varargName.finish
                Index = Index + 2
            end

            goto CONTINUE
        end
        if CharMapWord[ssub(token, 1, 1)] then
            if lastSep == false then
                missSymbol ','
            end
            lastSep = false
            if not params then
                params = {}
            end
            params[#params+1] = createLocal {
                start  = getPosition(Tokens[Index], 'left'),
                finish = getPosition(Tokens[Index] + #token - 1, 'right'),
                parent = params,
                [1]    = token,
            }
            if hasDots then
                pushError {
                    type   = 'ARGS_AFTER_DOTS',
                    start  = getPosition(Tokens[Index], 'left'),
                    finish = getPosition(Tokens[Index] + #token - 1, 'right'),
                }
            end
            if isKeyWord(token, Tokens[Index + 3]) then
                pushError {
                    type   = 'KEYWORD',
                    start  = getPosition(Tokens[Index], 'left'),
                    finish = getPosition(Tokens[Index] + #token - 1, 'right'),
                }
            end
            Index = Index + 2
            goto CONTINUE
        end
        skipUnknownSymbol()
        ::CONTINUE::
    end
    return params
end

local function parseFunction(declareType, isAction)
    local funcLeft  = getPosition(Tokens[Index], 'left')
    local funcRight = getPosition(Tokens[Index] + 7, 'right')
    local func = {
        type    = 'function',
        start   = funcLeft,
        finish  = funcRight,
        bstart  = funcRight,
        keyword = {
            [1] = funcLeft,
            [2] = funcRight,
        },
    }
    Index = Index + 2
    skipSpace(true)
    local hasLeftParen = Tokens[Index + 1] == '('
    if not hasLeftParen then
        local name = parseName()
        if name then
            local simple = parseSimple(name, true)
            if declareType == 'local' then
                if simple == name then
                    createLocal(name)
                else
                    resolveName(name)
                    pushError {
                        type   = 'UNEXPECT_LFUNC_NAME',
                        start  = simple.start,
                        finish = simple.finish,
                    }
                end
            elseif declareType == 'global' then
                if simple == name then
                    createGlobalDeclare(name)
                else
                    resolveName(name)
                    pushError {
                        type   = 'UNEXPECT_GFUNC_NAME',
                        start  = simple.start,
                        finish = simple.finish,
                    }
                end
            else
                resolveName(name)
            end
            func.name   = simple
            func.finish = simple.finish
            func.bstart = simple.finish
            if not isAction then
                simple.parent = func
                pushError {
                    type   = 'UNEXPECT_EFUNC_NAME',
                    start  = simple.start,
                    finish = simple.finish,
                }
            end
            skipSpace(true)
            hasLeftParen = Tokens[Index + 1] == '('
        end
    end
    local LastLocalCount = LocalCount
    LocalCount = 0
    pushChunk(func)
    local params
    if func.name and func.name.type == 'getmethod' then
        if func.name.type == 'getmethod' then
            params = {
                type   = 'funcargs',
                start  = funcRight,
                finish = funcRight,
                parent = func
            }
            params[1] = createLocal {
                start  = funcRight,
                finish = funcRight,
                parent = params,
                [1]    = 'self',
            }
            params[1].type = 'self'
        end
    end
    if hasLeftParen then
        params = params or {}
        local parenLeft = getPosition(Tokens[Index], 'left')
        Index = Index + 2
        params = parseParams(params)
        params.type   = 'funcargs'
        params.start  = parenLeft
        params.finish = lastRightPosition()
        params.parent = func
        func.args     = params
        skipSpace(true)
        if Tokens[Index + 1] == ')' then
            local parenRight = getPosition(Tokens[Index], 'right')
            func.finish = parenRight
            func.bstart = parenRight
            if params then
                params.finish = parenRight
            end
            Index = Index + 2
            skipSpace(true)
        else
            func.finish = lastRightPosition()
            func.bstart = func.finish
            if params then
                params.finish = func.finish
            end
            missSymbol ')'
        end
    else
        missSymbol '('
    end
    parseActions()
    popChunk()
    func.bfinish = getPosition(Tokens[Index], 'left')
    if Tokens[Index + 1] == 'end' then
        local endLeft   = getPosition(Tokens[Index], 'left')
        local endRight  = getPosition(Tokens[Index] + 2, 'right')
        func.keyword[3] = endLeft
        func.keyword[4] = endRight
        func.finish     = endRight
        Index = Index + 2
    else
        func.finish = lastRightPosition()
        missEnd(funcLeft, funcRight)
    end
    LocalCount = LastLocalCount
    return func
end

local function parseLambda(isDoublePipe)
    local lambdaLeft = getPosition(Tokens[Index], 'left')
    local lambdaRight = getPosition(Tokens[Index], 'right')
    local lambda = {
        type   = 'function',
        start  = lambdaLeft,
        finish = lambdaRight,
        bstart = lambdaRight,
        keyword = {
            [1] = lambdaLeft,
            [2] = lambdaRight,
        },
        hasReturn = true
    }
    Index = Index + 2
    local pipeLeft = getPosition(Tokens[Index], 'left')
    local pipeRight = getPosition(Tokens[Index], 'right')
    skipSpace(true)
    local params
    local LastLocalCount = LocalCount
    -- if nonstandardSymbol for '||' is true it is possible for token to be || when there are no params
    if isDoublePipe then
        params = {
            start = pipeLeft,
            finish = pipeRight,
            parent = lambda,
            type = 'funcargs'
        }
    else
        -- fake chunk to store locals
        pushChunk(lambda)
        LocalCount = 0
        params = parseParams({}, true)
        params.type   = 'funcargs'
        params.start  = pipeLeft
        params.finish = lastRightPosition()
        params.parent = lambda
        lambda.args   = params
        skipSpace()
        if Tokens[Index + 1] == '|' then
            pipeRight = getPosition(Tokens[Index], 'right')
            lambda.finish = pipeRight
            lambda.bstart = pipeRight
            if params then
                params.finish = pipeRight
            end
            Index = Index + 2
            skipSpace()
        else
            lambda.finish = lastRightPosition()
            lambda.bstart = lambda.finish
            if params then
                params.finish = lambda.finish
            end
            missSymbol '|'
        end
    end
    local child = parseExp()


    -- don't want popChunk logic here as this is not a real chunk
    Chunk[#Chunk] = nil

    if child then
        -- create dummy return
        local rtn = {
            type   = 'return',
            start  = child.start,
            finish = child.finish,
            parent = lambda,
            [1]    = child
        }
        child.parent = rtn
        lambda[1] = rtn
        lambda.returns = {rtn}
        lambda.finish = child.finish
        lambda.keyword[3] = child.finish
        lambda.keyword[4] = child.finish
    else
        lambda.finish = lastRightPosition()
        missExp()
    end
    lambda.bfinish = getPosition(Tokens[Index], 'left')
    LocalCount = LastLocalCount
    return lambda
end

local function checkNeedParen(source)
    local token = Tokens[Index + 1]
    if  token ~= '.'
    and token ~= ':' then
        return source
    end
    local exp = parseSimple(source, false)
    if exp == source then
        return exp
    end
    pushError {
        type   = 'NEED_PAREN',
        start  = source.start,
        finish = source.finish,
        fix = {
            title = 'FIX_ADD_PAREN',
            {
                start  = source.start,
                finish = source.start,
                text   = '(',
            },
            {
                start  = source.finish,
                finish = source.finish,
                text   = ')',
            }
        }
    }
    return exp
end

local function parseExpUnit()
    local token = Tokens[Index + 1]
    if token == '(' then
        local paren = parseParen()
        return parseSimple(paren, false)
    end

    if token == '...' then
        local varargs = parseVarargs()
        return varargs
    end

    if token == '{' then
        local table = parseTable()
        if not table then
            return nil
        end
        local exp = checkNeedParen(table)
        return exp
    end

    if CharMapStrSH[token] then
        local string = parseShortString()
        if not string then
            return nil
        end
        local exp = checkNeedParen(string)
        return exp
    end

    if CharMapStrLH[token] then
        local string = parseLongString()
        if not string then
            return nil
        end
        local exp = checkNeedParen(string)
        return exp
    end

    local number = parseNumber()
    if number then
        return number
    end

    if ChunkFinishMap[token] then
        return nil
    end

    if token == 'nil' then
        return parseNil()
    end

    if token == 'true'
    or token == 'false' then
        return parseBoolean()
    end

    if token == 'function' then
        return parseFunction()
    end

    -- FIXME: Use something other than nonstandardSymbol to check for lambda support
    if State.options.nonstandardSymbol['|lambda|'] and (token == '|'
    or token == '||') then
        return parseLambda(token == '||')
    end

    local node = parseName()
    if node then
        local nameNode = resolveName(node)
        if nameNode then
            return parseSimple(nameNode, false)
        end
    end

    return nil
end

local function parseUnaryOP()
    local token  = Tokens[Index + 1]
    local symbol = UnarySymbol[token] and token or UnaryAlias[token]
    if not symbol then
        return nil
    end
    local myLevel = UnarySymbol[symbol]
    local op = {
        type   = symbol,
        start  = getPosition(Tokens[Index], 'left'),
        finish = getPosition(Tokens[Index] + #symbol - 1, 'right'),
    }
    Index = Index + 2
    return op, myLevel
end

---@param level integer # op level must greater than this level
local function parseBinaryOP(asAction, level)
    local token  = Tokens[Index + 1]
    local symbol = (BinarySymbol[token] and token)
                or BinaryAlias[token]
                or (not asAction and BinaryActionAlias[token])
    if not symbol then
        return nil
    end
    if symbol == '//' and State.options.nonstandardSymbol['//'] then
        return nil
    end
    local myLevel = BinarySymbol[symbol]
    if level and myLevel < level then
        return nil
    end
    local op = {
        type   = symbol,
        start  = getPosition(Tokens[Index], 'left'),
        finish = getPosition(Tokens[Index] + #token - 1, 'right'),
    }
    if not asAction then
        if token == '=' then
            pushError {
                type   = 'ERR_EQ_AS_ASSIGN',
                start  = op.start,
                finish = op.finish,
                fix    = {
                    title = 'FIX_EQ_AS_ASSIGN',
                    {
                        start  = op.start,
                        finish = op.finish,
                        text   = '==',
                    }
                }
            }
        end
    end
    if BinaryAlias[token] then
        if not State.options.nonstandardSymbol[token] then
            pushError {
                type   = 'ERR_NONSTANDARD_SYMBOL',
                start  = op.start,
                finish = op.finish,
                info   = {
                    symbol = symbol,
                },
                fix    = {
                    title  = 'FIX_NONSTANDARD_SYMBOL',
                    symbol = symbol,
                    {
                        start  = op.start,
                        finish = op.finish,
                        text   = symbol,
                    },
                }
            }
        end
    end
    if token == '//'
    or token == '<<'
    or token == '>>' then
        if  State.version ~= 'Lua 5.3'
        and State.version ~= 'Lua 5.4'
        and State.version ~= 'Lua 5.5' then
            pushError {
                type    = 'UNSUPPORT_SYMBOL',
                version = {'Lua 5.3', 'Lua 5.4', 'Lua 5.5'},
                start   = op.start,
                finish  = op.finish,
                info    = {
                    version = State.version,
                }
            }
        end
    end
    Index = Index + 2
    return op, myLevel
end

function parseExp(asAction, level)
    local exp
    local uop, uopLevel = parseUnaryOP()
    if uop then
        skipSpace()
        local child = parseExp(asAction, uopLevel)
        -- 预计算负数
        if  uop.type == '-'
        and child
        and (child.type == 'number' or child.type == 'integer') then
            child.start = uop.start
            child[1]    = - child[1]
            exp = child
        else
            exp = {
                type   = 'unary',
                op     = uop,
                start  = uop.start,
                finish = child and child.finish or uop.finish,
                [1]    = child,
            }
            if child then
                child.parent = exp
            else
                missExp()
            end
        end
    else
        exp = parseExpUnit()
        if not exp then
            return nil
        end
    end

    while true do
        skipSpace()
        local bop, bopLevel = parseBinaryOP(asAction, level)
        if not bop then
            break
        end

        ::AGAIN::
        skipSpace()
        local isForward = SymbolForward[bopLevel]
        local child = parseExp(asAction, isForward and (bopLevel + 0.5) or bopLevel)
        if not child then
            if skipUnknownSymbol() then
                goto AGAIN
            else
                missExp()
            end
        end
        local bin = {
            type   = 'binary',
            start  = exp.start,
            finish = child and child.finish or bop.finish,
            op     = bop,
            [1]    = exp,
            [2]    = child
        }
        exp.parent = bin
        if child then
            child.parent = bin
        end
        exp = bin
    end

    return exp
end

local function skipSeps()
    while true do
        skipSpace()
        if Tokens[Index + 1] == ',' then
            missExp()
            Index = Index + 2
        else
            break
        end
    end
end

---@return parser.object?   first
---@return parser.object?   second
---@return parser.object[]? rest
local function parseSetValues()
    skipSpace()
    local first = parseExp()
    if not first then
        return nil
    end
    skipSpace()
    if Tokens[Index + 1] ~= ',' then
        return first
    end
    Index = Index + 2
    skipSeps()
    local second = parseExp()
    if not second then
        missExp()
        return first
    end
    skipSpace()
    if Tokens[Index + 1] ~= ',' then
        return first, second
    end
    Index = Index + 2
    skipSeps()
    local third = parseExp()
    if not third then
        missExp()
        return first, second
    end

    local rest = { third }
    while true do
        skipSpace()
        if Tokens[Index + 1] ~= ',' then
            return first, second, rest
        end
        Index = Index + 2
        skipSeps()
        local exp = parseExp()
        if not exp then
            missExp()
            return first, second, rest
        end
        rest[#rest+1] = exp
    end
end

local function pushActionIntoCurrentChunk(action)
    local chunk = Chunk[#Chunk]
    if chunk then
        chunk[#chunk+1] = action
        action.parent   = chunk
    end
end

---@return parser.object?   second
---@return parser.object[]? rest
local function parseVarTails(parser, isLocal)
    if Tokens[Index + 1] ~= ',' then
        return nil
    end
    Index = Index + 2
    skipSpace()
    local second = parser(true)
    if not second then
        missName()
        return nil
    end
    if isLocal then
        createLocal(second, parseLocalAttrs())
    end
    skipSpace()
    if Tokens[Index + 1] ~= ',' then
        return second
    end
    Index = Index + 2
    skipSeps()
    local third = parser(true)
    if not third then
        missName()
        return second
    end
    if isLocal then
        createLocal(third, parseLocalAttrs())
    end
    local rest = { third }
    while true do
        skipSpace()
        if Tokens[Index + 1] ~= ',' then
            return second, rest
        end
        Index = Index + 2
        skipSeps()
        local name = parser(true)
        if not name then
            missName()
            return second, rest
        end
        if isLocal then
            createLocal(name, parseLocalAttrs())
        end
        rest[#rest+1] = name
    end
end

local function bindValue(n, v, index, lastValue, isLocal, isSet)
    if isLocal then
        if v and v.special then
            addSpecial(v.special, n)
        end
    elseif isSet then
        n.type = GetToSetMap[n.type] or n.type
        if n.type == 'setlocal' then
            local loc = n.node
            if loc.attrs then
                pushError {
                    type   = 'SET_CONST',
                    start  = n.start,
                    finish = n.finish,
                }
            end
        end
        if n.type == 'setglobal' then
            -- n.var is set by resolveName, contains the found global declaration or global *
            if n.var then
                -- Check if it's const
                if n.var.attrs and hasAttr(n.var.attrs, 'const') then
                    pushError {
                        type   = 'ASSIGN_CONST_GLOBAL',
                        start  = n.start,
                        finish = n.finish,
                    }
                end
            else
                -- No var means no global declaration found at all (not even global *)
                -- This means we're not in strict global scope, allow free assignment
            end
        end
    end
    if not v and lastValue then
        if lastValue.type == 'call'
        or lastValue.type == 'varargs' then
            v = lastValue
            if not v.extParent then
                v.extParent = {}
            end
        end
    end
    if v then
        if v.type == 'call'
        or v.type == 'varargs' then
            local select = {
                type   = 'select',
                sindex = index,
                start  = v.start,
                finish = v.finish,
                vararg = v
            }
            if v.parent then
                v.extParent[#v.extParent+1] = select
            else
                v.parent = select
            end
            v = select
        end
        n.value  = v
        n.range  = v.finish
        v.parent = n
    end
end

local function parseMultiVars(n1, parser, isLocal)
    local n2, nrest = parseVarTails(parser, isLocal)
    skipSpace()
    local v1, v2, vrest
    local isSet
    local max = 1
    if expectAssign(not isLocal) then
        v1, v2, vrest = parseSetValues()
        isSet = true
        if not v1 then
            missExp()
        end
    end
    local index = 1
    bindValue(n1, v1, index, nil, isLocal, isSet)
    local lastValue = v1
    local lastVar   = n1
    if n2 then
        max = 2
        if not v2 then
            index = 2
        end
        bindValue(n2, v2, index, lastValue, isLocal, isSet)
        lastValue = v2 or lastValue
        lastVar   = n2
        pushActionIntoCurrentChunk(n2)
    end
    if nrest then
        for i = 1, #nrest do
            local n = nrest[i]
            local v = vrest and vrest[i]
            max = i + 2
            if not v then
                index = index + 1
            end
            bindValue(n, v, index, lastValue, isLocal, isSet)
            lastValue = v or lastValue
            lastVar   = n
            pushActionIntoCurrentChunk(n)
        end
    end

    local effect = lastValue and lastValue.finish or lastVar.finish
    n1.effect = effect
    if n2 then
        n2.effect = effect
    end
    if nrest then
        for i = 1, #nrest do
            nrest[i].effect = effect
        end
    end

    if isLocal then
        checkLocalCloseList(n1, n2, nrest)
    end

    if v2 and not n2 then
        v2.redundant = {
            max    = max,
            passed = 2,
        }
        pushActionIntoCurrentChunk(v2)
    end
    if vrest then
        for i = 1, #vrest do
            local v = vrest[i]
            if not nrest or not nrest[i] then
                v.redundant = {
                    max    = max,
                    passed = i + 2,
                }
                pushActionIntoCurrentChunk(v)
            end
        end
    end

    return n1, isSet
end

local function compileExpAsAction(exp)
    pushActionIntoCurrentChunk(exp)
    if GetToSetMap[exp.type] then
        skipSpace()
        local isLocal
        if exp.type == 'getlocal' and exp[1] == State.ENVMode then
            exp.special = nil
            -- TODO: need + 1 at the end
            LocalCount = LocalCount - 1
            local loc = createLocal(exp, parseLocalAttrs())
            loc.locPos = exp.start
            loc.effect = maxinteger
            isLocal = true
            skipSpace()
        end
        local action, isSet = parseMultiVars(exp, parseExp, isLocal)
        if isSet
        or action.type == 'getmethod' then
            return action
        end
    end

    if exp.type == 'call' then
        if exp.hasExit then
            for i = #Chunk, 1, -1 do
                local block = Chunk[i]
                if block.type == 'ifblock'
                or block.type == 'elseifblock'
                or block.type == 'elseblock'
                or block.type == 'function' then
                    block.hasExit = true
                    break
                end
            end
        end
        return exp
    end

    if exp.type == 'binary' then
        if GetToSetMap[exp[1].type] then
            local op = exp.op
            if op.type == '==' then
                pushError {
                    type   = 'ERR_ASSIGN_AS_EQ',
                    start  = op.start,
                    finish = op.finish,
                    fix    = {
                        title = 'FIX_ASSIGN_AS_EQ',
                        {
                            start  = op.start,
                            finish = op.finish,
                            text   = '=',
                        }
                    }
                }
                return
            end
        end
    end

    pushError {
        type   = 'EXP_IN_ACTION',
        start  = exp.start,
        finish = exp.finish,
    }

    return exp
end

local function parseLocal()
    local locPos = getPosition(Tokens[Index], 'left')
    Index = Index + 2
    skipSpace()
    local attrsBefore = parseLocalAttrs('prefix')
    if attrsBefore and State.version ~= 'Lua 5.5' then
        pushError {
            type    = 'UNSUPPORT_SYMBOL',
            start   = attrsBefore.start,
            finish  = attrsBefore.finish,
            version = 'Lua 5.5',
        }
    end
    skipSpace()
    local word, wstart, wfinish = peekWord()
    if not word then
        missName()
        return nil
    end

    if word == 'function' then
        if attrsBefore then
            pushError {
                type   = 'MISS_NAME',
                start  = wstart,
                finish = wfinish,
            }
        end
        local func = parseFunction('local', true)
        local name = func.name
        if name then
            func.name    = nil
            name.value  = func
            name.vstart = func.start
            name.range  = func.finish
            name.locPos = locPos
            func.parent  = name
            pushActionIntoCurrentChunk(name)
            return name
        else
            missName(func.keyword[2])
            pushActionIntoCurrentChunk(func)
            return func
        end
    end

    local name = parseName(true)
    if not name then
        missName()
        return nil
    end
    local attrsAfter = parseLocalAttrs('suffix')
    local attrs = mergeLocalAttrs(attrsBefore, attrsAfter)
    local loc = createLocal(name, attrs)
    loc.locPos = locPos
    loc.effect = maxinteger
    pushActionIntoCurrentChunk(loc)
    skipSpace()
    parseMultiVars(loc, parseName, true)

    return loc
end

local function parseGlobal()
    local globalPos = getPosition(Tokens[Index], 'left')
    Index = Index + 2
    skipSpace()
    local word = peekWord()

    -- global function Name funcbody
    if word == 'function' then
        local func = parseFunction('global', true)
        local name = func.name
        if name then
            func.name    = nil
            name.type    = 'setglobal'
            name.value   = func
            name.vstart  = func.start
            name.range   = func.finish
            func.parent  = name
            pushActionIntoCurrentChunk(name)
            return name
        else
            missName(func.keyword[2])
            pushActionIntoCurrentChunk(func)
            return func
        end
    end

    -- Parse optional attributes before '*'
    local attrs = parseLocalAttrs()
    skipSpace()
    if Tokens[Index + 1] == '*' then
        local action = {
            type   = 'globalall',
            start  = getPosition(Tokens[Index], 'left'),
            finish = getPosition(Tokens[Index], 'right'),
            attrs  = attrs,
            [1]    = '*',
        }
        if attrs then
            attrs.parent = action
            for i = 1, #attrs do
                local a = attrs[i]
                if a[1] == 'close' then
                    pushError {
                        type   = 'GLOBAL_CLOSE_ATTRIBUTE',
                        start  = a.start,
                        finish = a.finish,
                    }
                end
            end
        end
        createGlobalDeclare(action, attrs)
        Index = Index + 2
        pushActionIntoCurrentChunk(action)
        return action
    end

    -- global attnamelist ['=' explist]
    local name = parseName(true)
    if not name then
        missName()
        return nil
    end

    local glob = {
        start  = name.start,
        finish = name.finish,
        [1]    = name[1],
    }

    if attrs then
        glob.attrs = attrs
        attrs.parent = glob
        for i = 1, #attrs do
            if attrs[i][1] == 'close' then
                pushError {
                    type   = 'GLOBAL_CLOSE_ATTRIBUTE',
                    start  = attrs[i].start,
                    finish = attrs[i].finish,
                }
            end
        end
    end

    -- attributes after name
    local attrsAfter = parseLocalAttrs()
    if attrsAfter then
        for i = 1, #attrsAfter do
            if attrsAfter[i][1] == 'close' then
                pushError {
                    type   = 'GLOBAL_CLOSE_ATTRIBUTE',
                    start  = attrsAfter[i].start,
                    finish = attrsAfter[i].finish,
                }
            end
        end
        if glob.attrs then
            for i = 1, #attrsAfter do
                glob.attrs[#glob.attrs+1] = attrsAfter[i]
            end
        else
            glob.attrs = attrsAfter
            attrsAfter.parent = glob
        end
        glob.finish = attrsAfter[#attrsAfter].finish
    end

    createGlobalDeclare(glob, glob.attrs)
    pushActionIntoCurrentChunk(glob)
    skipSpace()

    local function parseGlobalVar()
        local attrsN = parseLocalAttrs()
        skipSpace()
        local nameN = parseName(true)
        if nameN then
            local gN = {
                start  = attrsN and attrsN.start or nameN.start,
                finish = nameN.finish,
                [1]    = nameN[1],
            }
            if attrsN then
                gN.attrs = attrsN
                attrsN.parent = gN
                for i = 1, #attrsN do
                    if attrsN[i][1] == 'close' then
                        pushError {
                            type   = 'GLOBAL_CLOSE_ATTRIBUTE',
                            start  = attrsN[i].start,
                            finish = attrsN[i].finish,
                        }
                    end
                end
            end
            local attrsNAfter = parseLocalAttrs()
            if attrsNAfter then
                for i = 1, #attrsNAfter do
                    if attrsNAfter[i][1] == 'close' then
                        pushError {
                            type   = 'GLOBAL_CLOSE_ATTRIBUTE',
                            start  = attrsNAfter[i].start,
                            finish = attrsNAfter[i].finish,
                        }
                    end
                end
                if gN.attrs then
                    for i = 1, #attrsNAfter do
                        gN.attrs[#gN.attrs+1] = attrsNAfter[i]
                    end
                else
                    gN.attrs = attrsNAfter
                    attrsNAfter.parent = gN
                end
                gN.finish = attrsNAfter[#attrsNAfter].finish
            end
            createGlobalDeclare(gN, gN.attrs)
            return gN
        end
        return nil
    end

    parseMultiVars(glob, parseGlobalVar, false)
    return glob
end

local function parseDo()
    local doLeft  = getPosition(Tokens[Index], 'left')
    local doRight = getPosition(Tokens[Index] + 1, 'right')
    local obj = {
        type   = 'do',
        start  = doLeft,
        finish = doRight,
        bstart = doRight,
        keyword = {
            [1] = doLeft,
            [2] = doRight,
        },
    }
    Index = Index + 2
    pushActionIntoCurrentChunk(obj)
    pushChunk(obj)
    parseActions()
    popChunk()
    obj.bfinish = getPosition(Tokens[Index], 'left')
    if Tokens[Index + 1] == 'end' then
        obj.finish     = getPosition(Tokens[Index] + 2, 'right')
        obj.keyword[3] = getPosition(Tokens[Index], 'left')
        obj.keyword[4] = getPosition(Tokens[Index] + 2, 'right')
        Index = Index + 2
    else
        missEnd(doLeft, doRight)
    end
    if obj.locals then
        LocalCount = LocalCount - #obj.locals
    end

    return obj
end

local function parseReturn()
    local returnLeft  = getPosition(Tokens[Index], 'left')
    local returnRight = getPosition(Tokens[Index] + 5, 'right')
    Index = Index + 2
    skipSpace()
    local rtn = parseExpList(true)
    if rtn then
        rtn.type  = 'return'
        rtn.start = returnLeft
    else
        rtn = {
            type   = 'return',
            start  = returnLeft,
            finish = returnRight,
        }
    end
    pushActionIntoCurrentChunk(rtn)
    for i = #Chunk, 1, -1 do
        local block = Chunk[i]
        if block.type == 'function'
        or block.type == 'main' then
            if not block.returns then
                block.returns = {}
            end
            block.returns[#block.returns+1] = rtn
            break
        end
    end
    for i = #Chunk, 1, -1 do
        local block = Chunk[i]
        if block.type == 'ifblock'
        or block.type == 'elseifblock'
        or block.type == 'elseblock'
        or block.type == 'function' then
            block.hasReturn = true
            break
        end
    end

    return rtn
end

local function parseLabel()
    local left = getPosition(Tokens[Index], 'left')
    Index = Index + 2
    skipSpace()
    local label = parseName()
    skipSpace()

    if not label then
        missName()
    end

    if Tokens[Index + 1] == '::' then
        Index = Index + 2
    else
        if label then
            missSymbol '::'
        end
    end

    if not label then
        return nil
    end

    label.type = 'label'
    pushActionIntoCurrentChunk(label)

    local block = guide.getBlock(label)
    if block then
        if not block.labels then
            block.labels = {}
        end
        local name = label[1]
        local olabel = guide.getLabel(block, name)
        if olabel then
            if State.version == 'Lua 5.4'
            or State.version == 'Lua 5.5'
            or block == guide.getBlock(olabel) then
                pushError {
                    type   = 'REDEFINED_LABEL',
                    start  = label.start,
                    finish = label.finish,
                    relative = {
                        {
                            olabel.start,
                            olabel.finish,
                        }
                    }
                }
            end
        end
        block.labels[name] = label
    end

    if State.version == 'Lua 5.1' then
        pushError {
            type   = 'UNSUPPORT_SYMBOL',
            start  = left,
            finish = lastRightPosition(),
            version = {'Lua 5.2', 'Lua 5.3', 'Lua 5.4', 'Lua 5.5', 'LuaJIT'},
            info = {
                version = State.version,
            }
        }
        return
    end
    return label
end

local function parseGoTo()
    local start = getPosition(Tokens[Index], 'left')
    Index = Index + 2
    skipSpace()

    local action = parseName()
    if not action then
        missName()
        return nil
    end

    action.type = 'goto'
    action.keyStart = start

    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        if chunk.type == 'function'
        or chunk.type == 'main' then
            if not chunk.gotos then
                chunk.gotos = {}
            end
            chunk.gotos[#chunk.gotos+1] = action
            break
        end
    end
    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        if chunk.type == 'ifblock'
        or chunk.type == 'elseifblock'
        or chunk.type == 'elseblock' then
            chunk.hasGoTo = true
            break
        end
    end

    pushActionIntoCurrentChunk(action)
    return action
end

local function parseIfBlock(parent)
    local ifLeft  = getPosition(Tokens[Index], 'left')
    local ifRight = getPosition(Tokens[Index] + 1, 'right')
    Index = Index + 2
    local ifblock = {
        type    = 'ifblock',
        parent  = parent,
        start   = ifLeft,
        finish  = ifRight,
        bstart  = ifRight,
        keyword = {
            [1] = ifLeft,
            [2] = ifRight,
        }
    }
    skipSpace()
    local filter = parseExp()
    if filter then
        ifblock.filter = filter
        ifblock.finish = filter.finish
        ifblock.bstart = ifblock.finish
        filter.parent  = ifblock
    else
        missExp()
    end
    skipSpace()
    local thenToken = Tokens[Index + 1]
    if thenToken == 'then'
    or thenToken == 'do' then
        ifblock.finish     = getPosition(Tokens[Index] + #thenToken - 1, 'right')
        ifblock.bstart     = ifblock.finish
        ifblock.keyword[3] = getPosition(Tokens[Index], 'left')
        ifblock.keyword[4] = ifblock.finish
        if thenToken == 'do' then
            pushError {
                type   = 'ERR_THEN_AS_DO',
                start  = ifblock.keyword[3],
                finish = ifblock.keyword[4],
                fix = {
                    title = 'FIX_THEN_AS_DO',
                    {
                        start  = ifblock.keyword[3],
                        finish = ifblock.keyword[4],
                        text   = 'then',
                    }
                }
            }
        end
        Index = Index + 2
    else
        missSymbol 'then'
    end
    pushChunk(ifblock)
    parseActions()
    popChunk()
    ifblock.finish = getPosition(Tokens[Index], 'left')
    ifblock.bfinish = ifblock.finish
    if ifblock.locals then
        LocalCount = LocalCount - #ifblock.locals
    end
    return ifblock
end

local function parseElseIfBlock(parent)
    local ifLeft  = getPosition(Tokens[Index], 'left')
    local ifRight = getPosition(Tokens[Index] + 5, 'right')
    local elseifblock = {
        type    = 'elseifblock',
        parent  = parent,
        start   = ifLeft,
        finish  = ifRight,
        bstart  = ifRight,
        keyword = {
            [1] = ifLeft,
            [2] = ifRight,
        }
    }
    Index = Index + 2
    skipSpace()
    local filter = parseExp()
    if filter then
        elseifblock.filter = filter
        elseifblock.finish = filter.finish
        elseifblock.bstart = elseifblock.finish
        filter.parent = elseifblock
    else
        missExp()
    end
    skipSpace()
    local thenToken = Tokens[Index + 1]
    if thenToken == 'then'
    or thenToken == 'do' then
        elseifblock.finish     = getPosition(Tokens[Index] + #thenToken - 1, 'right')
        elseifblock.bstart     = elseifblock.finish
        elseifblock.keyword[3] = getPosition(Tokens[Index], 'left')
        elseifblock.keyword[4] = elseifblock.finish
        if thenToken == 'do' then
            pushError {
                type   = 'ERR_THEN_AS_DO',
                start  = elseifblock.keyword[3],
                finish = elseifblock.keyword[4],
                fix = {
                    title = 'FIX_THEN_AS_DO',
                    {
                        start  = elseifblock.keyword[3],
                        finish = elseifblock.keyword[4],
                        text   = 'then',
                    }
                }
            }
        end
        Index = Index + 2
    else
        missSymbol 'then'
    end
    pushChunk(elseifblock)
    parseActions()
    popChunk()
    elseifblock.finish = getPosition(Tokens[Index], 'left')
    elseifblock.bfinish = elseifblock.finish
    if elseifblock.locals then
        LocalCount = LocalCount - #elseifblock.locals
    end
    return elseifblock
end

local function parseElseBlock(parent)
    local ifLeft  = getPosition(Tokens[Index], 'left')
    local ifRight = getPosition(Tokens[Index] + 3, 'right')
    local elseblock = {
        type    = 'elseblock',
        parent  = parent,
        start   = ifLeft,
        finish  = ifRight,
        bstart  = ifRight,
        keyword = {
            [1] = ifLeft,
            [2] = ifRight,
        }
    }
    Index = Index + 2
    skipSpace()
    pushChunk(elseblock)
    parseActions()
    popChunk()
    elseblock.finish = getPosition(Tokens[Index], 'left')
    elseblock.bfinish = elseblock.finish
    if elseblock.locals then
        LocalCount = LocalCount - #elseblock.locals
    end
    return elseblock
end

local function parseIf()
    local token = Tokens[Index + 1]
    local left  = getPosition(Tokens[Index], 'left')
    local action  = {
        type   = 'if',
        start  = left,
        finish = getPosition(Tokens[Index] + #token - 1, 'right'),
    }
    pushActionIntoCurrentChunk(action)
    if token ~= 'if' then
        missSymbol('if', left, left)
    end
    local hasElse
    while true do
        local word = Tokens[Index + 1]
        local child
        if     word == 'if' then
            child = parseIfBlock(action)
        elseif word == 'elseif' then
            child = parseElseIfBlock(action)
        elseif word == 'else' then
            child = parseElseBlock(action)
        end
        if not child then
            break
        end
        if hasElse then
            pushError {
                type   = 'BLOCK_AFTER_ELSE',
                start  = child.start,
                finish = child.finish,
            }
        end
        if word == 'else' then
            hasElse = true
        end
        action[#action+1] = child
        action.finish = child.finish
        skipSpace()
    end

    if Tokens[Index + 1] == 'end' then
        action.finish = getPosition(Tokens[Index] + 2, 'right')
        Index = Index + 2
    else
        missEnd(action[1].keyword[1], action[1].keyword[2])
    end

    return action
end

local function parseFor()
    local action = {
        type    = 'for',
        start   = getPosition(Tokens[Index], 'left'),
        finish  = getPosition(Tokens[Index] + 2, 'right'),
        keyword = {},
    }
    action.bstart     = action.finish
    action.keyword[1] = action.start
    action.keyword[2] = action.finish
    Index = Index + 2
    pushActionIntoCurrentChunk(action)
    pushChunk(action)
    skipSpace()
    local nameOrList = parseNameOrList(action)
    if not nameOrList then
        missName()
    end
    skipSpace()
    local forStateVars
    -- for i =
    if expectAssign() then
        action.type = 'loop'

        skipSpace()
        local expList = parseExpList()
        local name
        if nameOrList then
            if nameOrList.type == 'name' then
                name = nameOrList
            else
                name = nameOrList[1]
            end
        end
        -- for x in ... uses 4 variables
        forStateVars = 3
        LocalCount = LocalCount + forStateVars
        if name then
            ---@cast name parser.object
            -- In Lua 5.5, for loop variables are treated as constants
            local attrs
            if State.version == 'Lua 5.5' then
                attrs = {
                    type = 'localattrs',
                    start = name.start,
                    finish = name.finish,
                    [1] = {
                        type = 'localattr',
                        start = name.start,
                        finish = name.finish,
                        parent = nil, -- will be set by createLocal
                        [1] = 'const',
                    }
                }
                attrs[1].parent = attrs
            end
            local loc = createLocal(name, attrs)
            loc.parent    = action
            action.finish = name.finish
            action.bstart = action.finish
            action.loc    = loc
        end
        if expList then
            expList.parent = action
            local value = expList[1]
            if value then
                value.parent  = expList
                action.init   = value
                action.finish = expList[#expList].finish
                action.bstart = action.finish
            end
            local max = expList[2]
            if max then
                max.parent    = expList
                action.max    = max
                action.finish = max.finish
                action.bstart = action.finish
            else
                pushError {
                    type   = 'MISS_LOOP_MAX',
                    start  = lastRightPosition(),
                    finish = lastRightPosition(),
                }
            end
            local step = expList[3]
            if step then
                step.parent   = expList
                action.step   = step
                action.finish = step.finish
                action.bstart = action.finish
            end
        else
            pushError {
                type   = 'MISS_LOOP_MIN',
                start  = lastRightPosition(),
                finish = lastRightPosition(),
            }
        end

        if action.loc then
            action.loc.effect = action.finish
        end
    elseif Tokens[Index + 1] == 'in' then
        action.type = 'in'
        local inLeft  = getPosition(Tokens[Index], 'left')
        local inRight = getPosition(Tokens[Index] + 1, 'right')
        Index = Index + 2
        skipSpace()

        local exps = parseExpList()

        action.finish     = inRight
        action.bstart     = action.finish
        action.keyword[3] = inLeft
        action.keyword[4] = inRight

        local list
        if nameOrList and nameOrList.type == 'name' then
            list = {
                type   = 'list',
                start  = nameOrList.start,
                finish = nameOrList.finish,
                parent = action,
                [1]    = nameOrList,
            }
        else
            list = nameOrList
        end

        if exps then
            local lastExp = exps[#exps]
            if lastExp then
                action.finish = lastExp.finish
                action.bstart = action.finish
            end

            action.exps = exps
            exps.parent = action
            for i = 1, #exps do
                local exp = exps[i]
                exp.parent = exps
            end
        else
            missExp()
        end

        if State.version == 'Lua 5.4'
        or State.version == 'Lua 5.5' then
            forStateVars = 4
        else
            forStateVars = 3
        end
        LocalCount = LocalCount + forStateVars

        if list then
            local lastName  = list[#list]
            list.range  = lastName and lastName.range or inRight
            action.keys = list
            for i = 1, #list do
                local obj = list[i]
                ---@cast obj parser.object
                -- In Lua 5.5, for first loop variable is treated as constant
                local attrs
                if i == 1 and State.version == 'Lua 5.5' then
                    attrs = {
                        type = 'localattrs',
                        start = obj.start,
                        finish = obj.finish,
                        [1] = {
                            type = 'localattr',
                            start = obj.start,
                            finish = obj.finish,
                            parent = nil, -- will be set by createLocal
                            [1] = 'const',
                        }
                    }
                    attrs[1].parent = attrs
                end
                local loc = createLocal(obj, attrs)
                loc.parent = action
                loc.effect = action.finish
            end
        end
    else
        missSymbol 'in'
    end

    skipSpace()
    local doToken = Tokens[Index + 1]
    if doToken == 'do'
    or doToken == 'then' then
        local left  = getPosition(Tokens[Index], 'left')
        local right = getPosition(Tokens[Index] + #doToken - 1, 'right')
        action.finish                     = right
        action.bstart                     = action.finish
        action.keyword[#action.keyword+1] = left
        action.keyword[#action.keyword+1] = right
        if doToken == 'then' then
            pushError {
                type   = 'ERR_DO_AS_THEN',
                start  = left,
                finish = right,
                fix = {
                    title = 'FIX_DO_AS_THEN',
                    {
                        start  = left,
                        finish = right,
                        text    = 'do',
                    }
                }
            }
        end
        Index = Index + 2
    else
        missSymbol 'do'
    end

    skipSpace()
    parseActions()
    popChunk()
    skipSpace()
    action.bfinish = getPosition(Tokens[Index], 'left')
    if Tokens[Index + 1] == 'end' then
        action.finish                     = getPosition(Tokens[Index] + 2, 'right')
        action.keyword[#action.keyword+1] = getPosition(Tokens[Index], 'left')
        action.keyword[#action.keyword+1] = action.finish
        Index = Index + 2
    else
        missEnd(action.keyword[1], action.keyword[2])
    end

    if action.locals then
        LocalCount = LocalCount - #action.locals
    end
    if forStateVars then
        LocalCount = LocalCount - forStateVars
    end

    return action
end

local function parseWhile()
    local action = {
        type    = 'while',
        start   = getPosition(Tokens[Index], 'left'),
        finish  = getPosition(Tokens[Index] + 4, 'right'),
        keyword = {},
    }
    action.bstart     = action.finish
    action.keyword[1] = action.start
    action.keyword[2] = action.finish
    Index = Index + 2

    skipSpace()
    local nextToken = Tokens[Index + 1]
    local filter =  nextToken ~= 'do'
                and nextToken ~= 'then'
                and parseExp()
    if filter then
        action.filter = filter
        action.finish = filter.finish
        filter.parent = action
    else
        missExp()
    end

    skipSpace()
    local doToken = Tokens[Index + 1]
    if doToken == 'do'
    or doToken == 'then' then
        local left  = getPosition(Tokens[Index], 'left')
        local right = getPosition(Tokens[Index] + #doToken - 1, 'right')
        action.finish                     = left
        action.bstart                     = right
        action.keyword[#action.keyword+1] = left
        action.keyword[#action.keyword+1] = right
        if doToken == 'then' then
            pushError {
                type   = 'ERR_DO_AS_THEN',
                start  = left,
                finish = right,
                fix = {
                    title = 'FIX_DO_AS_THEN',
                    {
                        start  = left,
                        finish = right,
                        text    = 'do',
                    }
                }
            }
        end
        Index = Index + 2
    else
        missSymbol 'do'
    end

    pushActionIntoCurrentChunk(action)
    pushChunk(action)
    skipSpace()
    parseActions()
    popChunk()

    skipSpace()
    action.bfinish = getPosition(Tokens[Index], 'left')
    if Tokens[Index + 1] == 'end' then
        action.finish                     = getPosition(Tokens[Index] + 2, 'right')
        action.keyword[#action.keyword+1] = getPosition(Tokens[Index], 'left')
        action.keyword[#action.keyword+1] = action.finish
        Index = Index + 2
    else
        missEnd(action.keyword[1], action.keyword[2])
    end

    if action.locals then
        LocalCount = LocalCount - #action.locals
    end

    return action
end

local function parseRepeat()
    local action = {
        type    = 'repeat',
        start   = getPosition(Tokens[Index], 'left'),
        finish  = getPosition(Tokens[Index] + 5, 'right'),
        keyword = {},
    }
    action.bstart     = action.finish
    action.keyword[1] = action.start
    action.keyword[2] = action.finish
    Index = Index + 2

    pushActionIntoCurrentChunk(action)
    pushChunk(action)
    skipSpace()
    parseActions()

    skipSpace()
    action.bfinish = getPosition(Tokens[Index], 'left')
    if Tokens[Index + 1] == 'until' then
        action.finish                     = getPosition(Tokens[Index] + 4, 'right')
        action.keyword[#action.keyword+1] = getPosition(Tokens[Index], 'left')
        action.keyword[#action.keyword+1] = action.finish
        Index = Index + 2

        skipSpace()
        local filter = parseExp()
        if filter then
            action.filter = filter
            filter.parent = action
        else
            missExp()
        end

    else
        missSymbol 'until'
    end

    popChunk()
    if action.filter then
        action.finish = action.filter.finish
    end

    if action.locals then
        LocalCount = LocalCount - #action.locals
    end

    return action
end

local function parseBreak()
    local returnLeft  = getPosition(Tokens[Index], 'left')
    local returnRight = getPosition(Tokens[Index] + #Tokens[Index + 1] - 1, 'right')
    Index = Index + 2
    skipSpace()
    local action = {
        type   = 'break',
        start  = returnLeft,
        finish = returnRight,
    }

    local ok
    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        if chunk.type == 'function' then
            break
        end
        if chunk.type == 'while'
        or chunk.type == 'in'
        or chunk.type == 'loop'
        or chunk.type == 'repeat'
        or chunk.type == 'for' then
            if not chunk.breaks then
                chunk.breaks = {}
            end
            chunk.breaks[#chunk.breaks+1] = action
            ok = true
            break
        end
    end
    for i = #Chunk, 1, -1 do
        local chunk = Chunk[i]
        if chunk.type == 'ifblock'
        or chunk.type == 'elseifblock'
        or chunk.type == 'elseblock' then
            chunk.hasBreak = true
            break
        end
    end
    if not ok and Mode == 'Lua' then
        pushError {
            type   = 'BREAK_OUTSIDE',
            start  = action.start,
            finish = action.finish,
        }
    end

    pushActionIntoCurrentChunk(action)
    return action
end

function parseAction()
    local token = Tokens[Index + 1]

    if token == '::' then
        return parseLabel()
    end

    if token == 'local' then
        return parseLocal()
    end

    if token == 'global' then
        local nextToken = Tokens[Index + 3]
        if isGlobalActionStart(nextToken) then
            return parseGlobal()
        end
    end

    if token == 'if'
    or token == 'elseif'
    or token == 'else' then
        return parseIf()
    end

    if token == 'for' then
        return parseFor()
    end

    if token == 'do' then
        return parseDo()
    end

    if token == 'return' then
        return parseReturn()
    end

    if token == 'break' then
        return parseBreak()
    end

    if token == 'continue' and State.options.nonstandardSymbol['continue'] then
        return parseBreak()
    end

    if token == 'while' then
        return parseWhile()
    end

    if token == 'repeat' then
        return parseRepeat()
    end

    if token == 'goto' and isKeyWord('goto', Tokens[Index + 3]) then
        return parseGoTo()
    end

    if token == 'function' then
        local exp = parseFunction(false, true)
        local name = exp.name
        if name then
            exp.name    = nil
            name.type   = GetToSetMap[name.type]
            name.value  = exp
            name.vstart = exp.start
            name.range  = exp.finish
            exp.parent  = name
            if name.type == 'setlocal' then
                local loc = name.node
                if loc.attrs then
                    pushError {
                        type   = 'SET_CONST',
                        start  = name.start,
                        finish = name.finish,
                    }
                end
            end
            pushActionIntoCurrentChunk(name)
            return name
        else
            pushActionIntoCurrentChunk(exp)
            missName(exp.keyword[2])
            return exp
        end
    end

    local exp = parseExp(true)
    if exp then
        local action = compileExpAsAction(exp)
        if action then
            return action
        end
    end
    return nil, true
end

local function skipFirstComment()
    if Tokens[Index + 1] ~= '#' then
        return
    end
    while true do
        Index = Index + 2
        local token = Tokens[Index + 1]
        if not token then
            break
        end
        if NLMap[token] then
            skipNL()
            break
        end
    end
end

local function parseLua()
    local main = {
        type   = 'main',
        start  = 0,
        finish = 0,
        bstart = 0,
    }
    pushChunk(main)
    createLocal{
        type   = 'local',
        start  = -1,
        finish = -1,
        effect = -1,
        parent = main,
        tag    = '_ENV',
        special= '_G',
        [1]    = State.ENVMode,
    }
    LocalCount = 0
    skipFirstComment()
    while true do
        parseActions()
        if Index <= #Tokens then
            unknownSymbol()
            Index = Index + 2
        else
            break
        end
    end
    popChunk()
    main.finish = getPosition(#Lua, 'right')
    main.bfinish = main.finish

    return main
end

local function initState(lua, version, options)
    Lua                 = lua
    Line                = 0
    LineOffset          = 1
    LastTokenFinish     = 0
    LocalCount          = 0
    LocalLimited        = false
    Chunk               = {}
    Tokens              = tokens(lua)
    Index               = 1
    ---@class parser.state
    ---@field uri uri
    ---@field lines integer[]
    local state = {
        version = version,
        lua     = lua,
        ast     = {},
        errs    = {},
        comms   = {},
        lines   = {
            [0] = 1,
            size = #lua,
        },
        options = options or {},
    }
    if not state.options.nonstandardSymbol then
        state.options.nonstandardSymbol = {}
    end
    State = state
    if version == 'Lua 5.1' or version == 'LuaJIT' then
        state.ENVMode = '@fenv'
    else
        state.ENVMode = '_ENV'
    end

    pushError = function (err)
        local errs = state.errs
        if err.finish < err.start then
            err.finish = err.start
        end
        local last = errs[#errs]
        if last then
            if last.start <= err.start and last.finish >= err.finish then
                return
            end
        end
        err.level = err.level or 'Error'
        errs[#errs+1] = err
        return err
    end
end

return function (lua, mode, version, options)
    Mode = mode
    initState(lua, version, options)
    skipSpace()
    if     mode == 'Lua' then
        State.ast = parseLua()
    elseif mode == 'Nil' then
        State.ast = parseNil()
    elseif mode == 'Boolean' then
        State.ast = parseBoolean()
    elseif mode == 'String' then
        State.ast = parseString()
    elseif mode == 'Number' then
        State.ast = parseNumber()
    elseif mode == 'Name' then
        State.ast = parseName()
    elseif mode == 'Exp' then
        State.ast = parseExp()
    elseif mode == 'Action' then
        State.ast = parseAction()
    end

    if State.ast then
        State.ast.state = State
    end

    while true do
        if Index <= #Tokens then
            unknownSymbol()
            Index = Index + 2
        else
            break
        end
    end

    return State
end
