#!/usr/bin/php -qCdregister_argc_argv=1
<?php
/**
 * api: phrep
 * type: main
 * title: PHreProcessor
 * description: Rewrites preprocessing statements in PHP source files
 * url: http://fossil.include-once.org/phrep/
 * version: 0.3.0
 * license: MITL
 * author: mario@include-once.org
 * category: devel
 * depends: php (>= 5.4)
 * suggests: bin:make
 * config: { type: env list colon, name: PHP_INCLUDE_PATH, description: Additional include search dirs. }
 * architecture: all
 * pack: phrep=/usr/bin/, *.1=/usr/share/man/man1/
 * 
 * This tool works similar to CPP, in that it rewrites preprocessing directives
 * like #define and #include in source files. It's designed for PHP scripts
 * however, is context-sensitive per default, and does not strip other comments.
 * 
 * Usage:
 *     phrep  -D PKG_VERSION=1.2.3  -i infile.src -o outfile.php
 *
 * While #directives are honored regardless of file location, constant and macro
 * replacements are only applied within <?php code sections, in the -m token mode.
 *
 */


namespace {

  #-- Init
  $proc = new io\MacroProcessor();
  $opts = new ShellFlags(getopt("i:o:I:D:m:p:wWhvVkfEDn:", ShellFlags::$map), $proc);
  empty($opts->config) and $opts->config();

  #-- Process main file
  if (empty($opts->input)) {
    $proc->stderr(1, "No -i input filename given. See -h help or `man phrep`.");
    exit(66 /*EX_NOINPUT*/);
  }
  try {
    $proc->pragma["output"] = 1;  // just in case of eager pre-includes
    $src = $proc->on($opts->input);
  }
  catch (Exception $e) {
    $proc->stderr($e->getCode(), $e->getMessage());
    exit(70 /*EX_SOFTWARE*/);
  }


  // Write to target filename / or stdout
  switch($output_fn = $opts->output) {
    case "/dev/stdout":
    case "-":
    case "":
      print $src;
      exit(0 /*EX_OK*/);
    case "*.php":
    case ".php":
    case "php":
      $output_fn = preg_replace("/(\.[pm])?\.\w{2,4}$/", ".php", $opts->input);
      goto writefile;
    case "input":
    case "-i":
    case "i":
      $output_fn = $opts->input;
      goto writefile;
    case "target":
    case "t":
      $output_fn = $proc->pragma["target"];
    default:
    writefile:
      if (file_exists($output_fn) && !$opts->force) {
        $proc->stderr(2, "Output filename '$output_fn' exists. (Use -f option to force overwriting.)");
        exit(73 /*EX_CANTCREAT*/);
      }
      else {
        file_put_contents($output_fn, $src);
        exit(0 /*EX_OK*/);
      }
  }




  /**
   * Ties getopt($argv) results list to MacroProcessor options,
   * Populates $proc->defines/dirs/pragma fields.
   *
   */
  class ShellFlags {

      static $map = [
          "i" => "input:", "o" => "output:", "f" => "force",
          "I" => "dirs:", "D" => "defines:", "p" => "pragma:", 
          "m" => "mode:", "k" => "keep", "w" => "quiet", "W" => "fail",
          "E" => "cpp", "config" => "config:", "n" => "no:",
          "v" => "verbose", "V" => "version", "h" => "help", 
      ];
      var $input = NULL;
      var $output = "-";
      var $force = false;
      var $proc;


      // Dispatch -flags to handlers
      public function __construct($opts, $proc) {
          $this->proc = $proc;
          foreach ($opts as $key=>$list) {
              // Store literal flag value (mixed str/array)
              in_array($key, self::$map) or $key = trim(self::$map[$key], ":");
              $this->$key = $list;
              // Invoke custom handlers
              foreach ((array)$list as $val) {
                  $this->$key($val, $proc);
              }
          }
      }

      
      // Read config file from HOME/XDG_CONFIG_HOME
      public function config($fn = "phrep/config.php") {
          $proc = $this->proc;
          $opts = $this;
          if (file_exists($fn)) {
              include_once($fn);
          }
          foreach (["XDG_CONFIG_HOME" => $fn, "HOME" => ".config/$fn", "APPDATA" => $fn] as $env => $fn) {
              if (!empty($_SERVER[$env]) and file_exists($fn = "{$_SERVER[$env]}/$fn")) {
                   // config include script which may adapt options
                   return include_once(realpath($fn));
              }
          }
      }


      // Include search dirs, or preread defines.h without output
      function dirs($val) {
          if (file_exists($val)) {
              if (is_dir($val)) {
                  $this->proc->dirs[] = $val;   // add to #include <paths/*>
              }
              else {
                  $this->proc->on($val);   // preinclude.h without output
              }
          }
          else {
              $this->proc->stderr(E_ERROR, "Input file/dir '$val' does not exist.");
          }
      }

    
      // Custom constants
      function defines($val) {
          $this->proc->d_define($val);
      }


      // #Pragma(key=value)
      function pragma($val) {
          $this->proc->d_pragma("($val)");
      }


      // Input filename
      function input($val) {
          $this->proc->pragma["input"] = $val;
          if ($val == "-") {
              $val = "/dev/stdin";
              $this->input = $val;
          }
          else {
              // Also add basedir of input file to #include search $dirs for simplicity
              if ($dir = basename($val)) {
                  $this->proc->dirs[] = $val;
              }
          }
      }


      // Target filename
      function output($val) {
          $this->proc->pragma["target"] = $val;
      }


      // No: s=substitution, or just c=constants, m=macros, x=complex
      function no($val) {
          foreach (str_split($val) as $what) {
              switch ($what) {
                  case "s": $this->proc->pragma["substitution"] = 0; break;
                  case "c": $this->proc->pragma["constants"] = 0; break;
                  case "m": $this->proc->pragma["macros"] = 0; break;
                  case "x": $this->proc->pragma["complex"] = 0; break;
              }
          }
      }


      // Quiet quiet
      function quiet($val) {     $this->proc->pragma["quiet"] = 1;               }

      // Errors
      function fail($val) {      $this->proc->pragma["fail"] = 1;                }

      // Keep lines
      function keep($val) {      $this->proc->pragma["keep_empty"] = 1;          }

      // Interpolation mode (token/regex/erb/phpp)
      function mode($val) {      $this->proc->pragma["interpolate"] = $val;      }

      // verbose
      function verbose($val) {   $this->proc->pragma["verbose"] = $val;          }

      // Version
      function version() {
          preg_match("/version:\s*(\d+\.\S+)/i", file_get_contents(__FILE__), $ver);
          die("phrep $ver[1]\n");
      }


      // Local flags
      function force() { $this->force = 1; }
      function cpp() {  }

      
      // Helpsme
      function help() {
          $h = "\e[1mPHP source file preproccessor\e[0m. Handles #if, #ifdef, #define, #include directives
to substitute constants and macros, or conjoin 'header' files (include.ph).

Usage:
   \e[1;32mphrep\e[0m [-opts] -i source.php [-o target.php]

Options:
   -i source.fn   Source script filename, or pipe input via -i-
   -o filename    Output target filename, or special name -oi, -o input, -o target
   -f             Force overwriting if output file exists.
   -D CONST=VAL   Predefine constant or macro.
   -I dir/        Include directory and search path.
   -I preload.h   Read macros/constants from header file beforehand, without output.
   -E             Dummy CPP compat flag (\"just preprocess\").
   -w             Supress any non-fatal warnings.
   -W             Abort with errors instead of just printing warnings.
   -m token       Switch to `regex` or `phpp` or `erb` mode instead of `tokenizer`.
                  Define custom const/macro delimiters with `-m 'regex,[[,]]'` e.g.
   -k             Keep empty lines in place for #directives.
   -p opt=val     Set other processing (#pragma) options.
   -ns            No substitution of constants or macros.
   -nc            No constant substution, but still macro/complex@() replacement.
   -v             Verbose mode. (Dummy flag.)
   -V             Print version.
   -h             Show this help text.

Source files are scanned for macro directives such as #define, #if, #ifdef, #ifndef,
and #else, #elif, #endif, #undef, #pragma(opt=val), #stderr or #include. After all
include files have been joined, any collected constants and macros will be substituted.

If no -o output filename has been given, the processed script will be printed out
instead. But files can also be updated in-place using `-f -oi`.\n\n";
          exit(
            preg_replace( // colorize #directives, -flags w/ opts and `backticked` stuff
              ["/#\w+/", "/(?<=\W)(-\w+\s)(\S+)/", "/`(.+?)`|(?<=\W)-\w+\.?/"],
              ["\e[2m$0\e[0m", "$1\e[1;32m$2\e[0m", "\e[1;31m$0\e[0m"], $h
            )
          );
      }
  }

} # global namespace

/**
 * api: php
 * title: PHP Macro Preprocessor
 * description: Context-sensitive C-style #define/macro directive processing.
 * license: MITL
 * version: 0.3.0
 * state: beta
 * depends: php (>= 5.4.0)
 * type: rewrite
 * category: macro
 * url: http://fossil.include-once.org/phrep/
 * pack: macro.php=MacroProcessor.php
 *
 *
 * Preprocessor to apply C-style #define, #ifdef, #include statements. 
 * Intended as build helper and complex macro injector. It's language-
 * specific to PHP, but can consume most common C header #defines.
 *
 * Source code/file transformation works in two phases:
 *
 *   1.  All #define, #include, #if* directives are expanded in a regex
 *       splitting state machine.  Which joins includes, or omits
 *       conditional sections.  This step is syntax-insensitive, but
 *       strips open/close tokens from injected files, and searches
 *       *.ph/.php/.inc/.h includes.
 *
 *   2.  Constants and macros are applied on the accumulated source code
 *       using a tokenized representation.  This allows context-sensitive
 *       expansion of defined preprocessor constants, standard macros, and
 *       even more complex transformation callbacks - for constructs such
 *       as IFSET@(expr).
 *
 * Directives take the form `#define X Y` and must be noted leftmost in
 * source files.  Following preprocessor directives are known:
 *
 *     #define CONST 1.2.3
 *     #ifdef CONST
 *     #include <filename.ph>
 *     #elif CONST < 1.0 || CONST >= 2.0
 *     #undef CONST
 *     #define MACRO(x,y) (x+y*2)
 *     #stderr Print some message
 *     #endif
 *     #pragma(output=0)
 *     #srcout inject_literal($source);
 *     #macro VA@(fn,expr...) {return "$fn([$expr])";}
 *
 * For compatibility with preprocess.py there's also support for doubly-
 * commented directives such as:
 *
 *     # #include <linux/errno.h>
 *     // #define X Y
 *
 * They're even valid with one ␣ space before the init // comment, and
 * up to two spaces ␣␣ before the actual #directive name.  Which allows
 * minimal structuring.
 *
 * Various #pragma(options=...) can control processing and substitution.
 * For example, output=0 allows to suppress #included file source code. 
 * Or use the crude interpolate=regex mode for constant replacements,
 * disable macros=0 etc.
 *
 * Per default #include scripts are stripped off opening <?php and closing
 * PHP tags.  And literal C header .h files have content output disabled
 * per default.
 *
 */

namespace io
{
use \stdClass, \ArrayAccess, \SplStack,
    \Exception, \OutOfBoundsException;


/**
 * MacroProcessor assembles the input source file with all its #include
 * deps, and handles conditional sections (via MacroExpression).
 * 
 *  · Main invocation method is ->on($filename). Which is meant/named as
 *    simple array_map() callback for file lists.  Can also be pre-invoked
 *    to parse any includes.h, but discard output.
 *
 *  · Basic source assembly could alternatively be run via ->block($source).
 *
 * After the complete source code has been collected, passes over macro and
 * constant expansion to MacroInjector for tokenizer/regex substitutions.
 *
 */
class MacroProcessor {

    /**
     * Search paths for #include instructions.
     * Filename extensions .ph / .h / .php / .inc are probed for.
     */
    var $dirs = [
        ".",
        "/usr/include/",
        "/usr/include/php5",
    ];

    /**
     * Processing behaviour / status flags,
     * can be overridden with #pragma(output=0)
     */
    var $pragma = [
        "output" => 1,            # Join further file content into output.
        "encoding" => "UTF-8",    # unused
        "file" => 0,              # Currently processed file.
        "line" => 0,              # Track parsed line number (somewhat inexact).
        "input" => NULL,          # Input filename
        "target" => NULL,         # Target filename
        "omit" => "*.{h,def}",    # Like output=0, but for specific include file extensions.
        "comments" => 1,          # Retain inline /*comments*/ for constants.
        "substitution" => 1,      # General substitution of constants/macros/complex.
        "constants" => 1,         # Substitute constants in final code.
        "macros" => 1,            # Read macro definitions (disable for e.g. system/*.h includes).
        "complex" => 1,           # Allows complex macros (PHP callback code).
        "multipass" => 0,         # Resursively expand basic macros in basic macros.
        "defprefix" => "",        # Mask read defines/macro names with prefix (e.g. "C_DEF_")
        "keep_empty" => 0,        # Retain empty lines for #directives.
        "fail" => 0,              # Abort with Exception instead of just printing to stderr.
        "quiet" => 0,             # Suppress stderr warnings.
        "interpolate" => "token", # Or "regex" mode, or "erb", "phpp", or "delim << >>" for custom delimiters.
        "dirs" => NULL,           # Append include -I search paths.
        "preserve" => "output,file,line",  # Pragma states to retain between #includes
        "strip_php_tags" => 1,    # Remove <?php open and close tokens from #include files
    ];

    // Updated via versionnum
    const PHREP_VERSION = "0.3.0";

    /**
     * Predefined and collected constants and macros.
     */
    var $defines = [

       #-- Predeclare some C-style constants
       "__STDC_VERSION__" => [null, 201112],
       "__PHP_VERSION__" => [null, PHP_VERSION],
       "__PHREP_VERSION__" => [null, self::PHREP_VERSION],
       "__FLT_EPSILON__" => [null, 1.1920928955078E-7],
       #"__TIMESTAMP__" => ISO datetime string
       #"__BASE_FILE__" => set via constructor

       #-- Basic macros
       "INLINE_MAX" => [["a","b"], "((a)>(b)?(a):(b))"],

       #-- PHP expression transformation macros
       "IFSET@" => [["expr"], '{ return "(isset($expr) ? ($expr) : NULL)"; }'],
    ];


    /**
     * Regexps to match/split, assert or replace code and directive sections.
     *
     */

    // Directive splitting (two capture groups: directive name, and args)
    const RX_DIRECTIVES = "{
      ^ (?:[ ]? (?:\#|//) (?=[ ]))? [ ]{0,2}            # preprocess.py prefixes
      \# (include(?=\s)|define|undef|stderr|srcout      # allowed directives
         |ifndef|ifdef|if|elif|else|endif|pragma|macro)
         ( (?>\V*(?:\R|\Z))                             # arguments and multi-line
         (?>(?<=\\\\\\n|\\\\\\r\\n|\\\\\\r)\V*\R)* )    # expressions via <\\><NL>
    }mx"; // <?

    // Restrict #pragma syntax
    const RX_PRAGMA = "/^
        ( \( \s* )?           (?<key>\w+)               # either (..) enclosed
        (?(1) \s*=\s* | \s+)  (?<val>.*)                # then key=val
        (?(1) \s*\)   | $)                              # or just #pragma key val
    $/sx";

    const RX_DEFINE = "/^
        (?<name>\w+@?) (?<args>\((?:\w+\.*(?:\h*,\h*\w+\.*)* )?\))?
        (?:[=\s]\s* (?<val>.*))?
    $/six";

    // Constrain #include filename syntax
    const RX_INCLUDE_FN = "/^
       ^(?!\() ([\"\'\<])?                # valid enclosures < or \", not (
       ((?: [^\s\'\"\<\>\\\\]+ | \\\\.)+) # no quotes, str backslash escapes
       (?(1)[\>\'\"]|) (?<!;)             # assert enclosures, no trailing ;
    $/x";

    // Used to strip open/close tags
    const RX_PHP_TOKEN = "/
       ^ (\\xEF\\xBB\\xBF)? \s{0,5}       # strip UTF-8 BOM, max 5 spaces
         \<\?(php)?        (\h*\R)?       # open token, optionally HS+NL
       | \?\>             \s{0,20}$       # close token, optional spaces
    /x";



    /**
     * Initialize further pragmas, runtime #defines, and #include search paths.
     *
     * @param  array   $include_dirs   Include search paths.
     * @param  array   $defines        Inject custom/precollected defines.
     *
     */
    public function __construct($include_dirs=[], $defines=[]) {
    
        $this->dirs = array_merge($this->dirs, $include_dirs);
        
        // Non-static runtime constants
        $this->defines += $defines + [
            "__TIMESTAMP__" => [null, gmstrftime("%Y-%m-%dT%H:%M:%SZ")],
            "__BASE_FILE__" => [null, & $this->pragma["input"]],
        ];
    }


    /**
     * Output PHP warnings during preprocessing
     *
     */
    public function stderr(/*int*/$errno, /*string*/$msg, $file="macro.php", $line=0, $context=[]) {
        if ($this->pragma["fail"]) {
            throw new Exception($msg, $errno);
        }
        elseif ($this->pragma["quiet"]) {
            return;
        }
        // colorize output per default
        $msg = "\e[1;41m$msg\e[0m";
        if ($errno) {
            $msg = "\e[33;1m[$errno]\e[0m $msg";
        }
        if (1||$this->pragma["file"]) {
            $msg .= " (\e[2mprocessing \e[0;33m{$this->pragma['file']}:{$this->pragma['line']}\e[0m)";
        }
        if ($file && $line) {
            $msg .= " \e[36min $file:$line\e[0m";
        }

        fwrite(defined("STDERR") ? STDERR : fopen('php://stderr', 'w'), "$msg\n");
    }


    /**
     * Main invocation method
     * ––––––––––––––––––––––
     *
     * Reads and transform source file.
     * Returns fully assembled and constant/macro-substituted output.
     * Does not write to target file itself.
     *
     * @param  string  $fn     Primary input filename
     * @return string          Conjoined and substituted source
     *
     */
    public function on(/*string*/$fn) {

        // Enable STDERR reporting during runtime
        set_error_handler([$this, "stderr"]);

        // Read main file, process #directives between regex-split source blocks
        $this->pragma["input"] = $fn;
        $this->pragma["file"] = $fn;
        $output = $this->block(file_get_contents($fn));

        // Apply macros and constants in tokenization phase
        if ($this->pragma["substitution"]) {
            if ($this->pragma["constants"] || $this->pragma["macros"] || $this->pragma["complex"]) {
                $inj = new MacroInjector($this->filter_defines($this->defines), $this->pragma["multipass"]);
                $output = $inj->substitute($output, $this->pragma["interpolate"]);
            }
        }
        
        restore_error_handler();        
        return $output;
    }



    /**
     * Handle #directives
     * ––––––––––––––––––
     *
     * Regex-splits the source code, traverses it block-wise.
     * Handles #define, #if, #ifdef, #ifelse, #endif statements,
     * traverses into #include files, and honors #pragma overrides.
     *
     * @param  string  $src    Source code to apply #directives on
     * @param  string  $out    Prepend source
     * @return string          Assembled source code
     *
     */
    public function block(/*string*/ $src, $out="") {

        // Split source into a list of ["#directive", "args/expr", "source file content.."] blocks
        $src = array_chunk(
            array_merge(
                ["pragma", "(start=block)"] /* dummy prefix directive */,
                preg_split(self::RX_DIRECTIVES, $src, -1, PREG_SPLIT_DELIM_CAPTURE)
            ),  3
        );

        // Condition block nesting levels, and active/done state tracking
        $¶ = new ConditionState();
        
        // Run over each #directive + content block
        while ($block = array_shift($src)) {
            $directive = $block[0];
            $args = trim(preg_replace("/\\s*\\\\(\R)/", "$1", $block[1]));
            $pragma_lines = substr_count($block[1], "\n");

            // Dispatch or handle #directives
            switch ($directive) {

                // Condition state tracking
                case "if": 
                case "ifdef": 
                case "ifndef":
                    if ($¶->act) {
                        if ($directive == "if") {
                            $args = MacroExpression::strval($args, $this);
                        }
                        else {
                            $args = isset($this->defines[$args]) ^ ($directive == "ifndef");
                        }
                        $¶->descend();
                        $¶->end = $¶->act = intval($args);
                    }
                    else {
                        $¶->descend();
                    }
                    break;

                case "else":
                    $args = "1";
                case "elif":
                    if ($¶->end) {
                        $¶->act = 0;
                    }
                    elseif ($¶->act = MacroExpression::intval($args, $this)) {
                        $¶->end = 1;
                    }
                    break;

                case "endif":
                    $¶->done();
                    break;

                // Action directives
                case "macro": $directive = "define";
                case "define":
                case "undef":
                case "include":
                case "stderr":
                case "srcout":
                case "pragma":
                    if ($¶->act) {
                        $out .= $this->{"d_$directive"}($args);
                    }
                    break;

                default:  // Do nothing (custom directives go in RX_DIRECTIVES)
            }

            // Append output block / or empty lines
            $pragma_lines = substr_count($block[1], "\n");
            $output_lines = substr_count($block[2], "\n");
            if ($this->pragma["output"]) {
                if ($¶->act) {
                    $out .= $block[2];
                }
                elseif ($this->pragma["keep_empty"]) {
                    $out .= str_repeat("\n", $pragma_lines);
                    if (!$¶->act) {
                        $out .= str_repeat("\n", $output_lines);
                    }
                }
            }
            $this->pragma["line"] += $pragma_lines;
            $this->pragma["line"] += $output_lines;
        }
        return $out;
    }


    /**
     * Constant/Macro declarations take the form:
     *   → #define CONSTANT 1.2.3
     *   → #define MACRO(X,Y) X > Y
     *   → #define PHPEXP@(args...) { return "[$args]"; }
     */
    function d_define($decl) {
        if (preg_match(self::RX_DEFINE, $decl, $d)) {
            $val = $args = NULL;
            extract($d);  // name, args, val
            if (isset($args)) {
                preg_match_all("/[\w\.]+/", $d["args"], $m) and $args = $m[0];
                // Collecting macros can be disabled per #pragma
                if (!$this->pragma["macros"]) {
                    return;
                }
            }
            elseif (!$this->pragma["comments"]) {
                $val = preg_replace("~/\*.+?\*/|//\V+\R?~", "", $val);
            }
            $this->defines[$this->pragma["defprefix"] . $name] = array($args, $val);
        }
        else {
            $this->stderr(3, "Unrecognized #define statement syntax. '$decl'");
        }
    }

    /**
     * Removes declared constants or macros.
     *   → #undef CONSTANT
     */
    function d_undef($name) {
        unset($this->defines[$name]);
    }

    /**
     * Just prints out a warning/error message
     *
     */
    function d_stderr($name) {
        $this->stderr(5, $name);
    }

    /**
     * Injects commented out `#srcout code();` code - for conditional sections (with state=true).
     *
     */
    function d_srcout($line) {
        return $line . "\n";
    }

    /**
     * Sets processing options.
     *   → #pragma(output=1)
     *   → #pragma(target=bld/output_fn.php)
     *   → #pragma(interpolate=erb)
     */
    function d_pragma($args) {
        if (assert(preg_match(self::RX_PRAGMA, $args, $pm) and extract($pm))) {
            $this->pragma[$key = strtolower($key)] = $val;
            if (method_exists($this, "p_$key")) {
                $this->{"p_$key"}($key, $val);
            }
        }
    }
    // Pragma hook: dirs
    function p_dirs($key, $val) {
        assert('!is_int(strpos($val, "\\\\")) /* Ahem, $val forward slashes work on Windows too (ever since DOS 2.0). */');
        $this->dirs = array_unique(array_merge($this->dirs, preg_split("/(?<!:[A-Z]|phar):/", $val)));
    }

    /**
     * Include files get conjoined (.php or .ph) or C header files (.h) just parsed for constants/macros.
     * Looks through the defined include ->dirs, optionally tests file extensions (.ph, .php, .inc, .h)
     *   → #include lib/deps.php
     *   → #include "deps.ph"
     *   → #include <errno.h>
     */
    function d_include($fn) {

        // Extract filename, but eschew commented-out PHP include(); statements
        if (preg_match(self::RX_INCLUDE_FN, $fn, $m)) {
            $fn = stripcslashes($m[2]);
        }
        else {
            assert("\$fn && \$m /* invalid #include <$fn> syntax */");
            return "#include $fn\n";  // send back "#input...\n" string, just in case it was only a disabled PHP include()
        }
        if (isset($this->defines[$fn]) and empty($this->defines[$fn][0])) {
            $fn = $this->defines[$fn];  // can be `#include CONST_NAME_H`
        }

        // Read source, strip T_OPEN/CLOSE tokens
        if (!$fn = $this->find_include($ofn=$fn) or !$src = file_get_contents($fn)) {
            return;
        }
        if ($this->pragma["strip_php_tags"]) {
            $src = preg_replace(self::RX_PHP_TOKEN, "", $src, 2);
        }

        // Change some states between includes (output,file,line)
        $preserve = array_intersect_key($this->pragma, array_flip(str_getcsv($this->pragma["preserve"])));
        $this->pragma += ["file" => $fn, "line" => 0, "output" => fnmatch($fn, $this->pragma["omit"], GLOB_BRACE)];

        // Run through preprocessor phase, and reset kept states
        $src = $this->block($src);
        $this->pragma = array_merge($this->pragma, $preserve);
        return $src;
    }


    /**
     * Search ->dirs for #include $fn with automatic extension probing.
     *
     */
    function find_include($fn, $exts=["", ".ph", ".php", ".inc", ".h", ".p"]) {
        if (file_exists($fn)) {
            return $fn;  // bypass searching for absolute paths
        }
        foreach ($this->dirs as $dir) if (file_exists($dir)) {
            foreach ($exts as $ext) {
                if (file_exists("$dir/$fn$ext")) {
                    return "$dir/$fn$ext";
                }
            }
        }
        trigger_error("#include <$fn> not found");
    }


    /**
     * Return the allowed subset of collected constants/macros.
     * (Filters based on pragma options: constants, macros, complex.)
     *
     */
    function filter_defines($def, $constants=0, $complex=0, $macros=0) {
        extract($this->pragma, EXTR_IF_EXISTS);
        if (!$constants or !$complex or !$macros) {
            foreach ($def as $k => $v) {
                if (!$constants and empty($v[0])  or  !$macros and !empty($v[0])  or  !$complex and is_int(strpos($k, "@")))
                {
                    unset($def[$k]);
                }
            }
        }
        return $def;
    }

}



/**
 * Apply constants/macros
 * ––––––––––––––––––––––
 *
 * This is run after all #includes have been merged into the input file and
 * all #defines and #pragmas have been collected.
 *
 */
class MacroInjector {

    /**
     * Inherit $defines as collected by MacroProcessor parent.
     *
     * @param  object  parent
     */
    function __construct($defines, $multipass=0) {
        $this->defines = $defines;
        $this->multipass = $multipass;
    }

    // Parent attributes
    var $defines;
    var $multipass = 0;


    // Capture paired parens after MACRO names in `regex` replacement mode
    var $RX_PARENS = "(\(
       ((?>
           \"(?> [^\\\\\"] | \\\\. )*\"   # strings
         | \'(?> [^\\\\\'] | \\\\. )*\'
         | (?:\/\/|\#) .*\R               # comments
         | \/\* [\s\S]* \*\/
         | [^\(\)]                        # anything but parens
         | (?-2)                          # nested parens
       )*)
    \))?";


    /**
     * Substitutes preprocessor-#defined CONSTANTS and MACROS(x,y) in the
     * assembled source string.
     *
     * Either uses a regex extraction mode, or the tokenizer.
     *
     * @param  string  source code
     * @param  string  interpolation mode
     * @result string  constants and macros substituted
     *
     */
    public function substitute($src, $mode="token") {

        // "Simple" regex lookups
        if ($mode != "token" and preg_match("/^(regex|rx|erb|phpp|delim)\b[,\s]*([^\s,]*)[,\s]*([^\s,]*)$/", $mode, $delim)) {

            // Regex for allowed const/macro names
            list($del_l, $del_r) = $this->rx_delim($delim);
            $find = join("|", array_keys($this->defines));

            return preg_replace_callback("~$del_l ($find) $this->RX_PARENS $del_r~x", [$this, "regex_substitute"], $src);
        }

        // Tokenizer mode: looks for T_STRINGs that match collected #defines and #macros
        else {
            return Token::join(
                $this->token_substitute(
                    token_get_all($src),
                    $this->defines
                )
            );
        }
    }


    /**
     * Custom or predefined delimiters around constant/macro names.
     *
     */
    function rx_delim($delim) {
        $rx = [
            "wbnd" => ["(?<!\w)", "(?!\w)"],  // just word boundaries
            "erb"  => ["\Q%{\E", "\Q}\E"],    // %{ERB} syntax
            "phpb" => ["\Q{{{\E", "\Q}}}\E"], // {{{PHPP}} scheme
        ];
        if (!empty($delim[3])) {
            return ["\Q$delim[2]\E", "\Q$delim[3]\E"];
        }
        elseif (isset($rx[$delim[1]])) {
            return $rx[$delim[1]];
        }
        return $rx["wbnd"];
    }


    /**
     * Regex callback for CONST/MACRO@?(..) matches.
     *
     */
    function regex_substitute($m) {
        list($keys, $body) = $this->defines[$m[1]];

        // Only treat as macro if keys are defined, and arguments are present
        if (is_array($keys) and !empty($m[2])) {
            $token = array_slice(token_get_all("<?php\n$m[3]"), 1);
            $args = array_map("io\\Token::join", Token::args($token));
            return $this->expand_macro($m[1], $args, $token);
        }
        // Just a constant
        else {
            return $this->multipass($body);
        }
    }


    /**
     * Tokenize and scan for T_STRINGs and optional @s,
     * and assert we're not following some constructs (e.g. const= declarations).
     *
     */
    function token_substitute($token, $defs) {
        $prev = $type = $next = T_WHITESPACE;
        $max = count($token) - 1;

        // Only stop for array token entries
        for ($i=0; $i < $max; $i++) {

            // Look only at array tokens, T_STRING literals, not preceded by "const"
            if (!is_array($token[$i]) or (($type = $token[$i][0]) !== T_STRING) or ($prev == T_CONST) or ($prev == T_PAAMAYIM_NEKUDOTAYIM))
            {
                if ($type !== T_WHITESPACE) {
                    $prev = $type;
                }
                continue;
            }
            $name = $token[$i][1];
            $next = $token[$i + 1][0]; // careless

            // Plain constant
            if (isset($defs[$name][1]) && empty($defs[$name][0])) {
                $token[$i] = $this->multipass($defs[$name][1]);
            }

            // `Basic` macro names, or `Complex` with trailing "@", both followed directly by ( opening parenthesis
            elseif (isset($defs[$name][1]) and ($next === "(")
                or ($next === "@") and isset($defs[$name .= $next][1]) and ($token[$i+2][0] === "("))
            {
                $this->token_macro_repl($token, $i, $name);
                $max = count($token) - 1;
            }
            $prev = $type;
        }
        return $token;
    }


    /**
     * Split macro arguments in (...) parens,
     * inject expanded code within the found $token range.
     *
     */
    function token_macro_repl(&$token, $orig_i, $name) {
    
        // Find paired parens around macro arguments
        for ($i=$orig_i, $level=NULL, $len=1; $i < count($token); $i++, $len++) {
            if (is_string($t = $token[$i])) {
                if ($t == "(" and ++$level == 1) {
                    $istart = $i + 1;
                }
                elseif ($t == ")" and --$level == 0) {
                    break;
                }
            }
        }

        // Split out arguments
        $tok_list = array_slice($token, $istart, $i - $istart);
        $str_args = array_map("io\\Token::join", Token::args($tok_list));

        // Replace tokens with a single text blob
        $code = $this->expand_macro($name, $str_args, $tok_list);
        array_splice(
            $token, $orig_i, $len, [[T_FUNC_C, $code, -1]]
        );
    }



    /**
     * Expand macros. (Used by regex and tokenizer modes.)
     *
     * @param  string  Basic macro "NAME" or "COMPLEX@"
     * @param  array   List of param values/expressions as strings ["$a", "$b", "3.0+func()"]
     * @param  array   All arguments as token list [ T_VAR "$a", ">", T_VAR "$b", ",", T_VAR "$c", T_INCR "++", ...]
     * @return string
     *
     */
    function expand_macro($name, $args, $token=[]) {
        $complex = is_int(strpos($name, "@"));
        list($keys, $body) = $this->defines[$name];

        // Complex macro callbacks
        if ($complex) {

            // map args to names
            $php_keys = "\$token";
            $php_args = [$token];
            foreach ($keys as $key) {
                if (is_int(strpos($key, ".."))) {
                    $key = trim($key, ".");
                    $args = [$args];
                }
                if (!count($args)) {
                    $args[] = NULL;
                    trigger_error("No macro {$name}() argument for key `$key`");
                }
                $php_keys .= ",\$$key";
                $php_args[] = array_shift($args);
            }

            // create and call function body
            static $cb = [];
            if (empty($cb[$name]) || !is_callable($cb[$name])) {
                $cb[$name] = create_function($php_keys, $body);
            }
            $body = call_user_func_array($cb[$name], $php_args);
        }

        // Basic/standard macros
        else {
            foreach ($keys as $i=>$key) {
                if (is_int(strpos($key, "..")) and $key = trim($key, ".")) {
                    $val = [var_export(array_slice($args, $i), TRUE), join(",", array_slice($args, $i))];
                }
                elseif (!isset($args[$i])) {
                    trigger_error("No macro {$name}() argument for key `$key`");
                    continue;
                }
                elseif (strlen($key)) {
                    $val = [var_export($args[$i],1), $args[$i]];
                }
                $body = preg_replace(["/((?<!\#)\#)\Q$key\E\b/", "/(\s*##\s*)?\b\Q$key\E\b(\s*##\s*)?/"], $val, $body);
            }
            $body = $this->multipass($body);
        }
        return $body;
    }


    /**
     * Pipes substituted content through lookup phase again.
     *  · Switches to delimiter-free `regex` mode in any case.
     *  · Counts recursion loops itself to avoid carrying around state parameter.
     *
     */
    function multipass($body) {
        if ($this->multipass >= 1) {
            $this->multipass--;
            $body = $this->substitute($body, "regex");
            $this->multipass++;
        }
        return $body;
    }

}



/**
 * Token stream helper functions.
 * Used by macro substitution, and partly meant for complex macro callbacks.
 *
 */
class Token {


    /**
     * Combine literal content from token list into string.
     *
     * @param  array   $token
     * @return string
     */
    static function join($token) {
        return join(array_map(function($t) {  return is_array($t) ? $t[1] : $t;  }, $token));
    }


    /**
     * Split argument list on top-level commas.
     *
     * @param  array   $token   Combined argument list
     * @return array            Each argument as individual token list
     */
    static function args($token) {
        $token[] = ",";
        $args = [];
        for ($from = $i = $level = 0, $max = count($token); $i < $max; $i++) {
            if (is_string($t = $token[$i]) or $t = $t[0]) {
                if ($t == "," and !$level) {
                    $args[] = array_slice($token, $from, $i-$from);
                    $from = $i+1;
                }
                elseif ($t == "(") {   //@T support [] and {} as well?
                    $level++;
                }
                elseif ($t == ")") {
                    $level--;
                }
            }
        }
        return $args;
    }


    /**
     * Separate $varnames/expressions from other syntax/content.
     * Returns a paired [["raw..",'$var'], ["raw2..",'$var2']] list.
     * Does not escape collected raw syntax strings by itself.
     *
     */
    static function split_expr($token, $which_tokens=[T_VARIABLE]) {
        $r = [];
        $raw = "";
        foreach ($token as $t) {
            if (in_array($t[0], $which_tokens, TRUE)) {
                $r[] = [$raw, $t[1]];
                $raw = "";
            }
            else {
                $raw .= is_array($t) ? $t[1] : $t;
            }
        }
        if ($raw) {
            $r[] = [$raw, null];
        }
        return $r;
    }

}



/**
 * More readable tracking of #if..#endif nesting levels, with some attributes:

 *   ->level  Condition nesting, one for each #if/ifdef/endif chain.
 *   ->act    Output + conditional section still activatable - *inherited*.
 *   ->end    Activatable if/else leaves passed, no further truthy checks.
 *
 * An #endif terminates a stack, resumes to prior, where act=1 and end=0 still.
 * Note that each #included file gets a fresh condition stack in the ->block()
 * processor. Instances only track conditional nesting within one file. Once
 * act==false, #includes will not be processed anyway.
 *
 */
class ConditionState extends SplStack {

    // start level=0, still active, condition tree not at end
    function __construct() {
        $this->push(["level" => 0, "act" => "1", "end" => 0]);
    }

    // level -1 (endif)
    function done() {
        $this->pop();
    }
    // level +1 (new if), inherit ->act from current state
    function descend() {
        $this->push(["level" => $this->level + 1, "act" => $this->act, "end" => 0]);
    }

    // return state property
    function __get($name) {
        return $this[0][$name];  // #0 is the last pushed stack entry
    }

    // set flag
    function __set($name, $value) {
        $this[0] = [$name => $value] + $this[0];
    }
}



/**
 * #if/#elif expression evaluation. Permits common arithmetics, #define
 * lookups, a few hand-picked PHP callbacks, and decimal/float/tuple
 * comparisons.  It's neither fully CPP compatible, nor in line with
 * preprocess.py; and it's likely pointless featuritis anyway.
 *
 * Uses a trivial shunting-yard sorting method for its tokenized input,
 * then evaluates expressions down to a numeric or booleanish scalar. 
 * Has only minimal error reporting, and operator/func precedence is
 * incomplete/untuned..
 *
 */
class MacroExpression {

    # Expression tokens
    const RX_EXPR = "{
         (?: ([\"\']) (?<string>.*?) \\1)       # plain strings, no escape checking
       | (?<func>    defined|pragma|strpos|stripos|strstr|stristr|is_int|is_float|is_numeric|
                     max|min|file_exists|function_exists|class_exists)
       | (?<op>      >=|<=|==|!=|=~|\&\&|\|\| | [@\-+/*^~&|(),%!<>?:] )     # operators
       | (?<number>  [+-]?\d+(?:\.\d+)*)        # can be multi-tuple version string
       | (?<literal> [\w_]+@?)                  # defined constants
    }mx";


    // Keep #define and #pragma values for reference
    public $defines, $pragma;
    function __construct($defines=[], $pragma=[]) {
        $this->defines = $defines;
        $this->pragma = $pragma;  //@T: just merge both?
    }

    // Run expression through `intval` etc.
    public static function __callStatic($name, $args) {
        list($expr, $proc) = $args;
        $x = new self($proc->defines, $proc->pragma);
        return call_user_func($name, $x->expr($expr));
    }


    /**
     * Tokenize expression into a list of type => value pairs.
     * Convert into RPN, then evaluate operands/operator list.
     *
     */
    public function expr($src) {
        // Shortcuts
        if ($src === "1" or $src === "0") {
            return intval($src);
        }

        // Strip inline comments (just in case, commonly only there for #endif, #else.)
        $src = preg_replace("~/\*.*?\*/~s", "", $src);

        // Regex returns a list of optional [0]=>, [type]=>, [1]=> matches
        preg_match_all(self::RX_EXPR, $src, $tokens, PREG_SET_ORDER);
        $tokens = array_map(function($t) {
            return array_slice($t, -2, 1); // turn last named entry into [type=>value]* list
        }, $tokens);

        // Reorder by precedence into RPN
        $opvals = $this->group($tokens);

        // Apply operations
        return $this->run($opvals);
    }


    /**
     * Run through tokens, spool values+operators into reverse polish notation.
     * [2, 3, 5, *, +]  or for example  [CONST, defined, 1, >=]
     *
     */
    function group($tokens) {

        // operator precedence list
        $precedence = array_flip(explode(" ", "( ) , || && == >= > <= < == =~ != ! ? : & + - @ | ^ * / % ~"));
        $var = [];   // operand queue
        $ops = [];   // operator stack

        // .iteritems() over token list
        while ($t = array_shift($tokens)) {
            $type = key($t);
            $val = current($t);

            // literals
            if ($type == 'literal' or $type == 'number' or $type == 'string') {
                $var[] = [$type, $val];
            }

            // parenthesis grouping
            elseif (($val == "(")) {
                $ops[] = [$type, $val];
            }
            elseif ($val == ")") {
                // push all ops in parens
                while (count($ops) and ['op',"("] !== end($ops)) {
                   $var[] = array_pop($ops);
                }
                array_pop($ops);

                // check for preceding function name
                while (end($ops)[0] == 'func') {
                   $var[] = array_pop($ops);
                }
            }

            // add to operator queue
            else {
                while (count($ops) and $last = end($ops)
                  and (isset($precedence[$val], $precedence[$last[1]]) and $precedence[$val] < $precedence[$last[1]]))
                {
                    $var[] = array_pop($ops);
                }
                $ops[] = [$type, $val];
            }
        }

        // append remaining operators
        $var = array_merge($var, array_reverse($ops));
        return $var;
    }


    /**
     * Plop values of list, apply expressions or function callbacks as ordered.
     * (Operand association overriden here via $pop(NN) parameter.)
     *
     */
    function run(&$opvals) {

        // prepare value stack
        $sum = [];
        $pop = function($prev=1) use (&$sum) { 
            assert('count($sum) >= $prev /* Empty value stack, unbalanced expressions. */'); # OutOfBounds
            $v = array_splice($sum, -$prev, 1, []);
            return current($v);
        };

        // Dispatch value literals as encountered operations
        // (Rather should carry the token types over. Would also simplify ordering operands.)
        while (count($opvals)) {
            list($type, $val) = array_shift($opvals);

            if ($type == 'op'){
                switch ($val) {
                    case "@":  continue 2;
                    case "!":  $r = !$pop();  break;
                    case "~":  $r = ~intval($pop());  break;
                    case "+":  $r = $pop(1) + $pop(1);  break;
                    case "-":  $r = -$pop(); $r += $pop();  break;
                    case "*":  $r = $pop(1) * $pop(1);  break;
                    case "/":  $r = $pop(2) / $pop(1);  break;
                    case "^":  $r = intval($pop(1)) ^ intval($pop(1));  break;
                    case "&":  $r = $pop(1) & $pop(1);  break;
                    case "|":  $r = $pop(1) | $pop(1);  break;
                    case "%":  $r = $pop(1) % $pop(1);  break;
                    case "&&": $x = $pop(); $y = $pop(); $r = intval($x && $y); break;
                    case "||": $x = $pop(); $y = $pop(); $r = intval($x || $y); break;
                    case "==": $r = $pop(1) == $pop(1);  break;
                    case "!=": $r = $pop(1) != $pop(1);  break;
                    case "?":  $r = $pop(1); break;
                    case ":":  $y = $pop(1); $x = $pop(1); $c = $pop(1); $r = $c ? $x : $y;  break;
                    case ">":  $r = version_compare($pop(), $pop()) < 0;   break;
                    case "<":  $r = version_compare($pop(), $pop()) > 0;   break;
                    case ">=": $r = version_compare($pop(), $pop()) <= 0;  break;
                    case "<=": $r = version_compare($pop(), $pop()) >= 0;  break;
                    case "=~": $r = preg_match("\1{$pop()}\1sim", $pop());   break;
                    case ",":  $r = array_merge((array)$pop(), (array)$pop()); break;
                    default: assert('false /* unhandled operator `$val` */');
                }
            }

            // Run function calls.  Any previous params might have been arrayified through `,` operator
            elseif ($type == 'func' and $this->allowed_func($val)) {
                $r = call_user_func_array($val, array_reverse( (array)$pop() ) );  /*p*/$val="FUNC";
            }
            // Eagerly expand literals if constant name. But skip if `defined` operation follows.
            elseif (isset($this->defines[$val]) and (empty($opvals) or reset($opvals) !== ['func',"defined"])) {
                $r = $this->defines[$val][1];
            }
            // Just numbers, strings or constants
            else {
                $r = $val;
            }

#           printf(" %26s ➫ %-8s 🔛  %16s ← %4s\n", json_encode($opvals), "{$val}", json_encode($sum), json_encode($r));
            $sum[] = $r;
        }

        // Only one remaining data element if expression correctly processed
        if (count($sum) == 1) {
            return current($sum);
        }
        else {
            trigger_error("Expression leftovers, result discarded.");
            return;  //throw new OutOfBoundsException("Unbalanced expression result.");
        }
    }


    /**
     * Custom pseudo functions for expressions.
     * → defined(CONST) || pragma(optionval)
     *
     */
    protected function defined($name) {
        return !is_null($name) && isset($this->defines[$name]);
    }
    protected function pragma($name) {
        return isset($this->pragma[$name]) ? $this->pragma[$name] : NULL;
    }
    
    /**
     * Check for allowed functions; either global function or method (rewrite [$val] param).
     *
     */
    protected function allowed_func(&$val) {
        if (is_callable([$this, $val])) {
            $val = [$this, $val];
        }
        if (is_callable($val)) {
            return TRUE;
        }
    }
}


/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Mario Salzer
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

} # namespace \io
