-- Brainfuck interpreter in Lua using a [] cache.
-- This is public domain.
local src, err = io.open(arg[1], "r")
if src == nil then
    print("Error opening file " .. arg[1] .. ": " .. err)
    os.exit(1)
end
local code = src:read("*a")
src:close()
src = nil

local ip = 1
local dp = 1
local mem = {0}
local codelen = string.len(code)
local jumpcache = {}

local commands =
{
    add = string.byte("+"),
    sub = string.byte("-"),
    next = string.byte(">"),
    prev = string.byte("<"),
    startloop = string.byte("["),
    endloop = string.byte("]"),
    input = string.byte(","),
    output = string.byte(".")
}

while ip <= codelen do
    local cmd = string.byte(code, ip)
    if cmd == commands.add then
        mem[dp] = mem[dp] + 1
        if mem[dp] == 256 then mem[dp] = 0 end
    elseif cmd == commands.sub then
        mem[dp] = mem[dp] - 1
        if mem[dp] == -1 then mem[dp] = 255 end
    elseif cmd == commands.next then
        dp = dp + 1
        if mem[dp] == nil then mem[dp] = 0 end
    elseif cmd == commands.prev then
        dp = dp - 1
        if dp == 0 then
            print("Underflow error at " .. ip)
            os.exit(1)
        end
    elseif cmd == commands.input then
        local entry = io.stdin:read(1)
        if entry == nil then
            mem[dp] = 0 -- end of file
        else
            entry = string.byte(entry)
            if entry > 255 then entry = 255
            elseif entry < 0 then entry = 0 end
            mem[dp] = entry
        end
    elseif cmd == commands.output then
        io.stdout:write(string.char(mem[dp]))
    elseif cmd == commands.startloop and mem[dp] == 0 then
        if jumpcache[ip] ~= nil then ip = jumpcache[ip]
        else
            local descent, oldip = 1, ip
            repeat
                ip = ip + 1
                if ip > codelen then
                    print("Unmatched [")
                    os.exit(1)
                end
                cmd = string.byte(code, ip)
                if cmd == commands.startloop then descent = descent + 1
                elseif cmd == commands.endloop then descent = descent - 1 end
            until descent == 0
            jumpcache[oldip] = ip
        end
    elseif cmd == commands.endloop and mem[dp] ~= 0 then
        if jumpcache[ip] ~= nil then ip = jumpcache[ip]
        else
            local descent, oldip = 1, ip
            repeat
                ip = ip - 1
                if ip == 0 then
                    print("Unmatched ]")
                    os.exit(1)
                end
                cmd = string.byte(code, ip)
                if cmd == commands.startloop then descent = descent - 1
                elseif cmd == commands.endloop then descent = descent + 1 end
            until descent == 0
            jumpcache[oldip] = ip
        end
    end
    ip = ip + 1
end
