464 lines
20 KiB
Lua
464 lines
20 KiB
Lua
--[[
|
|
This file is part of cratera.lua - pure-Lua Cratera-to-Lua transpiler
|
|
Copyright (C) 2019 Soni L.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
--]]
|
|
|
|
-- slow af but works
|
|
|
|
-- need to run this first
|
|
local is51 = (function() local _ENV = {hasenv = true} return not hasenv end)()
|
|
|
|
local parser = require "parser"
|
|
local selfify = parser.selfify
|
|
local STATE = parser.STATE
|
|
local luatokens = require "luatokens"
|
|
local reverse_keywords, reverse_tokens = luatokens.reverse_keywords, luatokens.reverse_tokens
|
|
local TK = luatokens.TK
|
|
local error, assert, ipairs, tostring, type = error, assert, ipairs, tostring, type
|
|
-- try to avoid making too many locals because Lua has a limit to how many locals you can have
|
|
local math = {huge=math.huge, floor=math.floor}
|
|
local string = {format=string.format, byte=string.byte, gsub=string.gsub}
|
|
local table = {insert=table.insert, remove=table.remove}
|
|
|
|
-- marker for use with selfify()
|
|
local SELF = {}
|
|
-- other markers
|
|
local FINISH = {}
|
|
local START_OF_STMT = {}
|
|
local END_OF_STMT = {}
|
|
local END_OF_CRATERA = {}
|
|
|
|
-- implementation of myobj:[mytrait].myfunction(...)
|
|
local CRATERA_FUNCTION = "function(myobj, mytrait, myfunction, ...) return myobj[mytrait][myfunction](myobj, ...) end"
|
|
|
|
local EXTRA_DATA = {[TK.NAME] = true, [TK.INT] = true, [TK.FLT] = true, [TK.STRING] = true, [END_OF_STMT] = true}
|
|
|
|
local function is_tk(results, i, tk)
|
|
-- needed to prevent accidentally processing string literals as tokens
|
|
-- (note: it's generally safe, and faster, to do results[i] == tk,
|
|
-- if tk is one of the values in the TK.* table.)
|
|
return not EXTRA_DATA[results[i-1]] and results[i] == tk
|
|
end
|
|
|
|
local function ignore_newlines(results, i)
|
|
-- skip '\n' and return the new i
|
|
while is_tk(results, i, '\n') do -- ignore newlines
|
|
i = i - 1
|
|
end
|
|
return i
|
|
end
|
|
|
|
-- -- --
|
|
|
|
local defs = selfify({}, SELF)
|
|
|
|
local finish = selfify({}, SELF)
|
|
finish[parser.EOZ] = function(state, token)
|
|
local results = state.results
|
|
local tk = table.remove(results)
|
|
print((tk == START_OF_STMT and "START_OF_STMT") or (tk == END_OF_STMT and "END_OF_STMT") or tostring(tk))
|
|
if tk == TK.FLT then
|
|
local token = table.remove(results)
|
|
local extra, num, den = 1, token, 1
|
|
assert(token == token and token >= 0, "NYI") -- the tokenizer should never output NaNs or negative values
|
|
if token == math.huge then -- the tokenizer *can* output math.huge tho
|
|
num, den = 1, 0
|
|
else
|
|
while num ~= math.floor(num) do
|
|
num = num * 2 -- always safe (I think)
|
|
local oldden = den
|
|
den = den * 2
|
|
if den == math.huge then -- subnormals or something?
|
|
extra = oldden
|
|
den = 2
|
|
end
|
|
end
|
|
end
|
|
table.insert(state, string.format('((%d/%d)/%d)', num, den, extra))
|
|
elseif tk == TK.INT then
|
|
local v = table.remove(results)
|
|
if v == math.mininteger then
|
|
-- corner case ( https://github.com/lua/lua/commit/707b0ba6e2dbfd58cf1167dae0e17975904b18aa )
|
|
table.insert(state, string.format('0x%x', v))
|
|
else
|
|
table.insert(state, string.format('(%d)', v)) -- may be negative (overflow)
|
|
end
|
|
elseif tk == TK.STRING then
|
|
-- lua tends to use a backslash and a newline but we already do newline processing,
|
|
-- so we need to replace the escaped newline ("\\\n") with a newline escape ("\\n").
|
|
-- additionally lua 5.1 doesn't handle control characters other than '\0' and '\r' so we need to escape them as well
|
|
local fmted = string.format('%q', table.remove(results))
|
|
fmted = string.gsub(fmted, '\n', 'n')
|
|
if is51 then
|
|
fmted = string.gsub(fmted, "%c", function(c) return string.format("\\%03d", string.byte(c)) end)
|
|
end
|
|
table.insert(state, fmted)
|
|
elseif tk == TK.NAME then
|
|
table.insert(state, table.remove(results))
|
|
elseif type(tk) == "string" then
|
|
table.insert(state, tk)
|
|
elseif tk then
|
|
-- START_OF_STMT and END_OF_STMT are neither keywords nor tokens; this should error in that case.
|
|
table.insert(state, assert(reverse_keywords[tk] or reverse_tokens[tk]))
|
|
else
|
|
return {}
|
|
end
|
|
return SELF
|
|
end
|
|
defs[FINISH] = finish
|
|
|
|
defs[parser.EOZ] = function(state, token)
|
|
local results = state.results
|
|
-- flip results around
|
|
local len = results.n
|
|
for i=1, len do
|
|
local j = len-i+1
|
|
if i >= j then
|
|
break
|
|
end
|
|
results[i], results[j] = results[j], results[i]
|
|
end
|
|
return FINISH
|
|
end
|
|
defs[parser.FALLBACK] = function(state, token)
|
|
local results = state.results or (function() state.results = {} return state.results end)()
|
|
|
|
do -- handle newlines. this allows error messages to correctly map between lua and cratera
|
|
local oldline = state.oldline or 1
|
|
local linenumber = state.linenumber or 1
|
|
if linenumber > oldline then
|
|
local count = linenumber-oldline
|
|
local len = (results.n or 0)
|
|
for i=1, count do
|
|
results[len+i] = '\n'
|
|
end
|
|
results.n = len + count
|
|
if EXTRA_DATA[results[len]] then -- we're in the middle of a token with extra data. fix it up.
|
|
results[len], results[results.n] = results[results.n], results[len]
|
|
end
|
|
end
|
|
state.oldline = state.linenumber
|
|
end
|
|
|
|
results.n = (results.n or 0) + 1
|
|
results[results.n] = token
|
|
if not results.skip then -- don't process string literals as tokens
|
|
if token == ':' then
|
|
-- figure out whether we're in funcname
|
|
local i = results.n - 1 -- skip the ':'
|
|
local find_statement = true
|
|
i = ignore_newlines(results, i)
|
|
while results[i-1] == TK.NAME do
|
|
i = ignore_newlines(results, i-2) + 2
|
|
if is_tk(results, i-2, '.') then
|
|
-- keep going
|
|
i = i - 3
|
|
elseif results[i-2] == TK.FUNCTION then -- we're in funcname
|
|
find_statement = false -- don't even bother with : handling
|
|
break
|
|
else
|
|
-- found start of statement
|
|
find_statement = false
|
|
-- mark start
|
|
i = i - 1
|
|
table.insert(results, i, START_OF_STMT)
|
|
results.n = results.n + 1
|
|
-- no need to fix existing END_OF_STMT because this code
|
|
-- only detects patterns of the form Name {'.' Name} ':',
|
|
-- which do not contain subexpressions.
|
|
-- mark end
|
|
table.insert(results, results.n + 1, END_OF_STMT)
|
|
table.insert(results, results.n + 2, i)
|
|
results.n = results.n + 2
|
|
break
|
|
end
|
|
end
|
|
if find_statement then
|
|
while true do
|
|
i = ignore_newlines(results, i)
|
|
if is_tk(results, i, ')') then
|
|
-- (prefixexp) or (funcargs)
|
|
-- find matching '('
|
|
local depth = 1
|
|
repeat
|
|
i = i - 1
|
|
if is_tk(results, i, '(') then
|
|
depth = depth - 1
|
|
elseif is_tk(results, i, ')') then
|
|
depth = depth + 1
|
|
elseif not results[i] then
|
|
error("syntax error (unbalanced '()')")
|
|
end
|
|
until depth == 0
|
|
elseif is_tk(results, i, ']') then
|
|
-- [indexing]
|
|
-- find matching '['
|
|
local depth = 1
|
|
repeat
|
|
i = i - 1
|
|
if is_tk(results, i, '[') then
|
|
depth = depth - 1
|
|
elseif is_tk(results, i, ']') then
|
|
depth = depth + 1
|
|
elseif not results[i] then
|
|
error("syntax error (unbalanced '[]')")
|
|
end
|
|
until depth == 0
|
|
elseif results[i-1] == TK.NAME then
|
|
-- Name or '.' Name
|
|
i = i - 2
|
|
i = ignore_newlines(results, i)
|
|
if is_tk(results, i, '.') then
|
|
-- skip '.'
|
|
i = i - 1
|
|
else
|
|
-- found start of statement
|
|
break
|
|
end
|
|
elseif is_tk(results, i, '}') then
|
|
-- prefixexp '{' table '}'
|
|
local newi = i
|
|
local depth = 1
|
|
repeat
|
|
newi = newi - 1
|
|
if is_tk(results, newi, '[') then
|
|
depth = depth - 1
|
|
elseif is_tk(results, newi, ']') then
|
|
depth = depth + 1
|
|
elseif not results[i] then
|
|
error("syntax error (unbalanced '{}')")
|
|
end
|
|
until depth == 0
|
|
local checki = ignore_newlines(results, newi-1)
|
|
-- do I need these checks?
|
|
if is_tk(results, checki, ']') or
|
|
is_tk(results, checki, '}') or
|
|
is_tk(results, checki, ')') or
|
|
results[checki-1] == TK.NAME or
|
|
results[checki-1] == TK.STRING then
|
|
i = newi
|
|
else
|
|
-- syntax error?
|
|
error("syntax error")
|
|
end
|
|
elseif results[i-1] == TK.STRING then
|
|
-- prefixexp "string"
|
|
-- prefixexp 'string'
|
|
-- prefixexp [[string]]
|
|
local newi = i-1
|
|
local checki = ignore_newlines(results, newi-1)
|
|
-- do I need these checks?
|
|
if is_tk(results, checki, ']') or
|
|
is_tk(results, checki, '}') or
|
|
is_tk(results, checki, ')') or
|
|
results[checki-1] == TK.NAME or
|
|
results[checki-1] == TK.STRING then
|
|
i = newi
|
|
else
|
|
-- syntax error?
|
|
error("syntax error")
|
|
end
|
|
else
|
|
-- found start of statement
|
|
break
|
|
end
|
|
i = i - 1
|
|
end
|
|
-- mark start
|
|
i = i + 1
|
|
table.insert(results, i, START_OF_STMT)
|
|
results.n = results.n + 1
|
|
-- fix existing END_OF_STMT
|
|
for k=i, #results do
|
|
if results[k] == END_OF_STMT then
|
|
local v = results[k+1]
|
|
if v > i then -- this should always be true?
|
|
results[k+1] = v + 1
|
|
end
|
|
end
|
|
end
|
|
-- mark end
|
|
table.insert(results, results.n + 1, END_OF_STMT)
|
|
table.insert(results, results.n + 2, i)
|
|
results.n = results.n + 2
|
|
end
|
|
elseif token == '(' or token == '{' or token == TK.STRING then
|
|
local i = results.n - 1 -- skip the '(' / '{' / TK_STRING
|
|
i = ignore_newlines(results, i)
|
|
-- possible patterns:
|
|
-- ':' Name '(' -- plain Lua thing, ignore
|
|
-- ':' Name '.' Name '(' -- cratera string traits
|
|
-- ':' '[' exp ']' '.' Name '(' -- cratera object traits
|
|
-- ':' '[' exp ']' '(' -- supported in lua 5.3 cratera patch but no reason to support it here.
|
|
if results[i-1] == TK.NAME then
|
|
local tk_myfunction = i-1
|
|
-- maybe cratera
|
|
i = ignore_newlines(results, i-2)
|
|
if results[i-1] == END_OF_STMT then
|
|
-- lua, but we need to fix it up
|
|
-- we could just replace them with dummies, but
|
|
local pos = results[i]
|
|
table.remove(results, i) -- remove END_OF_STMT's value
|
|
table.remove(results, i-1) -- remove END_OF_STMT
|
|
table.remove(results, pos) -- remove START_OF_STMT
|
|
results.n = results.n - 3 -- adjust length
|
|
assert(results[i-3] == ':')
|
|
elseif is_tk(results, i, '.') then
|
|
-- maybe cratera
|
|
local tk_dot = i
|
|
local inject_cratera = false
|
|
i = ignore_newlines(results, i-1)
|
|
if results[i-1] == TK.NAME then
|
|
local tk_mytrait = i-1
|
|
i = ignore_newlines(results, i-2)
|
|
if results[i-1] == END_OF_STMT then
|
|
-- definitely cratera (stmt ':' Name '.' Name '(')
|
|
-- convert into '(' stmt ',' String ',' String
|
|
-- convert names into strings
|
|
results[tk_mytrait] = TK.STRING
|
|
inject_cratera = true
|
|
end -- else not cratera
|
|
elseif is_tk(results, i, ']') then
|
|
local tk_right = i
|
|
local depth = 1
|
|
repeat
|
|
i = i - 1
|
|
if is_tk(results, i, '[') then
|
|
depth = depth - 1
|
|
elseif is_tk(results, i, ']') then
|
|
depth = depth + 1
|
|
elseif not results[i] then
|
|
error("syntax error (unbalanced '[]')")
|
|
end
|
|
until depth == 0
|
|
local tk_left = i
|
|
i = ignore_newlines(results, i-1)
|
|
if results[i-1] == END_OF_STMT then
|
|
-- definitely cratera (stmt ':' '[' exp ']' '.' Name '(')
|
|
-- convert into '(' stmt ',' '(' exp ')' ',' String
|
|
-- replace '[' and ']'
|
|
results[tk_right] = ')'
|
|
results[tk_left] = '('
|
|
inject_cratera = true
|
|
end -- else not cratera
|
|
end
|
|
if inject_cratera then
|
|
--assert(token == '(', "unimplemented")
|
|
-- convert name into string
|
|
results[tk_myfunction] = TK.STRING
|
|
-- replace '.' with ','
|
|
results[tk_dot] = ','
|
|
local pos = results[i]
|
|
-- remove END_OF_STMT
|
|
table.remove(results, i-1)
|
|
table.remove(results, i-1)
|
|
results.n = results.n - 2
|
|
-- replace ':' with ','
|
|
results[ignore_newlines(results, i-2)] = ','
|
|
-- replace START_OF_STMT with '('
|
|
results[pos] = '('
|
|
if token == '(' then
|
|
-- replace '(' with ','
|
|
results[results.n] = ','
|
|
else
|
|
-- insert ',' before argument
|
|
table.insert(results, results.n, ',')
|
|
results.n = results.n + 1
|
|
end
|
|
-- inject cratera
|
|
table.insert(results, pos, ')')
|
|
table.insert(results, pos, CRATERA_FUNCTION)
|
|
table.insert(results, pos, '(')
|
|
-- check for potential prefixexp and correct for it
|
|
if is_tk(results, pos-1, ']') or
|
|
is_tk(results, pos-1, '}') or
|
|
is_tk(results, pos-1, ')') or
|
|
results[pos-2] == TK.NAME or
|
|
results[pos-2] == TK.STRING then
|
|
table.insert(results, pos, ';')
|
|
results.n = results.n + 1
|
|
end
|
|
results.n = results.n + 3
|
|
-- tag it so we know to insert a ')' to close our '('
|
|
-- and to handle '(' ')' (no argument) calls
|
|
-- we add the tag before TK.STRING/'{'/','
|
|
table.insert(results, results.n, END_OF_CRATERA)
|
|
results.n = results.n + 1
|
|
end
|
|
end -- else not cratera
|
|
end
|
|
elseif token == '}' then
|
|
local i = results.n -- we'll be subtracting anyway, see below
|
|
local depth = 1
|
|
repeat
|
|
i = i - 1
|
|
if is_tk(results, i, '{') then
|
|
depth = depth - 1
|
|
elseif is_tk(results, i, '}') then
|
|
depth = depth + 1
|
|
elseif not results[i] then
|
|
error("syntax error (unbalanced '{}')")
|
|
end
|
|
until depth == 0
|
|
assert(is_tk(results, i, '{'))
|
|
if results[i-1] == END_OF_CRATERA then
|
|
-- need to add ')' to close our '('
|
|
table.remove(results, i-1)
|
|
results[results.n] = ')'
|
|
end
|
|
elseif token == ')' then
|
|
local i = results.n - 1 -- skip the ')'
|
|
i = ignore_newlines(results, i)
|
|
if results[i] == ',' and results[i-1] == END_OF_CRATERA then
|
|
-- '(' CRATERA_FUNCTION ')' '(' something END_OF_CRATERA ',' ')'
|
|
-- need to fix it up into
|
|
-- '(' CRATERA_FUNCTION ')' '(' something ')'
|
|
table.remove(results, i-1)
|
|
table.remove(results, i-1)
|
|
results.n = results.n - 2
|
|
else
|
|
-- still might need to remove an END_OF_CRATERA somewhere
|
|
i = i + 1
|
|
local depth = 1
|
|
repeat
|
|
i = i - 1
|
|
if is_tk(results, i, '(') then
|
|
depth = depth - 1
|
|
elseif is_tk(results, i, ')') then
|
|
depth = depth + 1
|
|
elseif results[i] == END_OF_CRATERA then
|
|
table.remove(results, i)
|
|
results.n = results.n - 1
|
|
break
|
|
elseif not results[i] then
|
|
error("syntax error (unbalanced '()')")
|
|
end
|
|
until depth == 0
|
|
end
|
|
end
|
|
else -- we skipped a string literal
|
|
if results[results.n-1] == TK.STRING and results[results.n-2] == END_OF_CRATERA then
|
|
-- need to add ')' to close our '('
|
|
table.remove(results, results.n-2)
|
|
results[results.n] = ')'
|
|
end
|
|
end
|
|
results.skip = EXTRA_DATA[token]
|
|
return SELF
|
|
end
|
|
|
|
return {defs = defs}
|