Untitled

 avatar
unknown
lua
11 days ago
11 kB
6
Indexable
function checkSyntax(code, filename)
    local errors = {}
    filename = filename or "<string>"
    local lines = {}
    local line_ends = {}

    -- --- Phần tiền xử lý ---
    if type(code) ~= "string" then
        table.insert(errors, string.format("%s:0:0: Input code must be a string, got %s", filename, type(code)))
        return errors
    end
    do
        local pos = 1
        for line_content, newline_sequence in code:gmatch("([^\r\n]*)(\r?\n?)") do
            table.insert(lines, line_content)
            local line_len = #line_content; local nl_len = #newline_sequence
            pos = pos + line_len + nl_len; table.insert(line_ends, pos - 1)
        end
        local code_len = #code
        if (#line_ends == 0 and code_len > 0) or (#line_ends > 0 and line_ends[#line_ends] < code_len) then
            local last_line_start = (#line_ends > 0 and line_ends[#line_ends] + 1) or 1
            if last_line_start <= code_len then table.insert(lines, code:sub(last_line_start)); table.insert(line_ends, code_len) end
        elseif #lines == 0 and code_len == 0 then
            table.insert(lines, ""); table.insert(line_ends, 0)
        end
    end
    -- --- Kết thúc tiền xử lý ---

    -- --- Helper Functions ---
    local function getLineNum(char_pos)
        local low = 1; local high = #line_ends; local line_num = #line_ends
        while low <= high do
            local mid = math.floor((low + high) / 2); local start_of_line = (mid == 1 and 0) or line_ends[mid - 1]
            if start_of_line < 0 then start_of_line = 0 end -- Ensure start_of_line is not negative
            if char_pos > start_of_line and char_pos <= line_ends[mid] then line_num = mid; break
            elseif char_pos <= start_of_line then high = mid - 1
            else low = mid + 1 end
        end
        -- Handle edge case where char_pos is before the first recorded end
        if #line_ends > 0 and char_pos <= (line_ends[1] or 0) then line_num = 1 end
        if #lines == 0 then return 1 end
        return math.max(1, math.min(line_num, #lines))
    end

    local function getLineStartPos(line_num)
        if line_num <= 1 then return 1 end
        line_num = math.max(1, math.min(line_num, #line_ends + 1))
        -- Check if the previous line index exists
        if line_num > 1 and (line_num - 1) <= #line_ends then
             -- Ensure the value retrieved is a number before adding 1
             local prev_end_pos = line_ends[line_num - 1]
             if type(prev_end_pos) == "number" then
                 return prev_end_pos + 1
             else
                 -- Fallback if line_ends is somehow corrupted
                 return 1
             end
        else
             -- Fallback if line_num is out of expected range (e.g., line_num = #line_ends + 1)
             if #line_ends > 0 and type(line_ends[#line_ends]) == "number" then
                 return line_ends[#line_ends] + 1
             else
                 return 1 -- Safest fallback for empty or corrupted line_ends
             end
        end
    end

    local function addError(char_pos, msg)
        local line_num = getLineNum(char_pos)
        local line_start = getLineStartPos(line_num)
         -- Ensure line_start is valid before calculating col_num
        if line_start > char_pos then line_start = 1 end -- Safety check
        local col_num = char_pos - line_start + 1
        col_num = math.max(1, col_num)
        table.insert(errors, string.format("%s:%d:%d: %s", filename, line_num, col_num, msg))
    end

    local function addErrorAtLineEnd(line_num, col, msg)
         line_num = math.max(1, math.min(line_num, #lines))
         col = math.max(1, col)
         table.insert(errors, string.format("%s:%d:%d: %s", filename, line_num, col, msg))
     end
    -- --- End Helper Functions ---

    -- --- Parser State and Definitions ---
    local stack = {}; local i = 1; local len = #code
    local openPairs = { ["("] = ")", ["["] = "]", ["{"] = "}" }; local pairClosers = { [")"] = "(", ["]"] = "[", ["}"] = "{" }
    local blockOpeners = { ["function"]=true, ["if"]=true, ["for"]=true, ["while"]=true, ["repeat"]=true } -- no 'do'
    local blockEnders = { ["end"]=true, ["until"]=true }
    local blockModifiers = { ["else"]=true, ["elseif"]=true }
    local endCanClose = {["function"]=true, ["if"]=true, ["for"]=true, ["while"]=true} -- no 'do'
    local untilCanClose = {["repeat"]=true}
    -- --- End Definitions ---

    -- --- Main Parsing Loop ---
    while i <= len do
        local initial_i = i
        local handled = false

        local c1 = code:sub(i, i)
        local c2 = code:sub(i, i + 1)

        -- 1. Comments
        if c2 == '--' then
            handled = true; local start_comment_pos = i; local current_i = i + 2; local c3 = code:sub(current_i, current_i); local c4 = code:sub(current_i + 1, current_i + 1)
            if c3 == '[' and (c4 == '[' or c4 == '=') then local level = 0; local start_bracket = current_i; while code:sub(start_bracket + 1 + level, start_bracket + 1 + level) == '=' do level = level + 1 end
                if code:sub(start_bracket + level + 1, start_bracket + level + 1) == '[' then
                    local closer_str = ']' .. ('='):rep(level) .. ']'; local search_start = start_bracket + level + 2; local end_comment = code:find(closer_str, search_start, true)
                    if end_comment then local closer_len = #closer_str; i = end_comment + closer_len
                    else addError(start_comment_pos, "unfinished long comment"); i = len + 1 end
                else i = start_comment_pos + 2; while i <= len do local cc = code:sub(i,i); if cc == '\n' or cc == '\r' then i = i + 1; break end; i = i + 1 end end
            else i = current_i; while i <= len do local cc = code:sub(i,i); if cc == '\n' or cc == '\r' then i = i + 1; break end; i = i + 1 end end
        -- 2. Strings
        elseif c1 == '"' or c1 == "'" then handled = true; local quote = c1; local start_string_pos = i; i = i + 1; local closed = false; while i <= len do local nc = code:sub(i, i); if nc == '\\' then i = i + 2 elseif nc == quote then i = i + 1; closed = true; break elseif nc == '\n' or nc == '\r' then addError(start_string_pos, "malformed string (newline encountered)"); closed = true; break else i = i + 1 end end; if not closed then addError(start_string_pos, "unfinished string") end
        -- 3. Long Strings
        elseif c2 == '[[' or (c1 == '[' and code:sub(i + 1, i + 1) == '=') then
            local start_string_pos = i; local level = 0; local start_bracket = i; while code:sub(start_bracket + 1 + level, start_bracket + 1 + level) == '=' do level = level + 1 end
            if code:sub(start_bracket + level + 1, start_bracket + level + 1) == '[' then
                 handled = true; local closer_str = ']' .. ('='):rep(level) .. ']'; local search_start = start_bracket + level + 2; local end_string = code:find(closer_str, search_start, true)
                 if end_string then local closer_len = #closer_str; i = end_string + closer_len else addError(start_string_pos, "unfinished long string"); i = len + 1 end
            end
        end

        -- 4. Syntax Characters and Keywords
        if not handled then
            if openPairs[c1] then local line, col = getLineNum(i), i - getLineStartPos(getLineNum(i)) + 1; table.insert(stack, { type = c1, line = line, col = col, pos = i }); i = i + 1
            elseif pairClosers[c1] then local line, col = getLineNum(i), i - getLineStartPos(getLineNum(i)) + 1; local last = stack[#stack]; if last and last.type == pairClosers[c1] then table.remove(stack) else local expected = (last and openPairs[last.type]) and string.format(" (expected '%s' to close '%s' at %d:%d)", openPairs[last.type], last.type, last.line, last.col) or ""; addError(i, string.format("unexpected symbol '%s'%s", c1, expected)) end; i = i + 1
            elseif c1:match("[%a_]") then
                local word_start_pos = i; local word_end_pos = word_start_pos; while word_end_pos <= len and code:sub(word_end_pos, word_end_pos):match("[%w_]") do word_end_pos = word_end_pos + 1 end; local word = code:sub(word_start_pos, word_end_pos - 1); local word_line, word_col = getLineNum(word_start_pos), word_start_pos - getLineStartPos(getLineNum(word_start_pos)) + 1

                if blockOpeners[word] then table.insert(stack, { type = word, line = word_line, col = word_col, pos = word_start_pos })
                elseif blockEnders[word] then
                     local last = stack[#stack];
                     if word == "end" then if last and endCanClose[last.type] then table.remove(stack) else if not last then addError(word_start_pos, string.format("'%s' unexpected (no open block)", word)) else addError(word_start_pos, string.format("'%s' unexpected (cannot close block type '%s' opened at %d:%d)", word, last.type, last.line, last.col)) end end
                     elseif word == "until" then if last and untilCanClose[last.type] then table.remove(stack) else local expected_type = (last and last.type) or "nothing"; addError(word_start_pos, string.format("'until' unexpected (expected 'repeat', found '%s' at %d:%d or stack empty)", expected_type, last and last.line or 0, last and last.col or 0)) end end
                elseif blockModifiers[word] then
                     local last = stack[#stack];
                     if not last or last.type ~= "if" then addError(word_start_pos, string.format("'%s' unexpected (not immediately inside an 'if' block)", word)) end
                end
                i = word_end_pos
            elseif c1:match("%s") then i = i + 1
            else i = i + 1 end
        end

        if i == initial_i and i <= len then addError(i, "Internal parser error: loop stuck"); break end
    end
    -- --- End Main Parsing Loop ---

    -- --- Final Stack Check ---
    for idx, item in ipairs(stack) do
        local expected = "?"; if openPairs[item.type] then expected = "'"..openPairs[item.type].."'" elseif endCanClose[item.type] then expected = "'end'" elseif untilCanClose[item.type] then expected = "'until'" end
        local err_line = #lines; local err_col = 1; if err_line > 0 and #lines[err_line] > 0 then err_col = #lines[err_line] + 1 end
        if openPairs[item.type] then addError(item.pos, string.format("%s expected (to close '%s' starting at %d:%d)", expected, item.type, item.line, item.col))
        else addErrorAtLineEnd(err_line, err_col, string.format("%s expected (to close '%s' starting at %d:%d)", expected, item.type, item.line, item.col)) end
    end
    -- --- End Final Stack Check ---

    return errors
end
Editor is loading...
Leave a Comment