summary refs log blame commit diff stats
path: root/dirtycompiler.lua
blob: 3ece69449aaecbd62efe25938ca9892322eae399 (plain) (tree)

































































































































































                                                                                                                            

                                            

























                                                                               










                                                              

                                                                       











                                                              

                                                                       






















                                                                 

                                                                       




















































































                                                                                                          














                                                                              

                                                                       




                                                           
                                                                                     







                                                                               
                                                               










                                                                    









                                                                 













                                                                           



                                                                             
                                                 



                                       

















                                                                         


                                                   

                                                                                



















                                                                         
                                                               



                                





                                                                                            





                                    
--[[
    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}