Untitled
unknown
lua
8 months ago
11 kB
12
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
endEditor is loading...
Leave a Comment