-- LunaCON CON to Lunatic translator
-- requires LPeg, http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html

local require = require
local lpeg = require("lpeg")

local bit
local math = require("math")
local string = require("string")
local table = require("table")


local arg = arg

local assert = assert
local error = error
local ipairs = ipairs
local loadstring = loadstring
local pairs = pairs
local pcall = pcall
local print = print
local setmetatable = setmetatable
local tonumber = tonumber
local tostring = tostring
local type = type
local unpack = unpack

-- non-nil if running from EDuke32
-- (read_into_string~=nil  iff  string.dump==nil)
local read_into_string = read_into_string
local ffi, ffiC

if (string.dump) then  -- running stand-alone
    local ljp = pcall(function() require("ffi") end)
    -- "lbit" is the same module as LuaJIT's "bit" (LuaBitOp:
    -- http://bitop.luajit.org/), but under a different name for (IMO) less
    -- confusion. Useful for running with Rio Lua for cross-checking.
    bit = ljp and require("bit") or require("lbit")
    require("strict")
else
    bit = require("bit")
    ffi = require("ffi")
    ffiC = ffi.C
end



module("lunacon")


-- I think that the "too many pending calls/choices" is unavoidable in general.
-- This limit is of course still arbitrary, but writing long if/else cascades
-- in CON isn't pretty either (though sometimes necessary because nested switches
-- don't work?)
-- See also:  http://lua-users.org/lists/lua-l/2010-03/msg00086.html
lpeg.setmaxstack(1024);


local Pat, Set, Range, Var = lpeg.P, lpeg.S, lpeg.R, lpeg.V
local POS, Cc, Ctab = lpeg.Cp, lpeg.Cc, lpeg.Ct

-- CON language definitions (among other things, all keywords pattern).
local conl = require("con_lang")


local function match_until(matchsp, untilsp)  -- (!untilsp matchsp)* in PEG
    -- sp: string or pattern
    return (matchsp - Pat(untilsp))^0
end

local format = string.format
--[[
format = function(fmt, ...)
    local ok, res = pcall(string.format, fmt, ...)
    if (not ok) then
        error(string.format("FAILED format(%q, ...) | message: %s", fmt, res))
    end
    return res
end
--]]

local function printf(fmt, ...)
    print(format(fmt, ...))
end

--- some constants

local C = {
    -- These two are not used except for predefined labels.
    -- NOTE: in-game, MAXSPRITES may be 4096 for a V7 build!
    MAXSTATUS = ffiC and ffiC.MAXSTATUS or 1024,
    MAXSPRITES = ffiC and ffiC.MAXSPRITES or 16384,

    MAXTILES = ffiC and ffiC.MAXTILES or 30720,
    MAX_WEAPONS = ffiC and ffiC.MAX_WEAPONS or 12,
}

---=== semantic action functions ===---

local inf = 1/0
local NaN = 0/0

-- Last keyword position, for error diagnosis.
local g_lastkwpos = nil
local g_lastkw = nil
local g_badids = {}  -- maps bad id strings to 'true'

local g_recurslevel = -1  -- 0: base CON file, >0 included
local g_filename = "???"
local g_directory = ""  -- with trailing slash if not empty
local g_maxerrors = 20
local g_numerrors = 0

-- Default directory to search for GAME.CON etc.
-- Stand-alone LunaCON only.
local g_defaultDir = nil

-- Warning options. Key names are the same as cmdline options, e.g.
-- -Wno-bad-identifier for disabling the "bad identifier" warning.
local g_warn = { ["not-redefined"]=true, ["bad-identifier"]=false,
                 ["number-conversion"]=true, ["system-gamevar"]=true,
                 ["error-bad-getactorvar"]=false, ["chained-loadactor"]=true, }

-- Code generation and output options.
local g_cgopt = { ["no"]=false, ["debug-lineinfo"]=false, ["gendir"]=nil,
                  ["cache-sap"]=false, ["error-nostate"]=true,
                  ["playervar"]=true, ["trapv"]=false, ["wrapv"]=false, }

local function csapp() return g_cgopt["cache-sap"] end

local function handle_cmdline_arg(str)
    if (str:sub(1,1)=="-") then
        if (#str == 1) then
            printf("Warning: input from stdin not supported")
        else
            local ok = false
            local kind = str:sub(2,2)

            -- -W(no-)*: warnings
            if (kind=="W" and #str >= 3) then
                local val = true
                local warnstr = str:sub(3)

                if (warnstr == "all") then
                    -- Enable all warnings.
                    for wopt in pairs(g_warn) do
                        g_warn[wopt] = true
                    end
                    ok = true
                else
                    -- Enable or disable a particular warning.
                    if (warnstr:sub(1,3)=="no-") then
                        val = false
                        warnstr = warnstr:sub(4)
                    end

                    if (type(g_warn[warnstr])=="boolean") then
                        g_warn[warnstr] = val
                        ok = true
                    end
                end

            -- -fno* special handling
            elseif (str:sub(2)=="fno") then
                -- Disable printing code entirely.
                g_cgopt["no"] = true
                ok = true
            elseif (str:sub(2)=="fno=onlycheck") then
                -- Disable printing code, only do syntax check of gen'd code.
                g_cgopt["no"] = "onlycheck"
                ok = true

            -- -fgendir=<directory>: specify directory for generated code
            elseif (str:sub(2,9)=="fgendir=" and #str >= 10) then
                g_cgopt["gendir"] = str:sub(10)
                ok = true

            -- -f(no-)*: code generation options
            elseif (kind=="f" and #str >= 3) then
                local val = true
                local cgstr = str:sub(3)

                if (cgstr:sub(1,3)=="no-") then
                    val = false
                    cgstr = cgstr:sub(4)
                end

                if (type(g_cgopt[cgstr])=="boolean") then
                    g_cgopt[cgstr] = val
                    ok = true
                end

            -- -I<directory>: default search directory (only ONCE, not search path)
            elseif (kind=="I" and #str >= 3) then
                g_defaultDir = str:sub(3)
                ok = true
            end

            if (not ffi and not ok) then
                printf("Warning: Unrecognized option %s", str)
            end
        end

        return true
    end
end

-- Handle command line arguments. Has to happen before pattern construction,
-- because some of them depend on codegen options (specifically, -ftrapv,
-- -fwrapv).
if (string.dump) then
    -- running stand-alone
    local i = 1
    while (arg[i]) do
        if (handle_cmdline_arg(arg[i])) then
            table.remove(arg, i)  -- remove processed cmdline arg
        else
            i = i+1
        end
    end
else
    -- running from EDuke32
    local i=0
    while (ffiC.g_argv[i] ~= nil) do
        handle_cmdline_arg(ffi.string(ffiC.g_argv[i]))
        i = i+1
    end
end

-- Stack with *true* on top if the innermost block is a "whilevar*n".
local g_isWhile = {}
-- Sequence number of 'while' statements, used to implement CON "break" inside
-- whilevar*n, which really behaves like what sane languages call "continue"...
local g_whilenum = 0

---=== Code generation ===---
local GVFLAG = {
    PERPLAYER=1, PERACTOR=2, PERX_MASK=3,
    SYSTEM   = 0x00000800,
    READONLY = 0x00001000,

    NODEFAULT = 0x00000400,  -- don't reset on actor spawn
    NORESET   = 0x00020000,  -- don't reset when restoring map state

    CON_PERPLAYER = 0x40000000,  -- LunaCON internal
}

-- NOTE: This differs from enum GamevarFlags_t's GAMEVAR_USER_MASK
GVFLAG.USER_MASK = GVFLAG.PERX_MASK + GVFLAG.NODEFAULT + GVFLAG.NORESET

-- CON --> mangled Lua function name, also existence check:
local g_funcname = {}
-- while parsing a block, it is a table of "gencode" tables:
local g_switchCode = nil
-- Global number of switch statements:
local g_switchCount = 0
-- Number of session gamevars:
local g_numSessionVars = 0
-- [identifier] = { name=<mangled name / code>, flags=<gamevar flags> }
local g_gamevar = {}
-- [identifier] = { name=<mangled name / code>, size=<initial size> }
local g_gamearray = {}

-- * nil if dynamic tile remapping disabled
-- * {} if enabled but no remappings made
-- * else, a nonempty table { [name]=<g_dynTileList index> }
local g_dyntilei = nil
-- Analogously for sounds.
local g_dynsoundi = nil

local g_have_file = {}  -- [filename]=true
local g_curcode = nil  -- a table of string pieces or other "gencode" tables

-- will be a table, see reset.codegen()
local g_code = nil


local function ACS(s) return (csapp() and "_a" or "actor[_aci]")..s end
local function SPS(s) return (csapp() and "_spr" or "sprite[_aci]")..s end
local function PLS(s) return (csapp() and "_ps" or "player[_pli]")..s end
local function PLSX(s) return "player[_pli]"..s end


local function getlinecol(pos) end -- fwd-decl

local function new_initial_codetab()
    -- NOTE: Keep this one line per line to not confuse the Lua->CON line
    -- mapping system.
    return {
        -- Requires.
        "local require=require",
        "local _con, _bit, _math = require'con', require'bit', require'math'",
        "local _xmath = require'xmath'",

        -- Cache globals into locals.
        "local sector, sprite, wall, spriteext, _atsprite = sector, sprite, wall, spriteext, _atsprite",
        "local actor, player, projectile, g_tile = actor, player, projectile, g_tile",
        "local gameactor, gameevent, _gv = gameactor, gameevent, gv",
        "local updatesector, updatesectorz, cansee = updatesector, updatesectorz, cansee",
        "local print, printf = print, printf",

        -- Cache a couple of often-used functions.
        "local _div, _mod, _mulTR, _mulWR = _con._div, _con._mod, _con._mulTR, _con._mulWR",
        "local _band, _bor, _bxor = _bit.band, _bit.bor, _bit.bxor",
        "local _lsh, _rsh, _arsh = _bit.lshift, _bit.rshift, _bit.arshift",
        "local _setsprite,_ssp = _con._setsprite,_con._ssp",

        -- * CON "states" (subroutines) and
        -- * Switch function table, indexed by global switch sequence number:
        "local _F,_SW = {},{}",

        -- CON gamevars and gamearrays (see mangle_name()), set up for
        -- restoration from savegames.
        "module(...)",
        "_V,_A={},{}",
        "-- NOTE to the reader: This require's result is Lunatic-private API! DO NOT USE!",
        "local _dummy,_S=require'end_gamevars'",
-- XXX: Currently commented out because of gamevar restoration from loadmapstate.
--        "local _V,_A=_V,_A",
        "local _C,_M,_I={},{},{}",  -- actions, moves, ais

        -- Static ivec3s so that no allocations need to be made.
        "local _IVEC = { _xmath.ivec3(), _xmath.ivec3() }",
        "local function _IV(num, x, y, z)",
        "  local v=_IVEC[num]; v.x=x; v.y=y; v.z=z; return v;",
        "end",
           }
end

-- CON global system gamevar
local function CSV(var) return "_gv._csv"..var end

-- Creates the table of predefined game variables.
-- KEEPINSYNC gamevars.c: Gv_AddSystemVars()
local function new_initial_gvartab()
    local wmembers = conl.wdata_members

    local function GamevarCreationFunc(addflags)
        return function(varname)
            return { name=varname, flags=GVFLAG.SYSTEM+addflags }
        end
    end

    local RW = GamevarCreationFunc(0)
    local RO = GamevarCreationFunc(GVFLAG.READONLY)
    local PRW = GamevarCreationFunc(GVFLAG.PERPLAYER)
    local PRO = GamevarCreationFunc(GVFLAG.READONLY+GVFLAG.PERPLAYER)

    local gamevar = {
        -- NOTE: THISACTOR can mean different things in some contexts.
        THISACTOR = RO "_aci",

        RETURN = RW "_gv.RETURN",
        HITAG = RW(CSV".HITAG"),
        LOTAG = RW(CSV".LOTAG"),
        TEXTURE = RW(CSV".TEXTURE"),

        -- This will warn when defining from CON, but it's the most
        -- straightforward implementation.
        LOGO_FLAGS = RW "_gv.g_logoFlags",

        xdim = RO "_gv.xdim",
        ydim = RO "_gv.ydim",
        windowx1 = RO "_gv.windowx1",
        windowy1 = RO "_gv.windowy1",
        windowx2 = RO "_gv.windowx2",
        windowy2 = RO "_gv.windowy2",

        yxaspect = RO "_gv._get_yxaspect()",
        viewingrange = RO "_gv._get_viewingrange()",
        -- TODO: gravitationalconstant, gametype_flags

        numsectors = RO "_gv.numsectors",
        NUMSECTORS = RO "_gv.numsectors",
        NUMWALLS = RO "_gv.numwalls",
        -- TODO: Numsprites

        randomseed = RW "_gv.randomseed",
        totalclock = RO "_gv.totalclock",
        framerate = RO "_gv._currentFramerate()",
        current_menu = RO "_gv._currentMenu()",
        rendmode = RO "_gv.rendmode",

        screenpeek = RO "_gv.screenpeek",

        camerax = RW "_gv.cam.pos.x",
        cameray = RW "_gv.cam.pos.y",
        cameraz = RW "_gv.cam.pos.z",
        cameraang = RW "_gv.cam.ang",
        camerahoriz = RW "_gv.cam.horiz",
        camerasect = RW "_gv.cam.sect",
        cameradist = RW "_gv.cam.dist",
        cameraclock = RW "_gv.cam.clock",

        -- HUD weapon gamevars
        currentweapon = RW "_gv.hudweap.cur",
        weaponcount = RW "_gv.hudweap.count",
        weapon_xoffset = RW "_gv.hudweap.gunposx",
        looking_angSR1 = RW "_gv.hudweap.lookhalfang",
        gun_pos = RW "_gv.hudweap.gunposy",
        looking_arc = RW "_gv.hudweap.lookhoriz",
        gs = RW "_gv.hudweap.shade",

        -- Some per-player gamevars
        ZRANGE = PRW(PLSX".zrange"),
        ANGRANGE = PRW(PLSX".angrange"),
        AUTOAIMANGLE = PRW(PLSX".autoaimang"),

        PIPEBOMB_CONTROL = PRW(PLSX".pipebombControl"),
        GRENADE_LIFETIME = PRW(PLSX".pipebombLifetime"),
        GRENADE_LIFETIME_VAR = PRW(PLSX".pipebombLifetimeVar"),
        TRIPBOMB_CONTROL = PRW(PLSX".tripbombControl"),
        STICKYBOMB_LIFETIME = PRW(PLSX".tripbombLifetime"),
        STICKYBOMB_LIFETIME_VAR = PRW(PLSX".tripbombLifetimeVar"),

        -- Some *writable* system gamevars relating to multiplayer.
        -- TODO_MP.
        RESPAWN_MONSTERS = RO "0",
        RESPAWN_ITEMS = RO "0",
        RESPAWN_INVENTORY = RO "0",
        MONSTERS_OFF = RO "0",
        MARKER = RO "0",

        -- These are not 100% authentic (they're only updated in certain
        -- circumstances, see player.c: P_SetWeaponGamevars()). But IMO it's
        -- more useful like this.
        WEAPON = PRO(PLSX".curr_weapon"),
        WORKSLIKE = PRO(format(PLSX".weapon[%s].workslike", PLSX".curr_weapon")),

        VOLUME = RO "_gv.currentEpisode()",
        LEVEL = RO "_gv.currentLevel()",
    }

    -- Reserved bits
    gamevar.LOGO_FLAGS.rbits = bit.bnot(65535)

    for w=0,C.MAX_WEAPONS-1 do
        for i=1,#wmembers do
            local member = wmembers[i]:gsub(".*_t ","")  -- strip e.g. "const int32_t "
                                      :gsub("^_","")  -- strip potentially leading underscore
            local name = format("WEAPON%d_%s", w, member:upper())
            gamevar[name] = PRW(format(PLSX".weapon[%d].%s", w, member))

            if (member=="flags") then
                gamevar[name].rbits = bit.bnot(0x1ffff)
            end
        end
    end

    return gamevar
end

local reset = {}

function reset.codegen()
    g_funcname = {}
    g_switchCode = nil
    g_switchCount = 0
    g_numSessionVars = 0
    g_gamevar = new_initial_gvartab()
    g_gamearray = {
        -- SYSTEM_GAMEARRAY
        tilesizx = { name="g_tile.sizx", size=C.MAXTILES, sysp=true },
        tilesizy = { name="g_tile.sizy", size=C.MAXTILES, sysp=true },
    }

    g_dyntilei = nil
    g_dynsoundi = nil

    g_have_file = {}
    g_curcode = new_initial_codetab()
    -- [{actor, event, actor}num]=gencode_table
    g_code = { actor={}, event={}, loadactor={} }

    g_recurslevel = -1
    g_numerrors = 0
end

-- Is SYSTEM_GAMEARRAY?
local function issysgar(str)
    return str:match("^g_tile.siz[xy]")
end

local function addcode(x)
    assert(type(x)=="string" or type(x)=="table")
    g_curcode[#g_curcode+1] = x
end

local function addcodef(fmt, ...)
    addcode(format(fmt, ...))
end

local function paddcodef(pos, fmt, ...)
    addcodef(fmt.."--"..getlinecol(pos), ...)
end

local function add_code_and_end(codetab, endstr)
    assert(type(codetab)=="table")
    addcode(codetab)
    addcode(endstr)
end

local function get_cache_sap_code()
    return csapp() and "local _spr,_a,_ps=_con._getsap(_aci,_pli)" or ""
end

-- fwd-decls
local warnprintf, errprintf, pwarnprintf, perrprintf, contprintf

local on = {}

-- Map from CON actor usertype to SFLAGs.
local MAP_ACTOR_FLAGS = {
    [0] = 0,
    [1] = conl.SFLAG.SFLAG_BADGUY,
    [2] = conl.SFLAG.SFLAG_BADGUY + conl.SFLAG.SFLAG_BADGUYSTAYPUT,
    [3] = conl.SFLAG.SFLAG_BADGUY + conl.SFLAG.SFLAG_BADGUYSTAYPUT,
}
for i=4,7 do
    MAP_ACTOR_FLAGS[i] = MAP_ACTOR_FLAGS[i-4] + conl.SFLAG.SFLAG_ROTFIXED
end


function on.actor_end(pos, usertype, tsamm, codetab)
    local tilenum = tsamm[1]
    local flags = 0

    if (usertype ~= nil) then  -- useractor
        if (not (bit.band(usertype, bit.bnot(7)) == 0)) then
            perrprintf(pos, "invalid usertype: must be bitwise OR of 1, 2 and/or 4")
        else
            flags = MAP_ACTOR_FLAGS[usertype]
        end
    end

    -- 0x10000000: actor.FLAGS.replace_soft
    flags = bit.bor(flags, 0x10000000)

    local str = flags..","
    for i=2,math.min(#tsamm,4) do
        str = str .. tostring(tsamm[i])..","
    end
    if (#tsamm >= 5) then
        local movflags = bit.bor(unpack(tsamm, 5))
        str = str .. movflags..","
    end

    paddcodef(pos, "gameactor{%d,%sfunction(_aci,_pli,_dist)", tilenum, str)
    addcode(get_cache_sap_code())
    add_code_and_end(codetab, "end}")

    if (g_code.actor[tilenum] ~= nil) then
        pwarnprintf(pos, "redefined actor %d", tilenum)
    end
    g_code.actor[tilenum] = codetab
end

-- NOTE: in C-CON, the slash and backslash can also be part of an identifier,
-- but this is likely to support file names in other places.
local BAD_ID_CHARS0 = "_*?"  -- allowed 1st identifier chars
local BAD_ID_CHARS1 = "_*-+?"  -- allowed following identifier chars

local function truetab(tab)
    local ttab = {}
    for i=1,#tab do
        ttab[tab[i]] = true
    end
    return ttab
end

-- Lua 5.2 keywords. Not 5.1 because we use "goto" for codegen.
local LUA_KEYW = truetab {
    "and", "break", "do", "else", "elseif", "end",
    "false", "for", "function", "goto", "if", "in",
    "local", "nil", "not", "or", "repeat", "return",
    "then", "true", "until", "while"
}

-- Return the Lua code by which the CON object <name> is referenced in the
-- translated code.
local function mangle_name(name, prefix)
    if (name:match("^[A-Za-z_][A-Za-z_0-9]*$") and not LUA_KEYW[name]) then
        return format("_%s.%s", prefix, name)
    else
        return format("_%s[%q]", prefix, name)
    end
end

function on.state_begin_Cmt(_subj, _pos, statename)
    -- We must register the state name early (Cmt) because otherwise, it won't
    -- be found in a recursive state. XXX: The real issue seems to be the use
    -- of "Cmt"s in other places, which messes up the sequence of running the
    -- semantic actions.
    local ourname = mangle_name(statename, "F")
    g_funcname[statename] = ourname
    return true, ourname
end

function on.state_end(pos, funcname, codetab)
    paddcodef(pos, "%s=function(_aci,_pli,_dist)", funcname)
    addcode(get_cache_sap_code())
    add_code_and_end(codetab, "end")
end

function on.event_end(pos, eventidx, codetab)
    assert(type(codetab)=="table")
    -- 0x20000000: actor.FLAGS.chain_beg
    paddcodef(pos, "gameevent{%d,0x20000000,function (_aci,_pli,_dist)", eventidx)
    addcode(get_cache_sap_code())
    addcode(codetab)
    addcode("end}")

    g_code.event[eventidx] = codetab
end

function on.eventloadactor_end(pos, tilenum, codetab)
    -- Translate eventloadactor into a chained EVENT_LOADACTOR block
    paddcodef(pos, "gameevent{'LOADACTOR', function (_aci,_pli,_dist)")
    addcode(get_cache_sap_code())
    addcodef("if (%s==%d) then", SPS".picnum", tilenum)
    addcode(codetab)
    addcode("end")
    addcode("end}")

    if (g_code.loadactor[tilenum] ~= nil and g_warn["chained-loadactor"]) then
        -- NOTE: C-CON redefines loadactor code if encountered multiple times.
        pwarnprintf(pos, "chained additional loadactor %d code", tilenum)
    end
    g_code.loadactor[tilenum] = codetab
end

----------

local function linecolstr(pos)
    local line, col = getlinecol(pos)
    return format("%d:%d", line, col)
end

local function increment_numerrors()
    g_numerrors = g_numerrors+1
    if (g_numerrors == g_maxerrors) then
        g_numerrors = inf
        printf("Too many errors (%d), aborting...", g_maxerrors)
    end
end

function perrprintf(pos, fmt, ...)
    printf("%s %s: error: "..fmt, g_filename,
           pos and linecolstr(pos) or "???", ...)
    increment_numerrors()
end

function errprintf(fmt, ...)
    perrprintf(g_lastkwpos, fmt, ...)
end

function pwarnprintf(pos, fmt, ...)
    printf("%s %s: warning: "..fmt, g_filename,
           pos and linecolstr(pos) or "???", ...)
end

function warnprintf(fmt, ...)
    pwarnprintf(g_lastkwpos, fmt, ...)
end

-- Print a continuation line to an error or warning.
function contprintf(iserr, fmt, ...)
    printf("%s %s: %s  "..fmt, g_filename,
           g_lastkwpos and linecolstr(g_lastkwpos) or "???",
           iserr and "     " or "       ", ...)
end

local function parse_number(pos, numstr)
    -- <numstr> is a full number string, potentially prefixed with a minus sign.
    local num = tonumber((numstr:gsub("h$", "")))
--    local onum = num
    local hex = numstr:match("0[xX]([^h]*)h?")  -- get hex digits, if any

    -- num==nil for Rio Lua, which doesn't handle large hex literals.
    if (num==nil or not (num >= -0x80000000 and num <= 0xffffffff)) then
        -- number is <INT32_MIN or >UINT32_MAX or NaN
        if (hex and #hex>8 and hex:sub(1,#hex-8):match("^[fF]$")) then
            -- Too many hex digits, but they're all Fs.
            pwarnprintf(pos, "number %s truncated to 32 bits", numstr)
            num = bit.tobit(num)
        else
            perrprintf(pos, "number %s out of the range of a 32-bit integer", numstr)
            -- Be careful not to write bound checks like
            -- "if (i<LOWBOUND or i>HIGHBOUND) then error('...') end":
            num = NaN
        end
    elseif (num >= 0x80000000) then
        if (not hex and g_warn["number-conversion"]) then
            pwarnprintf(pos, "number %s converted to a negative one", numstr)
        end
        num = bit.tobit(num)
    end

--    printf("numstr:%s, num=%d (0x%s) '%s', resnum=%d (0x%s)",
--           numstr, onum, bit.tohex(onum), hex, num, bit.tohex(num))
    return num
end

-- Bound checking functions that generate a compilation error on failure.
local check = {}

function check.tile_idx(tilenum)
    if (not (tilenum >= 0 and tilenum < C.MAXTILES)) then
        errprintf("invalid tile number %d", tilenum)
        return false
    end
    return true
end

function check.sound_idx(sidx)
    if (not (sidx >= 0 and sidx < conl.MAXSOUNDS)) then
        errprintf("invalid sound number %d", sidx)
        return false
    end
    return true
end


-- Mapping of various "define" types to the respective number of members and
-- vice versa
local LABEL = { MOVE=2, AI=3, ACTION=5, [2]="move", [3]="ai", [5]="action",
                NUMBER=1, [1]="number" }

-- Function names in the 'con' module:
local LABEL_FUNCNAME = { [2]="move", [3]="ai", [5]="action" }
local LABEL_PREFIX = { [2]="M", [3]="I", [5]="C" }  -- _C, _M, _I in the gen'd code

local g_labeldef = {}  -- Lua numbers for numbers, strings for composites
local g_labeltype = {}
local g_labelspecial = {}  -- [<label>] = true
local g_labelloc = {}  -- [<label>] = { filename, linenum, colnum }

-- Get location table for use in continued warning/error reporting.
local function getLocation()
    return { g_filename, getlinecol(g_lastkwpos) }
end

function reset.labels()
    g_badids = {}

    -- NO is also a valid `move', `ai' or `action', but they are handled
    -- separately in lookup.composite().
    g_labeldef = {
        NO = 0,
        -- NOTE: these are read-only gamevars in C-CON
        CLIPMASK0 = 65536+1,  -- blocking
        CLIPMASK1 = (256*65536)+64,  -- hittable
        -- TODO_MP
        COOP = 0,
        MULTIMODE = 1,
        numplayers = 1,
        myconnectindex = 0,
        -- Predefined constants
        MAXSTATUS = C.MAXSTATUS,
        MAXSPRITES = C.MAXSPRITES,
        MAX_WEAPONS = C.MAX_WEAPONS,
    }

    g_labeltype = {}
    g_labelspecial = {}
    g_labelloc = {}

    for varname,_ in pairs(g_labeldef) do
        g_labeltype[varname] = LABEL.NUMBER
        g_labelspecial[varname] = true
    end

    -- Initialize default defines.
    for i=1,#conl.labels do
        for label, val in pairs(conl.labels[i]) do
            g_labeldef[label] = val
            g_labeltype[label] = LABEL.NUMBER
        end
    end
end

-- Table of functions doing various lookups (label, gamevar, ...)
local lookup = {}

function lookup.defined_label(pos, maybe_minus_str, identifier)
    local num = g_labeldef[identifier]

    if (num == nil) then
        perrprintf(pos, "label \"%s\" is not defined", identifier)
        return -inf  -- return a number for type cleanness
    end

    if (g_labeltype[identifier] ~= LABEL.NUMBER) then
        perrprintf(pos, "label \"%s\" is not a `define'd number", identifier)
        return -inf
    end

    assert(type(num)=="number")

    return (maybe_minus_str=="" and 1 or -1) * num
end

assert(not BAD_ID_CHARS1:find(":"))
function lookup.raw_defined_label(pos, maybe_minus_str, identifier)
    return pos..":"..maybe_minus_str..":"..identifier
end

local dynmap = {}
-- When necessary, initialize dynamic {tile,sound} mapping list.
function dynmap.maybe_init(dyni, dynList)
    if (dyni[1]==nil) then
        dyni[1] = true
        -- Init name -> g_dyn*List index mapping
        for i=0,math.huge do
            local str = dynList[i].str
            if (str==nil) then
                break
            end

            dyni[ffi.string(str)] = i
        end
    end
end

-- Potentially process one dynamic {tile,sound} remapping.
function dynmap.maybe_process(dyni, dynList, identifier, num)
    if (dyni[identifier]) then
        local di = dynList[dyni[identifier]]

        if (ffiC._DEBUG_LUNATIC~=0 and di.staticval~=num) then
            printf("REMAP %s (%d) --> %d", ffi.string(di.str), di.staticval, num)
        end
        di.dynvalptr[0] = num
    end
end

-- The 'check' table is also used to hold a couple of misc checkers.

function check.sysvar_def_attempt(identifier)
    if (identifier=="actorvar") then
        errprintf("cannot define reserved symbol `actorvar'")
        return true
    end
    if (identifier=="_IS_NORESET_GAMEVAR") then
        errprintf("cannot define reserved symbol `_IS_NORESET_GAMEVAR'")
        return true
    end
end


local inform = {}

function inform.common(loc, iserr)
    if (loc) then
        contprintf(iserr, "Old definition is at %s %d:%d",
                   loc[1], loc[2], loc[3])
    else
        contprintf(iserr, "Old definition is built-in")
    end
end

function inform.olddef_location(identifier, iserr)
    inform.common(g_labelloc[identifier], iserr)
end

function inform.oldgv_location(identifier, iserr)
    inform.common(g_gamevar[identifier].loc, iserr)
end


local Define = {}

function Define.label(identifier, num)
    if (check.sysvar_def_attempt(identifier)) then
        return
    end

    local oldtype = g_labeltype[identifier]
    local oldval = g_labeldef[identifier]

    if (oldval) then
        if (oldtype ~= LABEL.NUMBER) then
            errprintf("Refusing to overwrite `%s' label \"%s\" with a `define'd number.",
                      LABEL[oldtype], identifier)
            inform.olddef_location(identifier, true)
        else
            -- conl.labels[...]: don't warn for wrong PROJ_ redefinitions
            if (g_warn["not-redefined"]) then
                if (oldval ~= num and conl.PROJ[identifier]==nil) then
                    warnprintf("Label \"%s\" not redefined with new value %d (old: %d).",
                               identifier, num, oldval)
                    inform.olddef_location(identifier, false)
                end
            end
        end
    else
        if (g_gamevar[identifier]) then
            warnprintf("symbol `%s' already used for game variable", identifier)
            inform.oldgv_location(identifier, false)
        end

        if (ffi and g_dyntilei and (num>=0 and num<C.MAXTILES)) then
            dynmap.maybe_init(g_dyntilei, ffiC.g_dynTileList)
            dynmap.maybe_process(g_dyntilei, ffiC.g_dynTileList, identifier, num)
        end

        -- New definition of a label
        g_labeldef[identifier] = num
        g_labeltype[identifier] = LABEL.NUMBER
        g_labelloc[identifier] = getLocation()
    end
end

function check.composite_literal(labeltype, pos, num)
    if (num==0 or num==1) then
        return (num==0) and "0" or "1"
    else
        perrprintf(pos, "literal `%s' number must be either 0 or 1", LABEL[labeltype])
        return "_INVALIT"
    end
end

function lookup.composite(labeltype, pos, identifier)
    if (identifier=="NO") then
        -- NO is a special case and is valid for move, action and ai,
        -- being the same as passing a literal 0.
        return "0"
    end

    local val = g_labeldef[identifier]
    local typ = g_labeltype[identifier]

    if (val == nil) then
        perrprintf(pos, "label \"%s\" is not defined", identifier)
        return "_NOTDEF"
    elseif (typ ~= labeltype) then
        if (identifier=="randomangle" and labeltype==LABEL.MOVE and typ==LABEL.NUMBER) then
            -- Be forgiving with a 1.3/1.5 GAME.CON type error.
            pwarnprintf(pos, "label \"randomangle\" is not a `move' value, assuming 0")
            return "0"
        elseif (identifier=="BLIMPRESPAWNTIME" and labeltype==LABEL.ACTION and typ==LABEL.NUMBER) then
            -- Be forgiving with a 1.3 GAME.CON type error.
            pwarnprintf(pos, "label \"BLIMPRESPAWNTIME\" is not an `action' value, assuming 0")
            return "0"
        else
            perrprintf(pos, "label \"%s\" is not a%s `%s' value", identifier,
                       labeltype==LABEL.MOVE and "" or "n", LABEL[labeltype])
            return "_WRONGTYPE"
        end
    end

    return val
end

function check.reserved_bits(flags, allowedbits, suffix)
    local rbits = bit.bnot(allowedbits)
    if (bit.band(flags, rbits) ~= 0) then
        warnprintf("set one or more reserved bits (0x%s) "..suffix,
                   bit.tohex(bit.band(flags, rbits)))
    end
end

Define.ALLOWED_VIEWTYPE = truetab { 0, 1, 3,4, 5, 7, 8, -5, -7 }

function Define.composite(labeltype, identifier, ...)
    local oldtype = g_labeltype[identifier]
    local oldval = g_labeldef[identifier]

    if (oldval) then
        if (oldtype ~= labeltype) then
            errprintf("Refusing to overwrite `%s' label \"%s\" with a `%s' value.",
                      LABEL[oldtype], identifier, LABEL[labeltype])
            inform.olddef_location(identifier, true)
        else
            warnprintf("Duplicate `%s' definition of \"%s\" ignored.",
                       LABEL[labeltype], identifier)
            inform.olddef_location(identifier, false)
        end
        return
    end

    -- Fill up omitted arguments denoting composites with zeros.
    local isai = (labeltype == LABEL.AI)
    local args = {...}
    for i=#args+1,labeltype do
        -- Passing nil/nothing as remaining args to con.ai will make the
        -- action/move the null one.
        args[i] = (isai and i<=2) and "nil" or 0
    end

    if (isai) then
        assert(type(args[1])=="string")
        assert(type(args[2])=="string")

        -- OR together the flags
        for i=#args,LABEL.AI+1, -1 do
            args[LABEL.AI] = bit.bor(args[LABEL.AI], args[i])
            args[i] = nil
        end

        -- Check whether movflags use reserved bits.
        check.reserved_bits(args[LABEL.AI], 4096+2047, "for ai's movflags")
    end

    if (labeltype == LABEL.ACTION) then
        -- Sanity-check action members.
        -- KEEPINSYNC with ACTOR_CHECK in control.lua for consistency.
        if (not (args[2] >= 0)) then
            errprintf("action \"%s\" has negative number of frames", identifier)
        end
        if (Define.ALLOWED_VIEWTYPE[args[3]] == nil) then
            errprintf("action \"%s\" has disallowed viewtype %d", identifier, args[3])
        end
        if (not (args[4] >= -1 and args[4] <= 1)) then
            warnprintf("action \"%s\" has incval different from -1, 0 or 1", identifier)
        end
    end

    -- Make a string out of that.
    for i=1+(isai and 2 or 0),#args do
        args[i] = format("%d", args[i])
    end

    local refcode = mangle_name(identifier, LABEL_PREFIX[labeltype])
    addcodef(isai and "%s=_con.%s(%s)" or "%s=_con.%s{%s}",  -- ai has parens
             refcode, LABEL_FUNCNAME[labeltype], table.concat(args, ","))

    g_labeldef[identifier] = refcode
    g_labeltype[identifier] = labeltype
    g_labelloc[identifier] = getLocation()
end


local function parse(contents) end -- fwd-decl

local function do_include_file(dirname, filename, isroot)
    assert(type(filename)=="string")

    if (g_have_file[filename] ~= nil) then
        printf("[%d] Fatal error: infinite loop including \"%s\"", g_recurslevel, filename)
        g_numerrors = inf
        return
    end

    local contents

    if (read_into_string) then
        -- running from EDuke32
        contents = read_into_string(filename)
    else
        -- running stand-alone
        local io = require("io")

        local fd, msg = io.open(dirname..filename)
        while (fd == nil and not isroot and filename:find("/")) do
            -- strip up to and including first slash:
            filename = filename:gsub("^.-/", "")
            fd, msg = io.open(dirname..filename)
        end

        -- As a last resort, try the "default directory"
        if (fd==nil and not isroot and g_defaultDir) then
            -- strip up to and including last slash (if any):
            filename = filename:gsub("^.*/", "")
            dirname = g_defaultDir.."/"
            fd, msg = io.open(dirname..filename)
        end

        if (fd == nil) then
            printf("[%d] Fatal error: couldn't open %s", g_recurslevel, msg)
            g_numerrors = inf
            return
        end

        contents = fd:read("*all")
        fd:close()
    end

    if (contents == nil) then
        -- maybe that file name turned out to be a directory or other
        -- special file accidentally
        printf("[%d] Fatal error: couldn't read from \"%s\"",
               g_recurslevel, dirname..filename)
        g_numerrors = inf
        return
    end

    printf("%s[%d] Translating file \"%s\"", (g_recurslevel==-1 and "\n---- ") or "",
           g_recurslevel+1, dirname..filename);

    local oldfilename = g_filename
    g_filename = filename
    parse(contents)
    g_filename = oldfilename
end

-- Table of various outer command handling functions.
local Cmd = {}

function Cmd.NYI(msg)
    return function()
        errprintf(msg.." not yet implemented")
    end
end

function Cmd.nyi(msg)
    return function()
        warnprintf(msg.." not yet implemented")
    end
end

function Cmd.include(filename)
    do_include_file(g_directory, filename, false)
end

--- Per-module game data
local g_data = {}
local EPMUL = conl.MAXLEVELS

function reset.gamedata()
    g_data = {}

    -- [EPMUL*ep + lev] = { ptime=<num>, dtime=<num>, fn=<str>, name=<str> }
    g_data.level = {}
    -- [ep] = <str>
    g_data.volname = {}
    -- [skillnum] = <str>
    g_data.skillname = {}
    -- [quotenum] = <str>
    g_data.quote = {}
    -- table of length 26 or 30 containg numbers
    g_data.startup = {}
    -- [soundnum] = { fn=<str>, params=<table of length 5> }
    g_data.sound = {}
    -- [volnum] = <table of length numlevels (<= MAXLEVELS) of <str>>
    g_data.music = {}
end

-- TODO: PRE13 has no <dtstr> (3D Realms time).
function Cmd.definelevelname(vol, lev, fn, ptstr, dtstr, levname)
    if (not (vol >= 0 and vol < conl.MAXVOLUMES)) then
        errprintf("volume number exceeds maximum volume count.")
        return
    end

    if (not (lev >= 0 and lev < conl.MAXLEVELS)) then
        errprintf("level number exceeds maximum number of levels per episode.")
        return
    end

    -- TODO: Bcorrectfilename(fn)

    local function secs(tstr)
        local m, s = string.match(tstr, ".+:.+")
        m, s = tonumber(m), tonumber(s)
        return (m and s) and m*60+s or 0
    end

    local map = {
        ptime=secs(ptstr), dtime=secs(dtstr), fn="/"..fn, name=levname
    }

    if (ffi) then
        ffiC.C_DefineLevelName(vol, lev, map.fn, map.ptime, map.dtime, map.name)
    end

    g_data.level[EPMUL*vol+lev] = map
end

local function defineXname(what, ffiCfuncname, X, name)
    name = name:upper()
    if (ffi) then
        ffiC[ffiCfuncname](X, name)
        if (#name > 32) then
            warnprintf("%s %d name truncated to 32 characters.", what, X)
        end
    end
    return name
end

function Cmd.defineskillname(skillnum, name)
    if (not (skillnum >= 0 and skillnum < conl.MAXSKILLS)) then
        errprintf("skill number is negative or exceeds maximum skill count.")
        return
    end

    name = defineXname("skill", "C_DefineSkillName", skillnum, name)
    g_data.skillname[skillnum] = name
end

function Cmd.definevolumename(vol, name)
    if (not (vol >= 0 and vol < conl.MAXVOLUMES)) then
        errprintf("volume number is negative or exceeds maximum volume count.")
        return
    end

    name = defineXname("volume", "C_DefineVolumeName", vol, name)
    g_data.volname[vol] = name
end

function Cmd.definegamefuncname(idx, name)
    local NUMGAMEFUNCTIONS = (ffi and ffiC.NUMGAMEFUNCTIONS or 56)
    if (not (idx >= 0 and idx < NUMGAMEFUNCTIONS)) then
        errprintf("function number exceeds number of game functions.")
        return
    end

    assert(type(name)=="string")
    -- XXX: in place of C-CON's "invalid character in function name" report:
    name = name:gsub("[^A-Za-z0-9]", "_")

    if (ffi) then
        ffiC.C_DefineGameFuncName(idx, name)
    end
end

function Cmd.definegametype(idx, flags, name)
    if (not (idx >= 0 and idx < conl.MAXGAMETYPES)) then
        errprintf("gametype number exceeds maximum gametype count.")
        return
    end

    if (ffi) then
        ffiC.C_DefineGameType(idx, flags, name)
    end
end

-- strip whitespace from front and back
local function stripws(str)
    return str:match("^%s*(.*)%s*$")
end

function Cmd.definequote(qnum, quotestr)
    if (not (qnum >= 0 and qnum < conl.MAXQUOTES)) then
        errprintf("quote number is negative or exceeds limit of %d.", conl.MAXQUOTES-1)
        return ""
    end

    quotestr = stripws(quotestr)

    if (#quotestr >= conl.MAXQUOTELEN) then
        -- NOTE: Actually, C_DefineQuote takes care of this! That is,
        -- standalone, the string isn't truncated.
        warnprintf("quote %d truncated to %d characters.", qnum, conl.MAXQUOTELEN-1)
    end

    if (ffi) then
        ffiC.C_DefineQuote(qnum, quotestr)
    end

    g_data.quote[qnum] = quotestr
    return ""
end

local PROJ = {}
for key, val in pairs(conl.PROJ) do
    -- Strip "PROJ_"
    PROJ[key:sub(6)] = val
end

function Cmd.defineprojectile(tilenum, what, val)
    local ok = check.tile_idx(tilenum)

    if (what==PROJ.WORKSLIKE) then
        check.reserved_bits(val, 2^21-1, "for PROJ_WORKSLIKE")
    elseif (what==PROJ.SOUND or what==PROJ.ISOUND or what==PROJ.BSOUND) then
        ok = ok and (val==-1 or check.sound_idx(val))
    elseif (what==PROJ.SPAWNS or what==PROJ.DECAL or what==PROJ.TRAIL) then
        ok = ok and (val==-1 or check.tile_idx(val))
    end

    if (ffi and ok) then
        ffiC.C_DefineProjectile(tilenum, what, val)
    end
end

-- flags: if string, look up in ffiC and OR, else set number directly.
function Cmd.xspriteflags(tilenum, flags)
    local ok = check.tile_idx(tilenum)

    if (ffi and ok) then
        if (type(flags)=="number") then
            ffiC.g_tile[tilenum]._flags = flags
        else
            assert(type(flags)=="string")
            ffiC.g_tile[tilenum]._flags = bit.bor(ffiC.g_tile[tilenum]._flags, ffiC[flags])
        end
    end
end

function Cmd.cheatkeys(sc1, sc2)
    if (ffi) then
        ffiC.CheatKeys[0] = sc1
        ffiC.CheatKeys[1] = sc2
    end
end

function Cmd.setdefname(filename)
    assert(type(filename)=="string")
    if (ffi) then
        if (ffiC.C_SetDefName(filename) ~= 0) then
            error("OUT OF MEMORY", 0)
        end
    end
end

function Cmd.setcfgname(filename)
    assert(type(filename)=="string")
    if (ffi) then
        ffiC.C_SetCfgName(filename)
    end
end

function Cmd.gamestartup(...)
    local args = {...}

    -- TODO: PRE13: detection of other g_scriptVersion.
    if (#args ~= 26 and #args ~= 30) then
        errprintf("must pass either 26 (1.3D) or 30 (1.5) values")
        return
    end

    if (ffi) then
        -- running from EDuke32
        if (#args == 30) then
            ffiC.g_scriptVersion = 14
        end
        local params = ffi.new("int32_t [30]", args)
        ffiC.G_DoGameStartup(params)
    end

    g_data.startup = args  -- TODO: sanity-check them
end

function Cmd.definesound(sndlabel, fn, ...)
    local sndnum

    if (type(sndlabel)=="string") then
        local pos, minus, label = sndlabel:match("(.-):(.-):(.+)")
        sndnum = lookup.defined_label(tonumber(pos), minus, label)

        if (ffi and g_dynsoundi and (sndnum>=0 and sndnum<conl.MAXSOUNDS)) then
            dynmap.maybe_init(g_dynsoundi, ffiC.g_dynSoundList)
            dynmap.maybe_process(g_dynsoundi, ffiC.g_dynSoundList, label, sndnum)
        end
    else
        assert(type(sndlabel)=="number")
        sndnum = sndlabel
    end

    if (not (sndnum >= 0 and sndnum < conl.MAXSOUNDS)) then
        errprintf("sound number is negative or exceeds sound limit of %d", conl.MAXSOUNDS-1)
        return
    end

    local params = {...}  -- TODO: sanity-check them some more
    check.reserved_bits(params[4], 31+128, "for sound flags")

    if (ffi) then
        local cparams = ffi.new("int32_t [5]", params)
        assert(type(fn)=="string")
        ffiC.C_DefineSound(sndnum, fn, cparams)
    end

    g_data.sound[sndnum] = { fn=fn, params=params }
end

function Cmd.music(volnum, ...)
    local envmusicp = (volnum==0)

    if (not (volnum >= 0 and volnum <= conl.MAXVOLUMES+1)) then
        -- NOTE: Also allow MAXVOLUMES+1.
        errprintf("volume number must be between 0 and MAXVOLUMES=%d", conl.MAXVOLUMES)
        return
    end

    local filenames = {...}
    local MAXFNS = envmusicp and conl.MAXVOLUMES or conl.MAXLEVELS

    if (#filenames > MAXFNS) then
        warnprintf("ignoring extraneous %d music file names", #filenames-MAXFNS)
        for i=MAXFNS+1,#filenames do
            filenames[i] = nil
        end
    end

    if (ffi) then
        for i=1,#filenames do
            assert(type(filenames[i])=="string")
            ffiC.C_DefineMusic(volnum-1, i-1, "/"..filenames[i])
        end
    end

    g_data.music[volnum] = filenames
end


--- GAMEVARS / GAMEARRAYS

function Cmd.gamearray(identifier, initsize)
    if (check.sysvar_def_attempt(identifier)) then
        return
    end

    if (not (initsize >= 0 and initsize < 0x7fffffff)) then
        errprintf("invalid initial size %d for gamearray `%s'", initsize, identifier)
        return
    end

    local oga = g_gamearray[identifier]
    if (oga) then
        if (oga.sysp) then
            errprintf("attempt to define system gamearray `%s'", identifier)
            return
        elseif (initsize ~= oga.size) then
            errprintf("duplicate gamearray definition `%s' has different size", identifier)
            return
        else
            warnprintf("duplicate gamearray definition `%s' ignored", identifier)
            return
        end
    end

    if (g_gamevar[identifier]) then
        warnprintf("symbol `%s' already used for game variable", identifier)
        inform.oldgv_location(identifier, false)
    end

    local ga = { name=mangle_name(identifier, "A"), size=initsize }
    g_gamearray[identifier] = ga

    addcode("if _S then")
    addcodef("%s=_con._gamearray(%d)", ga.name, initsize)
    addcode("end")
end

function Cmd.gamevar(identifier, initval, flags)
    if (check.sysvar_def_attempt(identifier)) then
        return
    end

    -- TODO: handle user bits like NORESET or NODEFAULT
    if (bit.band(flags, bit.bnot(GVFLAG.USER_MASK)) ~= 0) then
        -- TODO: a couple of the presumably safe ones
        errprintf("gamevar flags other than 1, 2, 1024 or 131072: NYI or forbidden")
        return
    end

    local perPlayer = (bit.band(flags, GVFLAG.PERPLAYER) ~= 0)
    local perActor = (bit.band(flags, GVFLAG.PERACTOR) ~= 0)

    if (perPlayer and perActor) then
        errprintf("invalid gamevar flags: must be either PERPLAYER or PERACTOR, not both")
        return
    end

    local ogv = g_gamevar[identifier]
    local isSessionVar = (bit.band(flags, GVFLAG.NODEFAULT) ~= 0)
    local storeWithSavegames = (bit.band(flags, GVFLAG.NORESET) == 0)

    if (isSessionVar and (perPlayer or perActor)) then
        if (ogv == nil) then  -- warn only once per gamevar
            warnprintf("per-%s session gamevar `%s': NYI, made %s",
                       perPlayer and "player" or "actor",
                       identifier,
                       perPlayer and "global" or "non-session")
        end

        if (perActor) then
            flags = bit.band(flags, bit.bnot(GVFLAG.NODEFAULT))
            isSessionVar = false
        elseif (perPlayer) then
            flags = bit.band(flags, bit.bnot(GVFLAG.PERPLAYER))
            perPlayer = false
        end
    end

    if (ogv ~= nil) then
        local oflags = ogv.flags
        if (oflags ~= flags) then
            if (bit.band(oflags, GVFLAG.SYSTEM) ~= 0 and not isSessionVar) then
                -- Attempt to override a system gamevar. See if it's read-only...
                if (bit.band(oflags, GVFLAG.READONLY) ~= 0) then
                    errprintf("attempt to override read-only system gamevar `%s'", identifier)
                    return
                end

                local flagsnosys = bit.band(oflags, bit.bnot(GVFLAG.SYSTEM))
                if (flagsnosys ~= flags and g_warn["system-gamevar"]) then
                    warnprintf("overrode initial value of `%s', but kept "..
                               "flags (%d)", identifier, flagsnosys)
                end

                if (ogv.rbits and bit.band(ogv.rbits, initval)~=0) then
                    warnprintf("set one or more reserved bits (0x%s) in overriding `%s'",
                               bit.tohex(bit.band(ogv.rbits, initval)), identifier)
                end

                local linestr = "--"..getlinecol(g_lastkwpos)

                -- Emit code to set the variable at Lua parse time.
                -- XXX: How does this interact with savegame restoration?
                if (bit.band(oflags, GVFLAG.PERPLAYER) ~= 0) then
                    -- Replace player index by 0. PLAYER_0.
                    -- TODO_MP: init for all players.
                    local pvar, numrepls = ogv.name:gsub("_pli", "0")
                    assert(numrepls>=1)
                    addcodef("%s=%d%s", pvar, initval, linestr)
                else
                    addcodef("%s=%d%s", ogv.name, initval, linestr)
                end
                return
            end

            errprintf("duplicate definition of gamevar `%s' has different flags", identifier)
            inform.oldgv_location(identifier, true)
            return
        else
            warnprintf("duplicate definition of gamevar `%s' ignored", identifier)
            inform.oldgv_location(identifier, false)
            return
        end
    end

    local ltype = g_labeltype[identifier]
    if (ltype ~= nil) then
        warnprintf("Symbol `%s' already used for a defined %s.", identifier, LABEL[ltype])
        inform.olddef_location(identifier, false)
    end

    if (isSessionVar) then
        if (g_numSessionVars == conl.MAXSESSIONVARS) then
            errprintf("Declared too many session gamevars (flag 1024), can have at most %d.",
                      conl.MAXSESSIONVARS)
            return
        end

        -- Declare new session gamevar.
        g_gamevar[identifier] = { name=format("_gv._sessionVar[%d]", g_numSessionVars),
                                  flags=flags, loc=getLocation() }
        g_numSessionVars = g_numSessionVars+1
        return
    end

    local gv = { name=mangle_name(identifier, "V"), flags=flags, loc=getLocation() }
    g_gamevar[identifier] = gv

    if (storeWithSavegames) then
        addcode("if _S then")
    end

    if (perActor) then
        addcodef("%s=_con.actorvar(%d)", gv.name, initval)
    elseif (perPlayer and g_cgopt["playervar"]) then
        gv.flags = bit.bor(gv.flags, GVFLAG.CON_PERPLAYER)
        addcodef("%s=_con.playervar(%d)", gv.name, initval)
    else
        addcodef("%s=%d", gv.name, initval)
    end

    if (storeWithSavegames) then
        addcode("end")
    end
end

function Cmd.dynamicremap()
    if (g_dyntilei==nil) then
        print("Using dynamic tile remapping");
        g_dyntilei = {};
    end
end

function Cmd.dynamicsoundremap()
    if (g_dynsoundi==nil) then
        print("Using dynamic sound remapping");
        g_dynsoundi = {};
    end
end

function lookup.gamearray(identifier)
    local ga = g_gamearray[identifier]
    if (ga == nil) then
        errprintf("symbol `%s' is not a game array", identifier)
        return "_INVALIDGA"
    end
    return ga.name
end

local function thisactor_to_pli(var)
    return (var=="_aci") and "_pli" or var
end

function lookup.error_not_gamevar(identifier)
    errprintf("symbol `%s' is not a game variable", identifier)
    return "_INVALIDGV"
end

-- <aorpvar>: code for actor or player index
function lookup.gamevar(identifier, aorpvar, writable)
    local gv = g_gamevar[identifier]

    if (gv == nil) then
        return lookup.error_not_gamevar(identifier)
    end

    if (writable and bit.band(gv.flags, GVFLAG.READONLY) ~= 0) then
        errprintf("gamevar `%s' is read-only", identifier)
        return "_READONLYGV"
    end

    if (bit.band(gv.flags, GVFLAG.PERACTOR)~=0) then
        return format("%s[%s]", gv.name, aorpvar)
    elseif (bit.band(gv.flags, GVFLAG.CON_PERPLAYER)~=0 and g_cgopt["playervar"]) then
        return format("%s[%s]", gv.name, thisactor_to_pli(aorpvar))
    else
        return gv.name
    end
end

local function maybe_gamevar_Cmt(subj, pos, identifier)
    if (g_gamevar[identifier]) then
        return true, lookup.gamevar(identifier, "_aci", false)
    end
end


----==== patterns ====----

---- basic ones
-- Windows, *nix and Mac newlines all exist in the wild!
local newline = "\r"*Pat("\n")^-1 + "\n"
local EOF = Pat(-1)
local anychar = Pat(1)
-- comments
local comment = "/*" * match_until(anychar, "*/") * "*/"
local linecomment = "//" * match_until(anychar, newline)
local whitespace = Var("whitespace")
local sp0 = whitespace^0
-- This "WS+" pattern matches EOF too, so that a forgotten newline at EOF is
-- properly handled
local sp1 = whitespace^1 + EOF
local alpha = Range("AZ", "az")  -- locale?
local alphanum = alpha + Range("09")
--local alnumtok = alphanum + Set("{}/\\*-_.")  -- see isaltok() in gamedef.c

--- Basic lexical elements ("tokens"). See the final grammar ("Grammar") for
--- their definitions.
local tok =
{
    maybe_minus = (Pat("-") * sp0)^-1,
    number = Var("t_number"),

    -- Valid identifier names are disjunct from keywords!
    -- XXX: CON is more permissive with identifier name characters:
    identifier = Var("t_identifier"),
    -- This one matches keywords, too:
    identifier_all = Var("t_identifier_all"),
    define = Var("t_define"),
    rawdefine = Var("t_rawdefine"),
    move = Var("t_move"),
    ai = Var("t_ai"),
    action = Var("t_action"),

    -- NOTE: no chance to whitespace and double quotes in filenames:
    filename = lpeg.C((anychar-Set(" \t\r\n\""))^1),
    newline_term_str = match_until(anychar, newline),

    rvar = Var("t_rvar"),
    wvar = Var("t_wvar"),
    gamearray = Var("t_gamearray"),

    -- for definelevelname
    time = lpeg.C(alphanum*alphanum^-1*":"*alphanum*alphanum^-1),

    state_ends = Pat("ends")
        + POS() * "else" * sp1 * "ends"
        / function(pos) pwarnprintf(pos, "stray `else' at end of state") end,
}


---- helper patterns / pattern constructing functions
local maybe_quoted_filename = ('"' * tok.filename * '"' + tok.filename)
-- empty string is handled too; we must not eat the newline then!
local newline_term_string = (#newline + EOF)*lpeg.Cc("")
    + (whitespace-newline)^1 * lpeg.C(tok.newline_term_str)


-- (sp1 * tok.define) repeated exactly n times
local function n_defines(n)  -- works well only for small n
    local pat = Pat(true)
    for i=1,n do
        pat = sp1 * tok.define * pat
    end
    return pat
end


local D, R, W, I, GARI, AC, MV, AI = -1, -2, -3, -4, -5, -6, -7, -8
local TOKEN_PATTERN = { [D]=tok.define, [R]=tok.rvar, [W]=tok.wvar,
                        [I]=tok.identifier, [GARI]=tok.gamearray,
                        [AC]=tok.action, [MV]=tok.move, [AI]=tok.ai }

-- Generic command pattern, types given by varargs.
-- The command name to be matched is attached later.
-- Example:
--  "command" writtenvar readvar def def:  gencmd(W,R,D,D)
--    -->  sp1 * tok.wvar * sp1 * tok.rvar * sp1 * tok.define * sp1 * tok.define
--  "command_with_no_args":  gencmd()
--    --> Pat(true)
local function cmd(...)
    local pat = Pat(true)
    local vartypes = {...}

    for i=1,#vartypes do
        pat = pat * sp1 * assert(TOKEN_PATTERN[vartypes[i]])
    end

    return pat
end


-- The command names will be attached to the front of the patterns later!

--== Top level CON commands ==--
-- XXX: many of these are also allowed inside actors/states/events in CON.
local Couter = {
    --- 1. Preprocessor
    include = sp1 * maybe_quoted_filename
        / Cmd.include,
    includedefault = cmd()
        / Cmd.NYI("`includedefault'"),
    define = cmd(I,D)
        / Define.label,

    --- 2. Defines and Meta-Settings
    dynamicremap = cmd()
        / Cmd.dynamicremap,
    dynamicsoundremap = cmd()
        / Cmd.dynamicsoundremap,
    setcfgname = sp1 * tok.filename
        / Cmd.setcfgname,
    setdefname = sp1 * tok.filename
        / Cmd.setdefname,
    setgamename = newline_term_string
        / Cmd.nyi("`setgamename'"),

    precache = cmd(D,D,D)
        , -- / Cmd.nyi("`precache'"),
    scriptsize = cmd(D)
        / "",  -- no-op
    cheatkeys = cmd(D,D)
        / Cmd.cheatkeys,

    definecheat = newline_term_string  -- XXX: actually tricker syntax (TS)
        , -- / Cmd.nyi("`definecheat'"),
    definegamefuncname = sp1 * tok.define * newline_term_string  -- XXX: TS?
        / Cmd.definegamefuncname,
    definegametype = n_defines(2) * newline_term_string
        / Cmd.definegametype,
    definelevelname = n_defines(2) * sp1 * tok.filename * sp1 * tok.time * sp1 * tok.time *
        newline_term_string
        / Cmd.definelevelname,
    defineskillname = sp1 * tok.define * newline_term_string
        / Cmd.defineskillname,
    definevolumename = sp1 * tok.define * newline_term_string
        / Cmd.definevolumename,

    definequote = sp1 * tok.define * newline_term_string
        / Cmd.definequote,
    defineprojectile = cmd(D,D,D)
        / Cmd.defineprojectile,
    definesound = sp1 * tok.rawdefine * sp1 * maybe_quoted_filename * n_defines(5)
        / Cmd.definesound,

    -- NOTE: gamevar.ogg and the like is OK, too
    music = sp1 * tok.define * match_until(sp1 * tok.filename, sp1 * conl.keyword * sp1)
        / Cmd.music,

    --- 3. Game Settings
    -- gamestartup has 26/30 fixed defines, depending on 1.3D/1.5 version:
    gamestartup = (sp1 * tok.define)^26
        / Cmd.gamestartup,
    spritenopal = cmd(D)
        / function(tilenum, flags) Cmd.xspriteflags(tilenum, "SFLAG_NOPAL") end,
    spritenoshade = cmd(D)
        / function(tilenum, flags) Cmd.xspriteflags(tilenum, "SFLAG_NOSHADE") end,
    spritenvg = cmd(D)
        / function(tilenum, flags) Cmd.xspriteflags(tilenum, "SFLAG_NVG") end,
    spriteshadow = cmd(D)
        / function(tilenum, flags) Cmd.xspriteflags(tilenum, "SFLAG_SHADOW") end,

    spriteflags = cmd(D,D)  -- also see inner
        / function(tilenum, flags) Cmd.xspriteflags(tilenum, flags) end,

    --- 4. Game Variables / Arrays
    gamevar = cmd(I,D,D)
        / Cmd.gamevar,
    gamearray = cmd(I,D)
        / Cmd.gamearray,

    --- 5. Top level commands that are also run-time commands
    move = sp1 * tok.identifier * (sp1 * tok.define)^-2  -- hvel, vvel
        / function(...) Define.composite(LABEL.MOVE, ...) end,

    -- startframe, numframes, viewtype, incval, delay:
    action = sp1 * tok.identifier * (sp1 * tok.define)^-5
        / function(...) Define.composite(LABEL.ACTION, ...) end,

    -- action, move, flags...:
    ai = sp1 * tok.identifier * (sp1 * tok.action *
                                 (sp1 * tok.move * (sp1 * tok.define)^0)^-1
                                )^-1
        / function(...) Define.composite(LABEL.AI, ...) end,

    --- 6. Deprecated TLCs
    betaname = newline_term_string,
    enhanced = cmd(D),
}


--== Run time CON commands ==--
--- 1. Gamevar Operators
local Op = {}
Op.var = cmd(W,D)
Op.varvar = cmd(W,R)

function Op.var_common(thecmd, defaultop, trapop, wrapop)
    local theop =
        g_cgopt["trapv"] and trapop or
        g_cgopt["wrapv"] and wrapop or
        assert(defaultop)

    if (#theop <= 2) then
        return thecmd / ("%1=%1"..theop.."%2")
    else
        return thecmd / ("%1="..theop.."(%1,%2)")
    end
end

function Op.varf(...)
    return Op.var_common(Op.var, ...)
end

function Op.varvarf(...)
    return Op.var_common(Op.varvar, ...)
end

-- Allow nesting... stuff like
--   ifvarl actorvar[sprite[THISACTOR].owner].burning 0
-- is kinda breaking the classic "no array nesting" rules
-- (if there ever were any) but making our life harder else.
local arraypat = sp0 * "[" * sp0 * tok.rvar * sp0 * "]"

-- Table of various patterns that are (parts of) more complex inner commands.
local patt = {}

-- Have to bite the bullet here and list actor/player members with second
-- parameters, even though it's ugly to make it part of the syntax.  Also,
-- stuff like
--   actor[xxx].loogiex parm2 x
-- will be wrongly accepted at the parsing stage (loogiex is player's member)
-- because we don't discriminate between actor and player here.
patt.parm2member = lpeg.C(Pat("htg_t") + "loogiex" + "loogiey" + "ammo_amount" +
                          "weaprecs" + "gotweapon" + "pals" + "max_ammo_amount") * sp1 * tok.rvar

-- The member name must match keywords, too (_all), because e.g. cstat is a
-- member of sprite[].
patt.bothmember = sp0 * "." * sp0 * lpeg.Ct(patt.parm2member + tok.identifier_all)
patt.singlemember = sp0 * "." * sp0 * tok.identifier_all

patt.cmdgetstruct =  -- get<structname>[<idx>].<member> (<parm2>)? <<var>>
    arraypat * patt.bothmember * sp1 * tok.wvar

patt.cmdsetstruct =  -- set<structname>[<idx>].<<member>> (<parm2>)? <var>
    arraypat * patt.bothmember * sp1 * tok.rvar

patt.cmdgetperxvar =  -- get<actor/player>var[<idx>].<varname> <<var>>
    arraypat * patt.singlemember * sp1 * tok.wvar

patt.cmdsetperxvar = -- set<actor/player>var[<idx>].<<varname>> <var>
    arraypat * patt.singlemember * sp1 * tok.rvar

-- Function generating code for a struct read/write access.
local function StructAccess(Structname, writep, index, membertab)
    assert(type(membertab)=="table")
    -- Lowercase the member name for CON compatibility
    local member, parm2 = membertab[1]:lower(), membertab[2]

    local MemberCode = conl.StructAccessCode[Structname] or conl.StructAccessCode2[Structname]
    -- Look up array+member name first, e.g. "spriteext[%s].angoff".
    local armembcode = MemberCode[member]
    if (armembcode == nil) then
        errprintf("%s: invalid %s member `.%s'", g_lastkw, Structname, member)
        return "_MEMBINVALID"
    end

    if (type(armembcode)=="table") then
        -- Read and write accesses differ.
        armembcode = armembcode[writep and 2 or 1]
        if (armembcode==nil) then
            errprintf("%s access to %s[].%s is not available",
                      writep and "write" or "read", Structname, member)
            return "_MEMBNOACCESS"
        end
    end

    if (Structname~="userdef") then
        -- Count number of parameters ("%s"), don't count "%%s".
        local _, numparms = armembcode:gsub("[^%%]%%s", "", 2)
        if (#membertab ~= numparms) then
            local nums = { "one", "two" }
            errprintf("%s[].%s has %s parameter%s, but %s given", Structname,
                      member, nums[numparms], numparms==1 and "" or "s",
                      nums[#membertab])
            return "_MEMBINVPARM"
        end
    end

    -- THISACTOR special meanings
    if (Structname=="player" or Structname=="input") then
        index = thisactor_to_pli(index)
    elseif (Structname=="sector") then
        if (index=="_aci") then
            index = SPS".sectnum"
        end
    end

    -- METHOD_MEMBER
    local ismethod = (armembcode:find("%%s",1,true)~=nil)
    -- If ismethod is true, then the formatted string will now have an "%s"
    local code

    if (Structname=="userdef") then
--        assert(index==nil)
        assert(parm2==nil)
        code = format(armembcode, parm2)
    else
        code = format(armembcode, index, parm2)
    end

    if (csapp()) then
        if (Structname=="player") then
            code = code:gsub("^player%[_pli%]", "_ps")
        elseif (Structname=="sprite") then
            code = code:gsub("^actor%[_aci%]", "_a")
            code = code:gsub("^sprite%[_aci%]", "_spr")
        end
    end

    return code, ismethod
end

function lookup.array_expr(writep, structname, index, membertab)
    if (conl.StructAccessCode[structname] == nil) then
        -- Try a gamearray
        local ganame = g_gamearray[structname] and lookup.gamearray(structname)
        if (ganame == nil) then
            if (structname=="actorvar") then
                -- actorvar[] inline array expr
                -- XXX: kind of CODEDUP with GetOrSetPerxvarCmd() factory
                local gv = g_gamevar[structname]
                if (gv and bit.band(gv.flags, GVFLAG.PERX_MASK)~=GVFLAG.PERACTOR) then
                    errprintf("gamevar `%s' is not per-actor", structname, "actor")
                end

                if (membertab == nil) then
                    errprintf("actorvar[] requires a pseudo member (gamevar) name")
                    return "_INVALIDAV"
                end

                if (#membertab > 1) then
                    errprintf("actorvar[] cannot be used with a second parameter")
                    return "_INVALIDAV"
                end

                assert(#membertab == 1)
                return lookup.gamevar(membertab[1], index, writep)
            end

            errprintf("symbol `%s' is neither a struct nor a gamearray", structname)
            return "_INVALIDAR"
        end

        if (membertab ~= nil) then
            errprintf("gamearrays cannot be indexed with member names")
            return "_INVALIDAR"
        end

        assert(type(ganame)=="string")
        return format("%s[%s]", ganame, index)
    end

    local membercode, ismethod = StructAccess(structname, writep, index, membertab)
    -- Written METHOD_MEMBER syntax not supported as "qwe:method(asd) = val"
    -- isn't valid Lua syntax.
    assert(not (writep and ismethod))
    return membercode
end

local Access =
{
    sector = function(...) return StructAccess("sector", ...) end,
    wall = function(...) return StructAccess("wall", ...) end,
    xsprite = function(...) return StructAccess("sprite", ...) end,
    player = function(...) return StructAccess("player", ...) end,

    tspr = function(...) return StructAccess("tspr", ...) end,
    projectile = function(...) return StructAccess("projectile", ...) end,
    thisprojectile = function(...) return StructAccess("thisprojectile", ...) end,
    userdef = function(...) return StructAccess("userdef", ...) end,
    input = function(...) return StructAccess("input", ...) end,
}

local function GetStructCmd(accessfunc, pattern)
    return (pattern or patt.cmdgetstruct) /
      function(idx, memb, var)
        return format("%s=%s", var, accessfunc(false, idx, memb))
      end
end

local function SetStructCmd(accessfunc, pattern)
    local function capfunc(idx, memb, var)
        local membercode, ismethod = accessfunc(true, idx, memb)
--[[
        local member = memb[1]:lower()
        if (type(var)=="number" and var<0 and member:match("picnum")) then
            warnprintf("member '.%s' is set to a negative value", member)
        end
--]]
        if (ismethod) then
            -- METHOD_MEMBER syntax

            -- BE EXTRA CAREFUL! We must be sure that percent characters have
            -- not been smuggled into the member code string via variable names
            -- etc.
            local _, numpercents = membercode:gsub("%%", "", 2)
            assert(numpercents==1)

            return format(membercode, var)
        else
            return format("%s=%s", membercode, var)
        end
    end

    return (pattern or patt.cmdsetstruct) / capfunc
end

-- <Setp>: whether the perxvar is set
local function GetOrSetPerxvarCmd(Setp, Actorp)
    local EXPECTED_PERX_BIT = Actorp and GVFLAG.PERACTOR or GVFLAG.PERPLAYER
    local pattern = (Setp and patt.cmdsetperxvar or patt.cmdgetperxvar)

    local function capfunc(idx, perxvarname, var)
        local gv = g_gamevar[perxvarname]
        if (gv and bit.band(gv.flags, GVFLAG.PERX_MASK)~=EXPECTED_PERX_BIT) then
            -- [gs]set*var for wrong gamevar type. See if it's a getactorvar,
            -- in which case we may only warn and access that instead. Note
            -- that accesses of player gamevars with actor indices are usually
            -- meaningless.
            local warnp = not Setp and Actorp and not g_warn["error-bad-getactorvar"]
            local xprintf = warnp and warnprintf or errprintf

            xprintf("gamevar `%s' is not per-%s", perxvarname, Actorp and "actor" or "player")
        end

        if (not Actorp) then
            -- THISACTOR -> player index for {g,s}etplayervar
            idx = thisactor_to_pli(idx)
        end

        if (Setp) then
            return format("%s=%s", lookup.gamevar(perxvarname, idx, true), var)
        else
            return format("%s=%s", var, lookup.gamevar(perxvarname, idx, false))
        end
    end

    return pattern / capfunc
end


local function n_s_fmt(n)
    return string.rep("%s,", n-1).."%s"
end

-- Various inner command handling functions / string capture strings.
local handle =
{
    NYI = function()
        errprintf("command `%s' not yet implemented", g_lastkw)
        return ""
    end,

    dynNYI = function()
        return format([[print(%q..":%d: `%s' not yet implemented")]],
                      g_filename, getlinecol(g_lastkwpos), g_lastkw)
    end,

    addlog = function()
        return format("print(%q..':%d: addlog')", g_filename, getlinecol(g_lastkwpos))
    end,

    addlogvar = function(val)
        return format("printf(%q..':%d: addlogvar %%s', %s)", g_filename, getlinecol(g_lastkwpos), val)
    end,

    debug = function(val)
        return format("print(%q..':%d: debug %d')", g_filename, getlinecol(g_lastkwpos), val)
    end,

    getzrange = function(...)
        local v = {...}
        assert(#v == 10)  -- 4R 4W 2R
        return format("%s,%s,%s,%s=_con._getzrange(%s,%s,%s,%s,%s,%s)",
                      v[5], v[6], v[7], v[8],  -- outargs
                      v[1], v[2], v[3], v[4], v[9], v[10])  -- inargs
    end,

    hitscan = function(...)
        local v = {...}
        assert(#v == 14)  -- 7R 6W 1R
        local vals = {
            v[8], v[9], v[10], v[11], v[12], v[13],  -- outargs
            v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[14]  -- inargs
        }
        return format("%s,%s,%s,%s,%s,%s=_con._hitscan(%s,%s,%s,%s,%s,%s,%s,%s)",
                     unpack(vals))
    end,

    neartag = function(...)
        local v = {...}
        assert(#v == 11)  -- 5R 4W 2R
        local vals = {
            v[6], v[7], v[8], v[9],  -- outargs
            v[1], v[2], v[3], v[4], v[5], v[10], v[11]  -- inargs
        }
        return format("%s,%s,%s,%s=_con._neartag(%s,%s,%s,%s,%s,%s,%s)",
                      unpack(vals))
    end,

    clipmove = function(noslidep, ...)
        local v = {...}
        assert(#v == 11)  -- 3W 1R 1W 6R
        local vals = {
            v[1], v[2], v[3], v[5],  -- outargs
            v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], v[11],  -- inargs
            noslidep
        }
        return format("%s,%s,%s,%s=_con._clipmovex("..n_s_fmt(11)..")",
                     unpack(vals))
    end,

    palfrom = function(...)
        local v = {...}
        return format(PLS":_palfrom(%d,%d,%d,%d)",
                      v[1] or 0, v[2] or 0, v[3] or 0, v[4] or 0)
    end,

    qsprintf = function(qdst, qsrc, ...)
        local codes = {...}
        return format("_con._qsprintf(%s,%s%s%s)", qdst, qsrc,
                      #codes>0 and "," or "", table.concat(codes, ','))
    end,

    move = function(mv, ...)
        local flags = {...}
        return format(ACS":set_move(%s,%d)", mv, (flags[1] and bit.bor(...)) or 0)
    end,

    rotatesprite = function(...)
        return format("_con._rotspr(%s,%s,%s,%s,%s,%s,%s,%s,0,%s,%s,%s,%s)", ...)
    end,

    rotatesprite16 = function(...)  -- (orientation|ROTATESPRITE_FULL16)
        return format("_con._rotspr(%s,%s,%s,%s,%s,%s,%s,_bor(%s,2048),0,%s,%s,%s,%s)", ...)
    end,

    rotatespritea = function(...)
        return format("_con._rotspr(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ...)
    end,

    -- <fmt>: format string, number of %s's must match number of varargs
    -- <umask>: number, if 2^i is set, arg '%i' must not be a system gamearray
    arraycmd = function(fmt, dstargi, ...)
        local args = {...}
        if (issysgar(args[dstargi])) then
            errprintf("%s: system gamearray not supported", g_lastkw)
        end
        return format(fmt, ...)
    end,

    -- readgamevar or savegamevar
    RSgamevar = function(identifier, dosave)
        -- check identifier for sanity
        if (not identifier:match("^[A-Za-z][A-Za-z0-9_%-]*$")) then
            errprintf("%s: bad identifier `%s' for config file persistence",
                      g_lastkw, identifier)
            return "_BADRSGV()"
        end

        local gv = g_gamevar[identifier]
        if (gv == nil) then
            return lookup.error_not_gamevar(identifier)
        end

        -- For per-actor or per-player gamevars, the value at the current actor or
        -- player index gets saved / loaded.
        local gvkind = bit.band(gv.flags, GVFLAG.PERX_MASK)
        local index = (gvkind==GVFLAG.PERACTOR) and "_aci" or
            (gvkind==GVFLAG.PERPLAYER) and "_pli" or nil

        -- NOTE: more strict than C-CON: we require the gamevar being writable
        -- even if we're saving it.
        local code = lookup.gamevar(identifier, index, true)

        if (dosave) then
            return format("_con._savegamevar(%q,%s)", identifier, code)
        else
            return format("%s=_con._readgamevar(%q)", code, identifier)
        end
    end,

    state = function(statename)
        if (g_funcname[statename]==nil) then
            local warn = not g_cgopt["error-nostate"]
            local xprintf = warn and warnprintf or errprintf

            xprintf("state `%s' not found.", statename)
            return warn and "" or "_NULLSTATE()"
        end
        return format("%s(_aci,_pli,_dist)", g_funcname[statename])
    end,

    addweapon = format("if (%s) then _con.longjmp() end", PLS":addweapon(%1,%2)"),

    -- Sound commands
    sound = "_con._sound(_aci,%1)",
    globalsound = "_con._globalsound(_pli,%1)",
    stopsound = "_con._stopsound(_aci,%1)",
    soundonce = "_con._soundonce(_aci,%1)",
}

local userdef_common_pat = (arraypat + sp1)/{} * lpeg.Cc(0) * lpeg.Ct(patt.singlemember) * sp1

-- NOTE about prefixes: most is handled by all_alt_pattern(), however commands
-- that have no arguments and that are prefixes of other commands MUST be
-- suffixed with a "* #sp1" pattern.

local Cinner = {
    -- these can appear anywhere in the script
    ["break"] = cmd()
        / function()
              return g_isWhile[#g_isWhile]
                  and format("goto l%d", #g_isWhile)
                  or "do return end"
          end,
    ["return"] = cmd()  -- NLCF
        / "_con.longjmp()",

    state = cmd(I)
        / handle.state,

    --- 1. get*, set*
    getsector = GetStructCmd(Access.sector),
    getwall = GetStructCmd(Access.wall),
    getactor = GetStructCmd(Access.xsprite),
    getplayer = GetStructCmd(Access.player),

    getinput = GetStructCmd(Access.input),
    getprojectile = GetStructCmd(Access.projectile),
    getthisprojectile = GetStructCmd(Access.thisprojectile),
    gettspr = GetStructCmd(Access.tspr),
    -- NOTE: {get,set}userdef is the only struct that can be accessed without
    -- an "array part", e.g.  H266MOD has "setuserdef .weaponswitch 0" (space
    -- between keyword and "." is mandatory).
    -- NOTE2: userdef has at least three members with a second parameter:
    -- user_name, ridecule, savegame. Then there's wchoice. Given that they're
    -- arrays, I highly doubt that they worked (much less were safe) in CON.
    -- We disallow them, recent EDuke32 versions didn't expose them either.
    getuserdef = GetStructCmd(Access.userdef, userdef_common_pat * tok.wvar),

    getplayervar = GetOrSetPerxvarCmd(false, false),  -- THISACTOR
    getactorvar = GetOrSetPerxvarCmd(false, true),

    setsector = SetStructCmd(Access.sector),
    setwall = SetStructCmd(Access.wall),
    setactor = SetStructCmd(Access.xsprite),
    setplayer = SetStructCmd(Access.player),

    setinput = SetStructCmd(Access.input),
    setprojectile = SetStructCmd(Access.projectile),
    setthisprojectile = SetStructCmd(Access.thisprojectile),
    settspr = SetStructCmd(Access.tspr),
    setuserdef = SetStructCmd(Access.userdef, userdef_common_pat * tok.rvar),

    setplayervar = GetOrSetPerxvarCmd(true, false),  -- THISACTOR
    setactorvar = GetOrSetPerxvarCmd(true, true),

    setvarvar = Op.varvar / "%1=%2",
    addvarvar = Op.varvarf "+",
    -- NOTE the space after the minus sign so that e.g. "subvar x -1" won't get
    -- translated to "x=x--1" (-- being the Lua line comment start).
    subvarvar = Op.varvarf "- ",
    mulvarvar = Op.varvarf("*", "_mulTR", "_mulWR"),
    divvarvar = Op.varvarf "_div",
    modvarvar = Op.varvarf "_mod",
    andvarvar = Op.varvarf "_band",
    orvarvar = Op.varvarf "_bor",
    xorvarvar = Op.varvarf "_bxor",
    randvarvar = Op.varvar / "%1=_con._rand(%2)",

    setvar = Op.var / "%1=%2",
    addvar = Op.varf "+",
    subvar = Op.varf "- ",
    mulvar = Op.varf("*", "_mulTR", "_mulWR"),
    divvar = Op.varf "_div",
    modvar = Op.varf "_mod",
    andvar = Op.varf "_band",
    orvar = Op.varf "_bor",
    xorvar = Op.varf "_bxor",
    randvar = Op.var / "%1=_con._rand(%2)",
    shiftvarl = Op.varf "_lsh",
    shiftvarr = Op.varf "_arsh",

    --- 2. Math operations
    sqrt = cmd(R,W)
        / "%2=_gv.ksqrt(%1)",
    calchypotenuse = cmd(W,R,R)
        / "%1=_con._hypot(%2,%3)",
    sin = cmd(W,R)
        / "%1=_xmath.ksin(%2)",
    cos = cmd(W,R)
        / "%1=_xmath.kcos(%2)",
    mulscale = cmd(W,R,R,R)
        / "%1=_gv.Mulscale(%2,%3,%4)",
    getangle = cmd(W,R,R)
        / "%1=_gv.getangle(%2,%3)",
    getincangle = cmd(W,R,R)
        / "%1=_con._angdiff(%2,%3)",

    --- 3. Actors
    action = cmd(AC)
        / ACS":set_action(%1)",
    ai = cmd(AI)
        / ACS":set_ai(%1)",
    move = sp1 * tok.move * (sp1 * tok.define)^0
        / handle.move,

    cactor = cmd(D)
        / SPS":set_picnum(%1)",
    count = cmd(D)
        / ACS":set_count(%1)",
    cstator = cmd(D)
        / (SPS".cstat=_bor(%1,"..SPS".cstat)"),
    cstat = cmd(D)
        / SPS".cstat=%1",
    clipdist = cmd(D)
        / SPS".clipdist=%1",
    shadeto = cmd(D) /  -- PRE13
        "",
    sizeto = cmd(D,D)
        / "_con._sizeto(_aci,%1,%2)",  -- TODO: see control.lua:_sizeto
    sizeat = cmd(D,D)
        / (SPS".xrepeat,"..SPS".yrepeat=%1,%2"),
    strength = cmd(D)
        / SPS".extra=%1",
    addstrength = cmd(D)
        / (SPS".extra="..SPS".extra+%1"),
    spritepal = cmd(D)
        / SPS".pal=%1",

    hitradius = cmd(D,D,D,D,D)
        / "_con._A_RadiusDamage(_aci,%1,%2,%3,%4,%5)",
    hitradiusvar = cmd(R,R,R,R,R)
        / "_con._A_RadiusDamage(_aci,%1,%2,%3,%4,%5)",

    -- some commands taking read vars
    operaterespawns = cmd(R)
        / "_con._G_OperateRespawns(%1)",
    operatemasterswitches = cmd(R)
        / "_con._G_OperateMasterSwitches(%1)",
    checkactivatormotion = cmd(R)
        / "_gv.RETURN=_con._checkactivatormotion(%1)",
    time = cmd(R)  -- no-op
        / "",
    inittimer = cmd(R)
        / "_con._inittimer(%1)",
    lockplayer = cmd(R)
        / PLS".transporter_hold=%1",
    quake = cmd(R)
        / "_gv.doQuake(%1,81)",  -- TODO: dynsound (EARTHQUAKE)
    jump = cmd(R)
        / handle.NYI,  -- will never be
    cmenu = cmd(R)
        / "_gv._changeMenu(%1)",
    checkavailweapon = cmd(R)  -- THISACTOR
        / function(pli)
              return format("_con._checkavailweapon(%s)", thisactor_to_pli(pli))
          end,
    checkavailinven = cmd(R)  -- THISACTOR
        / function(pli)
              return format("_con._selectnextinv(player[%s])", thisactor_to_pli(pli))
          end,
    guniqhudid = cmd(R)
        / "_gv._set_guniqhudid(%1)",
    echo = cmd(R)
        / "_con._echo(%1)",
    activatecheat = cmd(R)
        / handle.NYI,
    setgamepalette = cmd(R)
        / "_con._setgamepalette(_pli,%1)",

    -- Sound commands
    sound = cmd(D)
        / handle.sound,
    soundvar = cmd(R)
        / handle.sound,
    globalsound = cmd(D)
        / handle.globalsound,
    globalsoundvar = cmd(R)
        / handle.globalsound,
    stopsound = cmd(D)
        / handle.stopsound,
    stopsoundvar = cmd(R)
        / handle.stopsound,
    soundonce = cmd(D)
        / handle.soundonce,
    soundoncevar = cmd(R)
        / handle.soundonce,
    stopactorsound = cmd(R,R)
        / "_con._stopactorsound(%1,%2)",
    stopallsounds = cmd()
        / "_con._stopallsounds()",
    screensound = cmd(R)
        / "_con._screensound(%1)",
    mikesnd = cmd()
        / format("_con._soundonce(_aci,%s)", SPS".yvel"),
    setactorsoundpitch = cmd(R,R,R)
        / "_con._setactorsoundpitch(%1,%2,%3)",

    -- some commands taking defines
    addammo = cmd(D,D)  -- NLCF
        / format("if (%s) then _con.longjmp() end", PLS":addammo(%1,%2)"),
    addweapon = cmd(D,D)  -- NLCF
        / handle.addweapon,
    debris = cmd(D,D)
        / "_con._debris(_aci, %1, %2)",
    addinventory = cmd(D,D)
        / format("_con._addinventory(%s,%%1,%%2,_aci)", PLS""),
    guts = cmd(D,D)
        / "_con._A_DoGuts(_aci,%1,%2)",

    spawn = cmd(D)
        / "_con.spawn(%1,_aci)",
    espawn = cmd(D)
        / "_gv.RETURN=_con.spawn(%1,_aci)",
    espawnvar = cmd(R)
        / "_gv.RETURN=_con.spawn(%1,_aci)",
    qspawn = cmd(D)
        / "_con.spawn(%1,_aci,true)",
    qspawnvar = cmd(R)
        / "_con.spawn(%1,_aci,true)",
    eqspawn = cmd(D)
        / "_gv.RETURN=_con.spawn(%1,_aci,true)",
    eqspawnvar = cmd(R)
        / "_gv.RETURN=_con.spawn(%1,_aci,true)",

    angoff = cmd(D)
        / "spriteext[_aci].angoff=%1",
    angoffvar = cmd(R)
        / "spriteext[_aci].angoff=%1",

    -- cont'd
    addkills = cmd(D)
        / (PLS".actors_killed="..PLS".actors_killed+%1;"..ACS".stayputsect=-1"),
    addphealth = cmd(D)
        / format("_con._addphealth(%s,_aci,%%1)", PLS""),
    debug = cmd(D)
        / handle.debug,
    endofgame = cmd(D)
        / "_con._endofgame(_pli,%1)",
    endoflevel = cmd(D)  -- PRE13
        / "_con._endofgame(_pli,%1)",
    lotsofglass = cmd(D)
        / "_con._A_SpawnGlass(_aci,%1)",
    mail = cmd(D)
        / "_con._spawnmany(_aci,'MAIL',%1)",
    money = cmd(D)
        / "_con._spawnmany(_aci,'MONEY',%1)",
    paper = cmd(D)
        / "_con._spawnmany(_aci,'PAPER',%1)",
    sleeptime = cmd(D)
        / ACS".timetosleep=%1",

    eshoot = cmd(D)
        / "_gv.RETURN=_con._shoot(_aci,%1)",
    eshootvar = cmd(R)
        / "_gv.RETURN=_con._shoot(_aci,%1)",
    ezshoot = cmd(R,D)
        / "_gv.RETURN=_con._shoot(_aci,%2,%1)",
    ezshootvar = cmd(R,R)
        / "_gv.RETURN=_con._shoot(_aci,%2,%1)",
    shoot = cmd(D)
        / "_con._shoot(_aci,%1)",
    shootvar = cmd(R)
        / "_con._shoot(_aci,%1)",
    zshoot = cmd(R,D)
        / "_con._shoot(_aci,%2,%1)",
    zshootvar = cmd(R,R)
        / "_con._shoot(_aci,%2,%1)",

    fall = cmd()
        / "actor.fall(_aci)",
    flash = cmd()
        / format("_con._flash(%s,%s)", SPS"", PLS""),
    getlastpal = cmd()
        / "_con._getlastpal(_aci)",
    insertspriteq = cmd()
        / "_con._addtodelqueue(_aci)",
    killit = cmd()  -- NLCF
        / "_con.killit()",
    nullop = cmd()
        / "",  -- NOTE: really generate no code
    pkick = cmd()
        / format("_con._pkick(%s,%s)", PLS"", ACS""),
    pstomp = cmd()
        / PLS":stomp(_aci)",
    resetactioncount = cmd()
        / ACS":reset_acount()",
    resetcount = cmd()
        / ACS":set_count(0)",
    resetplayer = cmd()  -- NLCF
        / "if (_con._VM_ResetPlayer2(_pli,_aci)) then _con.longjmp() end",
    respawnhitag = cmd()
        / format("_con._respawnhitag(%s)", SPS""),
    tip = cmd()
        / PLS".tipincs=26",
    tossweapon = cmd()
        / "_con._tossweapon(_pli)",
    wackplayer = cmd()
        / PLS":whack()",

    -- player/sprite searching
    findplayer = cmd(W)
        / "_gv.RETURN,%1=_con._findplayer(_pli,_aci)",  -- player index, distance
    findotherplayer = cmd(W)
        / "_gv.RETURN,%1=0,0x7fffffff",  -- TODO_MP
    findnearspritezvar = cmd(D,R,R,W)
        / "%4=_con._findnear(_aci,true,'z',%1,%2,%3)",
    findnearspritez = cmd(D,D,D,W)
        / "%4=_con._findnear(_aci,true,'z',%1,%2,%3)",
    findnearsprite3dvar = cmd(D,R,W)
        / "%3=_con._findnear(_aci,true,'d3',%1,%2)",
    findnearsprite3d = cmd(D,D,W)
        / "%3=_con._findnear(_aci,true,'d3',%1,%2)",
    findnearspritevar = cmd(D,R,W)
        / "%3=_con._findnear(_aci,true,'d2',%1,%2)",
    findnearsprite = cmd(D,D,W)
        / "%3=_con._findnear(_aci,true,'d2',%1,%2)",
    findnearactorzvar = cmd(D,R,R,W)
        / "%4=_con._findnear(_aci,false,'z',%1,%2,%3)",
    findnearactorz = cmd(D,D,D,W)
        / "%4=_con._findnear(_aci,false,'z',%1,%2,%3)",
    findnearactor3dvar = cmd(D,R,W)
        / "%3=_con._findnear(_aci,false,'d3',%1,%2)",
    findnearactor3d = cmd(D,D,W)
        / "%3=_con._findnear(_aci,false,'d3',%1,%2)",
    findnearactorvar = cmd(D,R,W)
        / "%3=_con._findnear(_aci,false,'d2',%1,%2)",
    findnearactor = cmd(D,D,W)
        / "%3=_con._findnear(_aci,false,'d2',%1,%2)",

    -- quotes
    qsprintf = sp1 * tok.rvar * sp1 * tok.rvar * (sp1 * tok.rvar)^-32
        / handle.qsprintf,
    qgetsysstr = cmd(R,R)
        / "_con._qgetsysstr(%1,%2,_pli)",
    qstrcat = cmd(R,R)
        / "_con._qstrcat(%1,%2)",
    qstrcpy = cmd(R,R)
        / "_con._qstrcpy(%1,%2)",
    qstrlen = cmd(W,R)
        / "%1=_con._qstrlen(%2)",
    qstrncat = cmd(R,R)
        / handle.NYI,
    qsubstr = cmd(R,R,R,R)
        / handle.NYI,
    quote = cmd(D)
        / "_con._quote(_pli,%1)",
    userquote = cmd(R)
        / "_con._userquote(%1)",
    getkeyname = cmd(R,R,R)
        / "_con._getkeyname(%1,%2,%3)",
    getpname = cmd(R,R)  -- THISACTOR
        / function(qnum, pli)
              return format("_con._getpname(%s,%s)", qnum, thisactor_to_pli(pli))
          end,

    -- array stuff
    copy = sp1 * tok.gamearray * arraypat * sp1 * tok.gamearray * arraypat * sp1 * tok.rvar
        / function(...) return handle.arraycmd("_con._gar_copy(%s,%s,%s,%s,%s)", 3, ...) end,
    setarray = sp1 * tok.gamearray * arraypat * sp1 * tok.rvar
        / function(...) return handle.arraycmd("%s[%s]=%s", 1, ...) end,
    resizearray = cmd(GARI,R)
        / function(...) return handle.arraycmd("%s:resize(%s)", 1, ...) end,
    getarraysize = cmd(GARI,W)
        / function(ar, dst)
              return format("%s=%s", dst, issysgar(ar) and tostring(C.MAXTILES) or ar.."._size")
          end,
    readarrayfromfile = cmd(GARI,D)
        / function(...)  -- false: error on no file, nil: don't.
              return handle.arraycmd("%s:read(%s,nil)", 1, ...)
          end,
    writearraytofile = cmd(GARI,D)
        / function(...)
              return handle.arraycmd("%s:write(%s)", 1, ...)
          end,

    -- Persistence
    clearmapstate = cmd(R)
        / handle.NYI,
    loadmapstate = cmd()
        / "_con._loadmapstate()",
    savemapstate = cmd()
        / "_con._savemapstate()",
    savegamevar = cmd(I)
        / function(id) return handle.RSgamevar(id, true) end,
    readgamevar = cmd(I)
        / function(id) return handle.RSgamevar(id, false) end,
    savenn = cmd(D)
        / handle.dynNYI,
    save = cmd(D)
        / handle.dynNYI,

    addlogvar = cmd(R)
        / handle.addlogvar,
    addlog = cmd() * #sp1
        / handle.addlog,
    addweaponvar = cmd(R,R)  -- NLCF
        / handle.addweapon,
    cansee = cmd(R,R,R,R,R,R,R,R,W)
        / "%9=cansee(_IV(1,%1,%2,%3),%4, _IV(2,%5,%6,%7),%8) and 1 or 0",
    canseespr = cmd(R,R,W)
        / "%3=_con._canseespr(%1,%2)",
    changespritesect = cmd(R,R)
        / "sprite.changesect(%1,%2,true)",
    changespritestat = cmd(R,R)
        / "sprite.changestat(%1,%2,true)",
    displayrand = cmd(W)
        / "%1=_con._displayrand(32767)",
    displayrandvar = cmd(W,D)
        / "%1=_con._displayrand(%2)",
    displayrandvarvar = cmd(W,R)
        / "%1=_con._displayrand(%2)",
    dist = cmd(W,R,R)
        / "%1=_xmath.dist(sprite[%2],sprite[%3])",
    ldist = cmd(W,R,R)
        / "%1=_xmath.ldist(sprite[%2],sprite[%3])",
    dragpoint = cmd(R,R,R)
        / "wall.dragto(%1,_IV(1,%2,%3,0))",
    rotatepoint = cmd(R,R,R,R,R,W,W)
        / "%6,%7=_con._rotatepoint(%1,%2,%3,%4,%5)",

    -- collision detection etc.
    hitscan = cmd(R,R,R,R,R,R,R,W,W,W,W,W,W,R)  -- 7R 6W 1R
        / handle.hitscan,
    clipmove = cmd(W,W,W,R,W,R,R,R,R,R,R)  -- 3W 1R 1W 6R
        / function(...) return handle.clipmove(0, ...) end,
    clipmovenoslide = cmd(W,W,W,R,W,R,R,R,R,R,R)  -- 3W 1R 1W 6R
        / function(...) return handle.clipmove(1, ...) end,
    lineintersect = cmd(R,R,R,R,R,R,R,R,R,R,W,W,W,W)  -- 10R 4W
        / handle.NYI,
    rayintersect = cmd(R,R,R,R,R,R,R,R,R,R,W,W,W,W)  -- 10R 4W
        / handle.NYI,
    movesprite = cmd(R,R,R,R,R,W)
        / "%6=_con._movesprite(%1,%2,%3,%4,%5)",
    neartag = cmd(R,R,R,R,R,W,W,W,W,R,R)  -- 5R 4W 2R
        / handle.neartag,
    getzrange = cmd(R,R,R,R,W,W,W,W,R,R)
        / handle.getzrange,

    -- screen text and numbers display
    qstrdim = cmd(W,W,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R)  -- 2W 16R
        / function(...)
              return format("%s,%s=_con._qstrdim("..n_s_fmt(16)..")", ...)
          end,
    screentext = cmd(R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R)  -- 20 R
        / function(...)
              return format("_con._screentext("..n_s_fmt(20)..")", ...)
          end,
    gametext = cmd(R,R,R,R,R,R,R,R,R,R,R)  -- 11 R
        / function(...)
              return format("_con._gametext("..n_s_fmt(11)..",65536)", ...)
          end,
    gametextz = cmd(R,R,R,R,R,R,R,R,R,R,R,R)  -- 12 R
        / function(...)
              return format("_con._gametext("..n_s_fmt(12)..")", ...)
          end,
    digitalnumber = cmd(R,R,R,R,R,R,R,R,R,R,R)  -- 11R
        / function(...)
              return format("_con._digitalnumber("..n_s_fmt(11)..",65536)", ...)
          end,
    digitalnumberz = cmd(R,R,R,R,R,R,R,R,R,R,R,R)  -- 12R
        / function(...)
              return format("_con._digitalnumber("..n_s_fmt(12)..")", ...)
          end,
    minitext = cmd(R,R,R,R,R)
        / "_con._minitext(%1,%2,%3,%4,%5)",

    palfrom = (sp1 * tok.define)^-4
        / handle.palfrom,

    activatebysector = cmd(R,R)
        / "_con._activatebysector(%1,%2)",
    activate = cmd(D)  -- PRE13, THISACTOR already translated to cur. player
        / "_con._operateactivators(%d,_pli)",
    operateactivators = cmd(R,R)  -- THISACTOR
        / function(tag, pli)
              return format("_con._operateactivators(%s,%s)", tag, thisactor_to_pli(pli))
          end,
    operatesectors = cmd(R,R)
        / "_con._operatesectors(%1,%2)",
    operate = cmd() * #sp1
        / "_con._operate(_aci)",

    myos = cmd(R,R,R,R,R)
        / "_con._myos(%1,%2,65536,%3,%4,%5)",
    myosx = cmd(R,R,R,R,R)
        / "_con._myos(%1,%2,32768,%3,%4,%5)",
    myospal = cmd(R,R,R,R,R,R)
        / "_con._myos(%1,%2,65536,%3,%4,%5,%6)",
    myospalx = cmd(R,R,R,R,R,R)
        / "_con._myos(%1,%2,32768,%3,%4,%5,%6)",

    headspritesect = cmd(W,R)
        / "%1=sprite._headspritesect[%2]",
    headspritestat = cmd(W,R)
        / "%1=sprite._headspritestat[%2]",
    nextspritesect = cmd(W,R)
        / "%1=sprite._nextspritesect[%2]",
    nextspritestat = cmd(W,R)
        / "%1=sprite._nextspritestat[%2]",
    prevspritesect = cmd(W,R)
        / "%1=sprite._prevspritesect[%2]",
    prevspritestat = cmd(W,R)
        / "%1=sprite._prevspritestat[%2]",

    -- NOTE: Yup, it's also an inner command. Do this one concession to
    -- cleanness for backward compatibility (e.g. Sonic3D v0.3).
    definequote = sp1 * tok.define * newline_term_string
        / Cmd.definequote,

    redefinequote = sp1 * tok.define * newline_term_string
        / function(qnum, qstr) return format("_con._definequote(%d,%q)", qnum, stripws(qstr)) end,
    rotatesprite = cmd(R,R,R,R,R,R,R,R,R,R,R,R)  -- 12R
        / handle.rotatesprite,
    rotatesprite16 = cmd(R,R,R,R,R,R,R,R,R,R,R,R)  -- 12R
        / handle.rotatesprite16,
    rotatespritea = cmd(R,R,R,R,R,R,R,R,R,R,R,R,R)  -- 13R
        / handle.rotatespritea,
    sectorofwall = cmd(W,R,R)
        / handle.NYI,
    sectclearinterpolation = cmd(R)
        / "_con._togglesectinterp(%1,0)",
    sectsetinterpolation = cmd(R)
        / "_con._togglesectinterp(%1,1)",

    sectgethitag = cmd()
        / (CSV".HITAG=sector["..SPS".sectnum].hitag"),
    sectgetlotag = cmd()
        / (CSV".LOTAG=sector["..SPS".sectnum].lotag"),
    spgethitag = cmd()
        / (CSV".HITAG="..SPS".hitag"),
    spgetlotag = cmd()
        / (CSV".LOTAG="..SPS".lotag"),
    gettextureceiling = cmd()
        / (CSV".TEXTURE=sector["..SPS".sectnum].ceilingpicnum"),
    gettexturefloor = cmd()
        / (CSV".TEXTURE=sector["..SPS".sectnum].floorpicnum"),

    startlevel = cmd(R,R)
        / "_con._startlevel(%1,%2)",
    starttrack = cmd(D)
        / "_con._starttrack(%1)",
    starttrackvar = cmd(R)
        / "_con._starttrack(%1)",

    setaspect = cmd(R,R)
        / "_con._setaspect(%1,%2)",
    showview = cmd(R,R,R,R,R,R,R,R,R,R)  -- 10R
        / function(...) return format("_con._showview("..n_s_fmt(10)..",0)", ...) end,
    showviewunbiased = cmd(R,R,R,R,R,R,R,R,R,R)  -- 10R
        / function(...) return format("_con._showview("..n_s_fmt(10)..",1)", ...) end,
    smaxammo = cmd(R,R)
        / PLS".max_ammo_amount[%1]=%2",
    gmaxammo = cmd(R,W)
        / ("%2="..PLS".max_ammo_amount[%1]"),
    spriteflags = cmd(R)  -- also see outer
        / ACS".flags=%1",
    ssp = cmd(R,R)
        / "_ssp(%1,%2)",
    setsprite = cmd(R,R,R,R)
        / "_setsprite(%1,_IV(1,%2,%3,%4))",
    updatesector = cmd(R,R,W)
        / format("%%3=updatesector(_IV(1,%%1,%%2,0),%s)", SPS".sectnum"),
    updatesectorz = cmd(R,R,R,W)
        / format("%%4=updatesectorz(_IV(1,%%1,%%2,%%3),%s)", SPS".sectnum"),

    getactorangle = cmd(W)
        / ("%1="..SPS".ang"),
    setactorangle = cmd(R)
        / SPS".ang=_band(%1,2047)",
    getplayerangle = cmd(W)
        / ("%1="..PLS".ang"),
    setplayerangle = cmd(R)
        / PLS".ang=_band(%1,2047)",
    getangletotarget = cmd(W)
        / "%1=_con._angtotarget(_aci)",

    getceilzofslope = cmd(R,R,R,W)
        / "%4=sector[%1]:ceilingzat(_IV(1,%2,%3,0))",
    getflorzofslope = cmd(R,R,R,W)
        / "%4=sector[%1]:floorzat(_IV(1,%2,%3,0))",
    getcurraddress = cmd(W)
        / handle.NYI,  -- will never be
    getticks = cmd(W)
        / "%1=_gv.getticks()",
    gettimedate = cmd(W,W,W,W,W,W,W,W)
        / "%1,%2,%3,%4,%5,%6,%7,%8=_con._gettimedate()",
}

local Cif = {
    ifai = cmd(AI)
        / ACS":has_ai(%1)",
    ifaction = cmd(AC)
        / ACS":has_action(%1)",
    ifmove = cmd(MV)
        / ACS":has_move(%1)",

    ifrnd = cmd(D)
        / "_con.rnd(%1)",
    ifpdistl = cmd(D)  -- DEFER
        / function(val) return { "_dist<"..val, nil, "_con._sleepcheck(_aci,_dist)" } end,
    ifpdistg = cmd(D)  -- DEFER
        / function(val) return { "_dist>"..val, nil, "_con._sleepcheck(_aci,_dist)" } end,
    ifactioncount = cmd(D)
        / ACS":get_acount()>=%1",
    ifcount = cmd(D)
        / ACS":get_count()>=%1",
    ifactor = cmd(D)
        / SPS".picnum==%1",
    ifstrength = cmd(D)
        / SPS".extra<=%1",
    ifspawnedby = cmd(D)
        / ACS".picnum==%1",
    ifwasweapon = cmd(D)
        / ACS".picnum==%1",
    ifgapzl = cmd(D)  -- factor into a con.* function?
        / format("_arsh(%s-%s,8)<%%1", ACS".floorz", ACS".ceilingz"),
    iffloordistl = cmd(D)
        / format("(%s-%s)<=256*%%1", ACS".floorz", SPS".z"),
    ifceilingdistl = cmd(D)
        / format("(%s-%s)<=256*%%1", SPS".z", ACS".ceilingz"),
    ifphealthl = cmd(D)
        / format("sprite[%s].extra<%%1", PLS".i"),
    ifplayersl = cmd(D)  -- PRE13
        / "1<%1",  -- TODO_MP
    ifspritepal = cmd(D)
        / SPS".pal==%1",
    ifgotweaponce = cmd(D)
        / "false",  -- TODO_MP
    ifangdiffl = cmd(D)
        / format("_con._angdiffabs(%s,%s)<=%%1", PLS".ang", SPS".ang"),
    ifsound = cmd(D)
        / "_con._soundplaying(_aci,%1)",
    ifpinventory = cmd(D,D)
        / format("_con._checkpinventory(%s,%%1,%%2,_aci)", PLS""),

    ifvarl = cmd(R,D)
        / "%1<%2",
    ifvarg = cmd(R,D)
        / "%1>%2",
    ifvare = cmd(R,D)
        / "%1==%2",
    ifvarn = cmd(R,D)
        / "%1~=%2",
    ifvarand = cmd(R,D)
        / "_band(%1,%2)~=0",
    ifvaror = cmd(R,D)
        / "_bor(%1,%2)~=0",
    ifvarxor = cmd(R,D)
        / "_bxor(%1,%2)~=0",
    ifvareither = cmd(R,D)
        / "%1~=0 or %2~=0",

    ifvarvarl = cmd(R,R)
        / "%1<%2",
    ifvarvarg = cmd(R,R)
        / "%1>%2",
    ifvarvare = cmd(R,R)
        / "%1==%2",
    ifvarvarn = cmd(R,R)
        / "%1~=%2",
    ifvarvarand = cmd(R,R)
        / "_band(%1,%2)~=0",
    ifvarvaror = cmd(R,R)
        / "_bor(%1,%2)~=0",
    ifvarvarxor = cmd(R,R)
        / "_bxor(%1,%2)~=0",
    ifvarvareither = cmd(R,R)
        / "%1~=0 or %2~=0",

    ifactorsound = cmd(R,R)
        / "_con._soundplaying(%1,%2)",

    ifp = (sp1 * tok.define)^1
        / function (...) return format("_con._ifp(%d,_pli,_aci)", bit.bor(...)) end,
    ifsquished = cmd()
        / "_con._squished(_aci,_pli)",
    ifserver = cmd()
        / "false",  -- TODO_MP
    ifrespawn = cmd()
        / format("_con._checkrespawn(%s)", SPS""),
    ifoutside = cmd()
        / format("_band(sector[%s].ceilingstat,1)~=0", SPS".sectnum"),
    ifonwater = cmd()
        / format("sector[%s].lotag==1 and _math.abs(%s-sector[%s].floorz)<32*256",
                 SPS".sectnum", SPS".z", SPS".sectnum"),
    ifnotmoving = cmd()
        / "_band(actor[_aci]._movflag,49152)>16384",
    ifnosounds = cmd()
        / "not _con._ianysound(_aci)",
    ifmultiplayer = cmd()
        / "false",  -- TODO_MP
    ifinwater = cmd()
        / format("sector[%s].lotag==2", SPS".sectnum"),
    ifinspace = cmd()
        / format("_con._checkspace(%s,false)", SPS".sectnum"),
    ifinouterspace = cmd()
        / format("_con._checkspace(%s,true)", SPS".sectnum"),
    ifhitweapon = cmd()
        / "_con._A_IncurDamage(_aci)>=0",
    ifhitspace = cmd()
        / "player.holdskey(_pli,'OPEN')",
    ifdead = cmd()
        / SPS".extra<=0",
    ifclient = cmd()
        / "false",  -- TODO_MP
    ifcanshoottarget = cmd()
        / "_con._canshoottarget(_dist,_aci)",
    ifcanseetarget = cmd()  -- DEFER -- XXX: 1536 is SLEEPTIME
        / function()
              return { format("_con._canseetarget(%s,%s)", SPS"", PLS""), ACS".timetosleep=1536" }
          end,
    ifcansee = cmd() * #sp1
        / format("_con._cansee(_aci,%s)", PLS""),
    ifbulletnear = cmd()
        / "_con._bulletnear(_aci)",
    ifawayfromwall = cmd()
        / format("_con._awayfromwall(%s,108)", SPS""),
    ifactornotstayput = cmd()
        / ACS".stayputsect==-1",
}


----==== Tracing and reporting ====----

-- g_newlineidxs will contain the 1-based file offsets to "\n" characters
local g_newlineidxs = {}

-- Returns index into the sorted table tab such that
--   tab[index] <= searchelt < tab[index+1].
-- Preconditions:
--  tab[i] < tab[i+1] for 0 <= i < #tab
--  tab[0] <= searchelt < tab[#tab]
-- If #tab is less than 2, returns 0. This plays nicely with newline index
-- tables like { [0]=0, [1]=len+1 }, e.g. if the file doesn't contain any.
local function bsearch(tab, searchelt)
--    printf("bsearch(tab, %d)", searchelt)
    local l, r = 0, #tab
    local i

    if (r < 2) then
        return 0
    end

    while (l ~= r) do
        i = l + math.ceil((r-l)/2)  -- l < i <= r
        assert(l < i and i <= r)
        local elt = tab[i]
--        printf("l=%d tab[%d]=%d r=%d", l, i, elt, r)

        if (searchelt == elt) then
            return i
        end

        if (searchelt < elt) then
            r = i-1
        else  -- (searchelt > elt)
            l = i
        end
    end

--    printf("return tab[%d]=%d", l, tab[l])
    return l
end

function getlinecol(pos)  -- local
    assert(type(pos)=="number")
    local line = bsearch(g_newlineidxs, pos)
    assert(line and g_newlineidxs[line]<=pos and pos<g_newlineidxs[line+1])
    local col = pos-g_newlineidxs[line]
    return line+1, col-1
end

-- A generic trace function, prints a position together with the match content.
-- The 'doit' parameter can be used to temporarily enable/disable a particular
-- tracing function.
local function TraceFunc(pat, label, doit)
    assert(doit ~= nil)
    pat = Pat(pat)

    if (doit) then
        local function tfunc(subj, pos, a)
            printf("%s:%s: %s", linecolstr(pos), label, a)
            return true
        end
        pat = lpeg.Cmt(pat, tfunc)
    elseif (label=="kw") then  -- HACK
        local function tfunc(subj, pos, a)
            g_lastkwpos = pos
            g_lastkw = a
            return true
        end
        -- XXX: is there a better way?
        pat = lpeg.Cmt(pat, tfunc)
    end

    return pat
end

local function BadIdent(pat)
    local function tfunc(subj, pos, a)
        if (g_warn["bad-identifier"] and not g_badids[a]) then
            warnprintf("bad identifier: %s", a)
            g_badids[a] = true
        end
        return true
    end
    return lpeg.Cmt(Pat(pat), tfunc)
end

-- These are tracers for specific patterns which can be disabled
-- if desired.
local function Keyw(kwname) return TraceFunc(kwname, "kw", false) end
--local function NotKeyw(text) return TraceFunc(text, "!kw", false) end
--local function Ident(idname) return TraceFunc(idname, "id", false) end
local function Stmt(cmdpat) return TraceFunc(cmdpat, "st", false) end

--local function Temp(kwname) return TraceFunc(kwname, "temp", true) end
--Cinner["myosx"] = Temp(Cinner["myosx"])

----==== Translator continued ====----
local function attachlinenum(capts, pos)
    capts[1] = capts[1].."--"..getlinecol(pos)
    return capts[1]
end

local function after_inner_cmd_Cmt(subj, pos, ...)
    if (g_numerrors == inf) then
        return nil
    end

    local capts = {...}
    assert(type(capts[1])=="string" and capts[2]==nil)
    return true, attachlinenum(capts, pos)
end

local function after_if_cmd_Cmt(subj, pos, ...)
    if (g_numerrors == inf) then
        return nil
    end

    local capts = {...}
    assert(capts[1] ~= nil)
    assert(#capts <= 3)

    for i=#capts,1, -1 do
        assert(type(capts[i])=="string" or type(capts[i])=="table")
    end

    -- IF_LINE_NUMBERING
    local firstistab = (type(capts[1])=="table")
    attachlinenum(firstistab and capts[1] or capts, pos)

    return true, unpack(capts)
end

local function after_cmd_Cmt(subj, pos, ...)
    if (g_numerrors == inf) then
--        print("Aborting parsing...")
        return nil  -- make the match fail, bail out of parsing
    end

    return true  -- don't return any captures
end

-- Attach the command names at the front!
local function attachnames(kwtab, matchtimefunc)
    for cmdname,cmdpat in pairs(kwtab) do
        -- The match-time function capture at the end is so that every command
        -- acts as a barrier to captures to delay (but not fully prevent) stack
        -- overflow (and to make lpeg.match return a subject position at the
        -- end)
        local newpat = Keyw(cmdname) * cmdpat
        if (cmdname~="break") then
            kwtab[cmdname] = lpeg.Cmt(newpat, matchtimefunc)
        else
            -- Must not attack a Cmt to "break" because it would break the
            -- while/switch sequencing.
            kwtab[cmdname] = newpat
        end
    end
end

attachnames(Couter, after_cmd_Cmt)
attachnames(Cinner, after_inner_cmd_Cmt)
attachnames(Cif, after_if_cmd_Cmt)


-- Takes one or more tables and +'s all its patterns together in reverse
-- lexicographical order.
-- Each such PATTAB must be a table that maps command names to their patterns.
local function all_alt_pattern(...)
    local cmds = {}

    local args = {...}
    assert(#args <= 2)

    for argi=1,#args do
        local pattab = args[argi]

        -- pairs() iterates in undefined order, so we first fill in the names...
        for cmdname,_ in pairs(pattab) do
            cmds[#cmds+1] = cmdname
        end
    end

    -- ...and then sort them in ascending lexicographical order
    table.sort(cmds)

    local pat = Pat(false)

    for i=1,#cmds do
        local ourpat = args[1][cmds[i]] or args[2][cmds[i]]
        -- shorter commands go at the end!
        pat = pat + ourpat
    end

    return pat
end

-- actor ORGANTIC is greeting!
function on.lonely_else(pos)
    pwarnprintf(pos, "found `else' with no `if'")
end

local con_inner_command = all_alt_pattern(Cinner)
local con_if_begs = all_alt_pattern(Cif)

local lone_else = (POS() * "else" * sp1)/on.lonely_else

local stmt_list = Var("stmt_list")
-- possibly empty statement list:
local stmt_list_or_eps = lpeg.Ct((stmt_list * sp1)^-1)
local stmt_list_nosp_or_eps = lpeg.Ct((stmt_list * (sp1 * stmt_list)^0)^-1)

-- Reused LPeg patterns
local common = {}

-- common to actor and useractor: <name/tilenum> [<strength> [<action> [<move> [<flags>... ]]]]
common.actor_end = sp1 * lpeg.Ct(tok.define *
    (sp1 * tok.define *
     (sp1 * tok.action *
      (sp1 * tok.move *
       (sp1 * tok.define)^0
      )^-1
     )^-1
    )^-1)
* sp1 * stmt_list_or_eps * "enda"

common.block_begin = lpeg.Cc(nil) / function()
    g_switchCode = {}
end

common.block_end = lpeg.Cc(nil) / function()
    if (#g_switchCode > 0) then
        addcode(g_switchCode)
    end
    g_switchCode = nil
end

--== block delimiters (no syntactic recursion) ==--
local Cblock = {
    -- actor (...)
    actor = POS() * lpeg.Cc(nil) * common.actor_end / on.actor_end,
    -- useractor <actortype> (...)
    useractor = POS() * sp1 * tok.define * common.actor_end / on.actor_end,
    -- eventloadactor <name/tilenum>
    eventloadactor = POS() * sp1 * tok.define * sp1 * stmt_list_or_eps * "enda"
        / on.eventloadactor_end,

    onevent = POS() * sp1 * tok.define * sp1 * stmt_list_or_eps * "endevent"
        / on.event_end,

    state = POS() * sp1 * (lpeg.Cmt(tok.identifier, on.state_begin_Cmt))
                  * sp1 * stmt_list_or_eps * tok.state_ends
        / on.state_end,
}

for cmdname, cmdpat in pairs(Cblock) do
    Cblock[cmdname] = common.block_begin * cmdpat * common.block_end
end

attachnames(Cblock, after_cmd_Cmt)


local t_good_identifier = Range("AZ", "az", "__") * Range("AZ", "az", "__", "09")^0

-- CON isaltok also has chars in "{}.", but these could potentially
-- interfere with *CON* syntax.  The "]" is so that the number in e.g. array[80]
-- isn't considered a broken identifier.
-- "-" is somewhat problematic, but we allow it only as 2nd and up character, so
-- there's no ambiguity with unary minus.  (Commands must be separated by spaces
-- in CON, so a trailing "-" is "OK", too.)
-- This is broken in itself, so we ought to make a compatibility/modern CON switch.
local t_broken_identifier = BadIdent(-((tok.number + t_good_identifier) * (sp1 + Set("[]:"))) *
                                     (alphanum + Set(BAD_ID_CHARS0)) * (alphanum + Set(BAD_ID_CHARS1))^0)

---
local function do_flatten_codetab(code, intotab)
    for i=1,math.huge do
        local elt = code[i]

        if (type(elt)=="string") then
            intotab[#intotab+1] = elt
        elseif (type(elt)=="table") then
            do_flatten_codetab(elt, intotab)
        else
            assert(elt==nil)
            return
        end
    end
end

-- Return a "string buffer" table that can be table.concat'ed
-- to get the code string.
local function flatten_codetab(codetab)
    local tmpcode = {}
    do_flatten_codetab(codetab, tmpcode)
    return tmpcode
end

function on.if_else_end(ifconds, ifstmt, elsestmt, ...)
    assert(#{...}==0)
    assert(type(ifconds)=="table" and #ifconds>=1)

    -- A condition may be a table carrying "deferred" code to add either
    --  [1] after the 'if' or
    --  [2] after the whole if/else block.
    -- In CON, it's always the same code for the same kind of "deferedness",
    -- and it's always idempotent (executing it multiple times has the same
    -- effect as executing it once), so generate code for it only once, too.
    local deferred = { nil, nil }
    local linenum = ""

    local ifcondstr = {}
    for i=1,#ifconds do
        local cond = ifconds[i]
        local hasmore = type(cond=="table")

        ifcondstr[i] = hasmore and cond[1] or cond
        assert(type(ifcondstr[i])=="string")

        -- IF_LINE_NUMBERING
        local tlinum = assert(ifcondstr[i]:match("^.*(%-%-[0-9]+)$"))
        ifcondstr[i] = assert(ifcondstr[i]:match("^(.*)%-%-[0-9]+$"))
        if (linenum == "") then
            linenum = tlinum
        end

        if (hasmore) then
            for i=1,2 do
                if (deferred[i]==nil) then
                    deferred[i] = cond[i+1]
                end
            end
        end
    end

    -- Construct a string of ANDed conditions
    local conds = "(" .. table.concat(ifcondstr, ")and(") .. ")"

    local code = {
        format("if %s then%s", conds, linenum),
        assert(ifstmt),
    }

    code[#code+1] = deferred[1]

    if (elsestmt~=nil) then
        local elseifp = false

        if (type(elsestmt)=="table") then
            elsestmt = flatten_codetab(elsestmt)

            if (#elsestmt>=2 and elsestmt[1]:match("^if ") and elsestmt[#elsestmt]=="end") then
                elsestmt[1] = elsestmt[1]:sub(4)
                elsestmt[#elsestmt] = nil
                elseifp = true
            end
        end

        code[#code+1] = elseifp and "elseif" or "else"
        code[#code+1] = elsestmt
    end

    code[#code+1] = "end"
    code[#code+1] = deferred[2]

    return code
end

function on.while_begin(v1, v2)
    table.insert(g_isWhile, true)
    return format("while (%s~=%s) do", v1, v2)
end

function on.while_end()
    local whilenum = #g_isWhile
    table.remove(g_isWhile)
    return format("::l%d:: end", whilenum)
end

function on.switch_begin()
    table.insert(g_isWhile, false)
end

function on.switch_end(testvar, blocks)
    local SW = format("_SW[%d]", g_switchCount)
    local swcode = { format("%s={", SW) }
    local have = {}
    local havedefault = false

    table.remove(g_isWhile)

    for i=1,#blocks do
        local block = blocks[i]
        assert(#block >= 1)
        local isdefault = (#block==1)
        local index = isdefault and "'default'" or tostring(block[1])

        if (have[index]) then
            if (isdefault) then
                errprintf("duplicate 'default' block in switch statement")
                return "_INVALIDSW()"
            else
                warnprintf("duplicate case %s in switch statement", index)
            end
        end
        have[index] = true

        swcode[#swcode+1] = format("[%s]=function(_aci,_pli,_dist)", index)
        swcode[#swcode+1] = get_cache_sap_code()
        -- insert the case/default code:
        swcode[#swcode+1] = block[#block]
        swcode[#swcode+1] = "end,"
    end

    swcode[#swcode+1] = "}"

    -- insert additional case test numbers (e.g. case 0: >>> case 1 <<<: <code...>)
    for i=1,#blocks do
        local block = blocks[i]
        for j=2,#block-1 do
            local index = tostring(block[j])
            swcode[#swcode+1] = format("%s[%d]=%s[%d]", SW, index, SW, tostring(block[1]))
        end
    end

    assert(g_switchCode ~= nil)
    g_switchCode[#g_switchCode+1] = swcode

    -- The code for the switch statement itself:
    local code = format("_con._switch(_SW[%d], %s, _aci,_pli,_dist)", g_switchCount, testvar)
    g_switchCount = g_switchCount+1
    return code
end


--- The final grammar!
local Grammar = Pat{
    -- The starting symbol.
    -- A translation unit is a (possibly empty) sequence of outer CON
    -- commands, separated by at least one whitespace which may be
    -- omitted at the EOF.
    sp0 * (all_alt_pattern(Couter, Cblock) * sp1)^0,

    -- Some often-used terminals follow.  These appear here because we're
    -- hitting a limit with LPeg else.
    -- http://lua-users.org/lists/lua-l/2008-11/msg00462.html

    -- NOTE: NW demo (NWSNOW.CON) contains a Ctrl-Z char (decimal 26)
    whitespace = Set(" \t\r\26") + newline + Set("(),;") + comment + linecomment,

    t_number = POS() * lpeg.C(
        tok.maybe_minus * ((Pat("0x") + "0X") * Range("09", "af", "AF")^1 * Pat("h")^-1
                           + Range("09")^1)
                             ) / parse_number,

    t_identifier_all = lpeg.C(t_broken_identifier + t_good_identifier),
    -- NOTE: -conl.keyword alone would be wrong, e.g. "state breakobject":
    -- NOTE 2: The + "[" is so that stuff like
    --   getactor[THISACTOR].x x
    --   getactor[THISACTOR].y y
    -- is parsed correctly.  (Compared with this:)
    --   getactor[THISACTOR].x x
    --   getactor [THISACTOR].y y
    -- This is in need of cleanup!
    t_identifier = -(conl.keyword * (sp1 + "[")) * tok.identifier_all,
    -- TODO?: SST TC has e.g. "1267AT", relying on it to be parsed as a number "1267".
    -- However, this conflicts with bad-identifiers, so it should be checked last.
    -- This would also handle LNGA2's "00000000h", though would give problems with
    -- e.g. "800h" (hex 0x800 or decimal 800?).
    t_define = (POS() * lpeg.C(tok.maybe_minus) * tok.identifier / lookup.defined_label) + tok.number,
    -- A defined label token, but returning the label if one was passed
    -- (specially shoehorned into a string):
    t_rawdefine = (POS() * lpeg.C(tok.maybe_minus) * tok.identifier / lookup.raw_defined_label) + tok.number,

    -- Defines and constants can take the place of vars that are only read.
    -- XXX: now, when tok.rvar fails, the tok.define failure message is printed.
    t_rvar = Var("t_botharrayexp") + lpeg.Cmt(tok.identifier, maybe_gamevar_Cmt) + tok.define,
    -- For written-to vars, only (non-parm2) array exprs and writable gamevars
    -- are permitted.  NOTE: C-CON doesn't support inline array exprs here.
    t_wvar = Var("t_singlearrayexp") / function() errprintf("t_wvar: array exprs NYI") return "_NYIVAR" end
        + (tok.identifier / function(id) return lookup.gamevar(id, "_aci", true) end),

    t_gamearray = Var("t_identifier") / lookup.gamearray,

    t_move =
        POS()*tok.identifier / function(...) return lookup.composite(LABEL.MOVE, ...) end +
        POS()*tok.number / function(...) return check.composite_literal(LABEL.MOVE, ...) end,

    t_ai =
        POS()*tok.identifier / function(...) return lookup.composite(LABEL.AI, ...) end +
        POS()*tok.number / function(...) return check.composite_literal(LABEL.AI, ...) end,

    t_action =
        POS()*tok.identifier / function(...) return lookup.composite(LABEL.ACTION, ...) end +
        POS()*tok.number / function(...) return check.composite_literal(LABEL.ACTION, ...) end,

    -- New-style inline arrays and structures.
    t_botharrayexp = tok.identifier * arraypat * patt.bothmember^-1
        / function(...) return lookup.array_expr(false, ...) end,
    t_singlearrayexp = tok.identifier * arraypat * patt.singlemember^-1,

    -- SWITCH
    switch_stmt = Keyw("switch") * sp1 * tok.rvar * (lpeg.Cc(nil)/on.switch_begin) *
        lpeg.Ct((Var("case_block") + Var("default_block"))^0) * sp1 * "endswitch"
        / on.switch_end,

    -- NOTE: some old DNWMD has "case: PIGCOP".  I don't think I'll allow that.
    case_block = lpeg.Ct((sp1 * Keyw("case") * sp1 * tok.define * (sp0*":")^-1)^1 * sp1 *
                         stmt_list_nosp_or_eps), -- * "break",

    default_block = lpeg.Ct(sp1 * Keyw("default") * (sp0*":"*sp0 + sp1) *
                            stmt_list_nosp_or_eps),  -- * "break",

    if_stmt = lpeg.Ct((con_if_begs * sp1)^1) * Var("single_stmt")
        * (sp1 * Keyw("else") * sp1 * Var("single_stmt"))^-1 / on.if_else_end,

    while_stmt = Keyw("whilevarvarn") * sp1 * tok.rvar * sp1 * tok.rvar / on.while_begin
          * sp1 * Var("single_stmt") * (lpeg.Cc(nil) / on.while_end)
        + Keyw("whilevarn") * sp1 * tok.rvar * sp1 * tok.define / on.while_begin
          * sp1 * Var("single_stmt") * (lpeg.Cc(nil) / on.while_end),

    stmt_common = Keyw("{") * sp1 * "}" / ""  -- space separation of commands in CON is for a reason!
        -- XXX: this do...end can lead to exceeding Lua nesting limits, see nightstrike's tan.con
        + lpeg.Ct(Keyw("{")/"do" * sp1 * stmt_list * sp1 * (Keyw("}")/"end"))
        + con_inner_command + Var("switch_stmt") + lpeg.Ct(Var("while_stmt")),

    single_stmt = Stmt( lone_else^-1 * (Var("stmt_common") + Var("if_stmt")) ),

    -- a non-empty statement/command list
    stmt_list = Var("single_stmt") * (sp1 * Var("single_stmt"))^0,
}


local function setup_newlineidxs(contents)
    local newlineidxs = {}
    for i in string.gmatch(contents, "()\n") do
        newlineidxs[#newlineidxs+1] = i
    end
    if (#newlineidxs == 0) then
        -- try CR only (old Mac)
        for i in string.gmatch(contents, "()\r") do
            newlineidxs[#newlineidxs+1] = i
        end
--        if (#newlineidxs > 0) then print('CR-only lineends detected.') end
    end
    -- dummy newlines at beginning and end
    newlineidxs[#newlineidxs+1] = #contents+1
    newlineidxs[0] = 0

    return newlineidxs
end


--== Lua -> CON line number mapping for error messages ==--

local lineinfo_mt = {
    __index = {
        -- Get CON file name and CON line number from Lua line number.
        getfline = function(self, lualine)
            local llines, lfiles = self.llines, self.lfiles
            assert(lualine >= 1 and lualine <= #llines)

            -- Get the CON line number: a simple lookup.
            local conline = llines[lualine]

            -- Find the CON file name next.
            local confile = nil
            for i=1,#lfiles do
                if (lfiles[i].line > lualine) then
                    break
                end
                -- Shorten the file name by stripping the directory parts.
                confile = lfiles[i].name:match("[^/]+$")
            end

            return confile or "???", conline
        end,
    },

    __metatable = true,
}

-- Handle a line of translated CON->Lua code. Return its CON line number.
local function lineinfo_handle_line(i, code, curline, curfile, lfiles)
    local lnumstr = code:match("%-%-([0-9]+)$")
    local begfn = lnumstr and nil or code:match("^%-%- BEGIN (.+)$")
    local endfn = lnumstr and nil or code:match("^%-%- END (.+)$")

    if (lnumstr) then
        curline[#curline] = assert(tonumber(lnumstr))
    elseif (begfn) then
        curfile[#curfile+1] = begfn
        curline[#curline+1] = 1
        -- Begin an included file.
        lfiles[#lfiles+1] = { line=i, name=begfn }
    elseif (endfn) then
        assert(endfn==curfile[#curfile])  -- assert proper nesting
        curfile[#curfile] = nil
        curline[#curline] = nil
        -- End an included file, so reset the name to the includer's one.
        lfiles[#lfiles+1] = { line=i, name=curfile[#curfile] }
    end

    return assert(curline[#curline])
end

-- Construct Lua->CON line mapping info.  This walks the generated code and
-- looks for our inserted comment strings, so it's kind of hackish.
function get_lineinfo(flatcode)
    local curline, curfile = { 0 }, { "<none>" }  -- stacks
    -- llines: [<Lua code line number>] = <CON code line number>
    -- lfiles: [<sequence number>] = { line=<Lua line number>, name=<filename> }
    local llines, lfiles = {}, {}

    if (type(flatcode)=="table") then
        for i=1,#flatcode do
            llines[i] = lineinfo_handle_line(i, flatcode[i], curline, curfile, lfiles)
        end
    else
        -- Already concat'ed code given.
        assert(type(flatcode)=="string")
        local olinestart = 1

        for i=1,math.huge do
            local curnli = flatcode:find("\n", olinestart, true)
            local line

            if (curnli ~= nil) then
                line = flatcode:sub(olinestart, curnli-1)
                olinestart = curnli+1
            else
                -- Last line
                line = flatcode:sub(olinestart, -1)
                break
            end

            llines[i] = lineinfo_handle_line(i, line, curline, curfile, lfiles)
        end
    end

    return setmetatable({ llines=llines, lfiles=lfiles }, lineinfo_mt)
end

-- <lineinfop>: Get line info?
local function get_code_string(codetab, lineinfop)
    -- Create meta-info gamevar: which gamevars have bit NORESET set?
    codetab[#codetab+1] = "_V._IS_NORESET_GAMEVAR={"
    for identifier, gv in pairs(g_gamevar) do
        if (bit.band(gv.flags, GVFLAG.NORESET) ~= 0) then
            codetab[#codetab+1] = format("[%q]=true,", identifier)
        end
    end
    codetab[#codetab+1] = "}"

    -- Return defined labels in a table...
    codetab[#codetab+1] = "return {"
    for label, val in pairs(g_labeldef) do
        -- ... skipping 'NO' and those that are gamevars in C-CON.
        if (g_labeltype[label]==LABEL.NUMBER and not g_labelspecial[label]) then
            codetab[#codetab+1] = format("[%q]=%d,", label, val)
        end
    end
    codetab[#codetab+1] = "},_C,_M,_I"  -- CONCODE_RETURN

    local flatcode = flatten_codetab(codetab)
    local lineinfo = lineinfop and get_lineinfo(flatcode)
    return table.concat(flatcode, "\n"), lineinfo
end

function on.parse_begin()
    g_isWhile = {}
    g_have_file[g_filename] = true

    -- set up new state
    -- TODO: pack into one "parser state" table?
    g_lastkw, g_lastkwpos, g_numerrors = nil, nil, 0
    g_recurslevel = g_recurslevel+1
end


---=== EXPORTED FUNCTIONS ===---

function parse(contents)  -- local
    -- save outer state
    local lastkw, lastkwpos, numerrors = g_lastkw, g_lastkwpos, g_numerrors
    local newlineidxs = g_newlineidxs

    on.parse_begin()

    g_newlineidxs = setup_newlineidxs(contents)

    addcodef("-- BEGIN %s", g_filename)

    local idx = lpeg.match(Grammar, contents)

    if (not idx) then
        printf("[%d] Match failed.", g_recurslevel)
        g_numerrors = inf
    elseif (idx == #contents+1) then
        if (g_numerrors ~= 0) then
            printf("[%d] Matched whole contents (%d errors).",
                   g_recurslevel, g_numerrors)
        elseif (g_recurslevel==0) then
            printf("[0] Matched whole contents.")
        end
    else
        local i, col = getlinecol(idx)
        local bi, ei = g_newlineidxs[i-1]+1, g_newlineidxs[i]-1

        printf("[%d] Match succeeded up to line %d, col %d (pos=%d, len=%d)",
               g_recurslevel, i, col, idx, #contents)
        g_numerrors = inf

--        printf("Line goes from %d to %d", bi, ei)
        local suffix = ""
        if (ei-bi > 76) then
            ei = bi+76
            suffix = " (...)"
        end
        printf("%s%s", string.sub(contents, bi, ei), suffix)

        if (g_lastkwpos) then
            i, col = getlinecol(g_lastkwpos)
            printf("Last keyword was at line %d, col %d: %s", i, col, g_lastkw)
        end
    end

    addcodef("-- END %s", g_filename)
    g_recurslevel = g_recurslevel-1

    -- restore outer state
    g_lastkw, g_lastkwpos = lastkw, lastkwpos
    g_numerrors = math.max(g_numerrors, numerrors)
    g_newlineidxs = newlineidxs
end

function reset.all()
    reset.labels()
    reset.gamedata()
    reset.codegen()
end

local function print_on_failure(msg)
    if (g_lastkwpos ~= nil) then
        printf("LAST KEYWORD POSITION: %s, %s", linecolstr(g_lastkwpos), g_lastkw)
    end
    print(msg)
end

if (string.dump) then
    -- running stand-alone
    local io = require("io")

    local function compile(filename)
        reset.all()

        -- Construct file name for the output code: (...)/xxx/qwe.con -->
        -- xxx_qwe.con, so that common root CON file names like EDUKE.CON will
        -- result in distinct file names for different mods. From that name,
        -- strip the extension.
        local codedir = g_cgopt["gendir"]
        local codefn = codedir and
            codedir.."/"..filename:match("[^/]+/[^/]+$"):gsub('/','_')..".lua"

        -- Get the directory part...
        g_directory = filename:match(".*/") or ""
        -- ...and the file name alone.
        filename = filename:sub(#g_directory+1, -1)

        -- NOTE: xpcall isn't useful here since the traceback won't give us
        -- anything inner to the lpeg.match call
        local ok, msg = pcall(do_include_file, g_directory, filename, true)
        -- ^v Swap commenting (comment top, uncomment bottom line) to get backtraces
--        local ok, msg = true, do_include_file(g_directory, filename, true)

        if (not ok) then
            print_on_failure(msg)
        end

        if (not (g_cgopt["no"]==true)) then
            local onlycheck = (g_cgopt["no"] == "onlycheck")
            -- The file for the output messages:
            local msgfile = onlycheck and io.stdout or io.stderr

            local code, lineinfo = get_code_string(g_curcode, g_cgopt["debug-lineinfo"])
            local func, errmsg = loadstring(code, "CON")

--            msgfile:write(format("-- GENERATED CODE for \"%s\":\n", filename))
            if (func == nil) then
                msgfile:write(format("-- %s%s: INVALID Lua CODE: %s\n",
                                     g_directory, filename, errmsg))
            end

            if (lineinfo) then
                for i=1,#lineinfo.llines do
                    msgfile:write(format("%d -> %s:%d\n", i, lineinfo:getfline(i)))
                end
            elseif (not onlycheck) then
                -- The file for the generated code:
                local codefile = codefn and assert(io.open(codefn, "w+")) or msgfile

                codefile:write(code)
                codefile:write("\n")
            end
        end
    end

    local havelists = false

    for argi=1,#arg do
        local filename = arg[argi]

        if (filename=="@") then
            -- Start file list processing from the next positional argument on.
            havelists = true
        elseif (havelists) then
            printf("\n------ Handling list of CON files \"%s\"", filename)
            for fn in io.lines(filename) do
                -- A hash at the beginning of a line denotes a comment, an
                -- empty line is skipped.
                if (#fn>0 and not fn:match("^#")) then
                    compile(fn)
                end
            end
        else
            compile(filename)
        end
    end
else
    -- running from EDuke32

    function compile(filenames)
        -- TODO: pathsearchmode=1 set in G_CompileScripts

        reset.all()

        for _, fname in ipairs(filenames) do
            local ok, msg = pcall(do_include_file, "", fname, true)
            if (not ok or g_numerrors > 0) then
                if (not ok) then
                    -- Unexpected error in the Lua code (i.e. a bug here).
                    print_on_failure(msg)
                end
                return nil
            end
        end

        return get_code_string(g_curcode, true)
    end
end
