firebox_compiler.php

19.2 KB, 592 lines

<?php

/* Firebox - PHP Web Application Framework
   Copyright (C) 2007 Andrew J. Fabian
   All rights reserved.
*/

function fbx_compile($filename)
{
    global $fbx, $content;
    
    $is_controller = substr($filename, 0, 10) == 'controller';
    $is_model = substr($filename, 0, 5) == 'model';
    $is_view = !$is_controller && !$is_model;
    
    // load and compile our list of libraries, and their source files
    // this also compiles require() statements so that relative paths are made explicit to site_root/lib
    
    if (!isset($fbx['libs']))
    {
        $fbx['libs'] = fbx_load_libs($fbx['site_root'] . 'lib/');
    }
    
    if (fbx_url_option('fbx_compiler_debug')) $content['debugpanes']['compiler'] .= "<br><br>Compiling $filename<br><br>";
    
    // if we've already compiled this file during this page request, quit immediately
    
    if (isset($fbx['compiled']) && false !== array_search($filename, $fbx['compiled'])) return;
    else $fbx['compiled'][] = $filename;

    // figure out what filename we're writing to
    
    $outputfile = $fbx['site_root'] . 'parsed/' . ($fbx['production'] ? 'prod' : 'dev') . '/' . str_replace("/", ".", $filename);

    // if that file exists and is up-to-date, return now
    
    if (file_exists($outputfile) && filemtime($filename) <= filemtime($outputfile))
    {
        fbx_debug("Skipping compile of $filename because it's parsed copy is still current.", __FILE__, __LINE__);
        return;
    }

    fbx_debug("Compiling $filename", __FILE__, __LINE__);

    // read in the source file
    
    fbx_debug("Reading file", __FILE__, __LINE__);
    $input = join('', file($filename));
        
    // turn the file into lexemes
    
    fbx_debug("Running lexical parser", __FILE__, __LINE__);
    $lex = fbx_lexical_parser($input);
        
    if (fbx_url_option('fbx_compiler_debug'))
    {
        $content['debugpanes']['compiler'] .= "FBX_Lexical_Parser<br>";
        $content['debugpanes']['compiler'] .= "CurrentState        Pos   Type                Content<br>";
        $content['debugpanes']['compiler'] .= "------------------- ----- ------------------- ----------------------------------<br>";
        foreach ($lex as $lexeme) $content['debugpanes']['compiler'] .= str_pad($lexeme['state'], 20, ' ', STR_PAD_RIGHT) 
                                                                     . str_pad($lexeme['pos'], 6, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($lexeme['type'], 20, ' ', STR_PAD_RIGHT)
                                                                     . str_replace("\n", "<br>", htmlentities(wordwrap(str_replace("\n", ' ', $lexeme['content']), 80, "\n" . str_repeat(' ', 46)))) . "<br>";
    }
    
    // build a list of code blocks, and where they start and end
    
    fbx_debug("Identifying code blocks", __FILE__, __LINE__);
    $blocks = fbx_get_blocks_from_lex($lex);
        
    if (fbx_url_option('fbx_compiler_debug'))
    {
        $content['debugpanes']['compiler'] .= "<br>FBX_Get_Blocks_From_Lex<br>";
        $content['debugpanes']['compiler'] .= "BlockType      Detail           Depth StartIndex,Pos EndIndex,Pos<br>";
        $content['debugpanes']['compiler'] .= "-------------- ---------------- ----- -------------- ---------------<br>";
        foreach ($blocks as $block) $content['debugpanes']['compiler'] .= str_pad($block['typeindex'] . ':' . $block['type'], 15, ' ', STR_PAD_RIGHT) 
                                                                       . @str_pad($block['nameindex'] . ':' . $block['name'], 20, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($block['depth'], 3, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($block['startindex'] . ':' . $block['start'], 15, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($block['endindex'] . ':' . $block['end'], 15, ' ', STR_PAD_RIGHT) . "<br>";
    }
    
    // look for each time a function is called
    
    fbx_debug("Identifying function references", __FILE__, __LINE__);
    $refs = fbx_get_function_references_from_lex($lex);
    if (fbx_url_option('fbx_compiler_debug'))
    {
        $content['debugpanes']['compiler'] .= "<br>FBX_Get_Function_References_From_Lex<br>";
        $content['debugpanes']['compiler'] .= "Function            Pos<br>";
        $content['debugpanes']['compiler'] .= "------------------- -----<br>";
        foreach ($refs as $ref) $content['debugpanes']['compiler'] .= str_pad($ref['function'], 20, ' ', STR_PAD_RIGHT) 
                                                                     . $ref['index'] . ':' . $ref['pos'] . "<br>";
    }
    
    // find the contents of our pre and post functions, if they exist; and where to cut to remove them from the output

    fbx_debug("Getting contents of pre() and post() functions, if thsey exist.", __FILE__, __LINE__);
           
    $pre = '';
    $post = '';
    
    foreach ($blocks as $block)
    {
        if ($block['type'] == 'function' && $block['name'] == 'pre')
        {
            $pre = substr($input, $block['start']+1, $block['end'] - $block['start'] - 1);
            $prestartindex = $block['typeindex'];
            $preendindex = $block['endindex'];
        }
        if ($block['type'] == 'function' && $block['name'] == 'post')
        {
            $post = substr($input, $block['start']+1, $block['end'] - $block['start'] - 1);
            $poststartindex = $block['typeindex'];
            $postendindex = $block['endindex'];
        }
    }
    
    // translate names in our function definitions now, and insert our global variables and !function_exists() blocks.
    // if this is a model or view file, only translate function names matching the file name.  that way the file can still define
    // it's own functions for internal or site use.  also translate it's test.
    
    // first, the library requirements for pre- and post-functions, since they apply to all other functions
    
    $pre_post_requires = array();
    foreach ($blocks as $block)
    {
        if ($is_controller && (@$block['type'] == 'function' && @$block['name'] == 'pre' || @$block['name'] == 'post'))
        {
            foreach($refs as $ref)
            {
                if ($ref['index'] > $block['startindex'] && $ref['index'] < $block['endindex'])
                {
                    if (isset($fbx['libs'][$ref['function']]))
                    {
                        $pre_post_requires[$ref['function']] = "\trequire_once('{$fbx['libs'][$ref['function']]}');\n";
                    }
                }
            }
        }
    }
    
    // now, the main iteration through all functions
    
    $functions = array();
    foreach ($blocks as $block)
    {
        if ($block['type'] == 'function' && $block['name'] != 'pre' && $block['name'] != 'post')
        {
            if ($is_controller || $block['name'] == substr(basename($filename), 0, -4) || $block['name'] == 'test_' . substr(basename($filename), 0, -4))
            {
                $newname = str_replace(array('.php','/','.'), '_', $filename) . $block['name'];
                if (substr($block['name'], 0, 5) == 'test_') $newname = 'test_' . str_replace(array('.php','/','.'), '_', $filename) . substr($block['name'], 5);
                $functions[$block['name']] = $newname;
                $requires = $pre_post_requires;
                foreach($refs as $ref)
                {
                    if ($ref['index'] > $block['startindex'] && $ref['index'] < $block['endindex'])
                    {
                        if (isset($fbx['libs'][$ref['function']]))
                        {
                            $requires[$ref['function']] = "\trequire_once('{$fbx['libs'][$ref['function']]}');\n";
                        }
                    }
                }
                fbx_debug("Renaming function {$block['name']} to $newname", __FILE__, __LINE__);
                $lex[$block['typeindex']]['content'] = "if (!function_exists('$newname')) { function";
                $lex[$block['nameindex']]['content'] = $newname;
                $lex[$block['startindex']]['content'] = "{\n\tglobal \$fbx, \$content;\n" . join('', $requires) . "$pre\n";
                $lex[$block['endindex']]['content'] = "\n$post\n}}";
            }
            else 
            {
                fbx_debug("Not renaming function {$block['name']} because it's name doesn't match the file name and this isn't a controller.", __FILE__, __LINE__);
            }
        }
    }
    
    // translate names in function references

    foreach ($refs as $ref)
    {
        if (false !== array_search($ref['function'], array_keys($functions)))
        {
            fbx_debug("Updating function reference to {$ref['function']}", __FILE__, __LINE__);
            $lex[$ref['index']]['content'] = $functions[$ref['function']];
        }
    }
    
    // if the file doesn't define any functions, it still needs the firebox global variables
    
    if (!count($functions))
    {
        array_unshift($lex, array('content' => '<? global $fbx, $content; ?>'));
    }
    
    // build out output by combining all of the modified lexemes back togethers
    
    fbx_debug("Building and writing output", __FILE__, __LINE__);
    $output = '';
    for ($i=0; $i<count($lex); $i++)
    {
        if ( !($pre && $i >= $prestartindex && $i<=$preendindex) && !($post && $i >= $poststartindex && $i <= $postendindex) )
        {
            $output .= $lex[$i]['content'];
        }
    }
        
    // compile the tree back into a string, and write out compiled file
    
    $fh = fopen($outputfile, 'w') or fbx_error("Couldn't open $outputfile for writing");
    fbx_debug("Writing out $outputfile", __FILE__, __LINE__);
    fputs($fh, $output);
    fclose($fh);
    
    if (!empty($content['debugpanes']['compiler']))
    {    
        $content['debugpanes']['compiler'] = str_replace(' ', '&nbsp;', $content['debugpanes']['compiler']);
        if (fbx_url_option('fbx_debug_to_screen'))
        {
            echo $content['debugpanes']['compiler'];
            $content['debugpanes']['compiler'] = '';
        }
    }
}

// this turns a string into a series of lexemes

function fbx_lexical_parser($input)
{
    global $fbx;
    
    // load patterns for lexical analyzer
    
    require_once($fbx['fbx_root'] . 'firebox_compiler_data.php');

    // this holds what kind of block we're currently in

    $stack = array('html');
    
    // this is the position our current block starts at
    
    $left = 0;
        
    do
    {
        // shortcut to top of stack
    
        $state = $stack[count($stack)-1];
        
        // find the next point of interest    
        if (!count($fbx['lex'][$state])) fbx_error("Compiler error: no data for state $state at offset $left.");    
        $right = fbx_lexical_parser_next_lexeme($input, ($fbx['lex'][$state]), $left, $match, $contents);

        if ($right !== false)
        {
            
            $lex[] = array('state' => $state, 'type' => $match, 'content' => $contents, 'pos' => $right);
            
            // figure out what state we're in now
            
            if (is_array($fbx['lex'][$state][$match]) && $fbx['lex'][$state][$match][1] == 'POP')
            {
                array_pop($stack);
            }
            
            else if (is_array($fbx['lex'][$state][$match]) && $fbx['lex'][$state][$match][1] == 'POP2')
            {
                array_pop($stack); array_pop($stack);
            }
            
            else if (is_array($fbx['lex'][$state][$match]) && $fbx['lex'][$state][$match][1])
            {
                array_push($stack, $fbx['lex'][$state][$match][1]);
            }
            
            if ($match == 'heredocident')
            {
                $fbx['lex']['heredoc']['text'] = array('/((.|\n)+?)\n' . $contents . '/', false, 1);
                $fbx['lex']['heredoc']['endofdoc'] = array('/\n(' . $contents . ')/', 'POP2', 1);
            }
            
            // get ready to start again
            
            $left = $right + strlen($contents);
        }
            
    } while ($right !== false);

    return($lex);
}

// this function returns the position of the first match from an array of search strings.  It also sets an variable with the matching string.

function fbx_lexical_parser_next_lexeme($haystack, $needles, $offset, &$match, &$lexcontent)
{
    $bestpos = false;
    $bestmatch = false;
    $bestcontent = false;

    foreach ($needles as $name => $data)
    {
        if (is_array($data)) 
        {
            $pattern = $data[0];
        }
        else 
        {
            $pattern = $data;
        }
                
        if ($pattern[0]=='/')
        {
            $mypos = preg_match($pattern, $haystack, $mycontent, null, $offset);
            if ($mypos)
            {
                if (is_array($data)) {
                    $mycontent = $mycontent[count($data) == 3 ? $data[2] : 0];
                }
                else {
                    $mycontent = $mycontent[0];
                }
                $mypos = strpos($haystack, $mycontent, $offset);
            }
            else 
            {
                $mypos = false;
            }
        }
        else
        { 
            $mypos = strpos($haystack, $pattern, $offset);
            if ($mypos !== false) $mycontent = substr($haystack, $mypos, strlen($pattern));
        }
        
        if ($mypos !== false)
        {
            if ($mypos < $bestpos || $bestpos === false)
            {
                $bestpos = $mypos;
                $bestmatch = $name;
                $bestcontent = $mycontent;
            }
        }
    }

    
    $match = $bestmatch;
    $lexcontent = $bestcontent;
    return($bestpos);
}

// this builds a table of code blocks {.....}; where they start and end, 
// what type they are, what their nesting depth is, and their character positions

function fbx_get_blocks_from_lex($lex)
{
    $blocks = array();
    $counter = 0;
    $depth = 0;
    $word = '';
    $wordindex = 0;

    $block_identifiers = array('if', 'else', 'while', 'do', 'for', 'foreach', 'function', 'switch', 'try', 'catch');
    
    foreach ($lex as $lexeme)
    {
        if ($lexeme['state'] == 'php' && $lexeme['type'] == 'o_curly')
        {
            $backdepth = 0;
            for ($i=$counter; $i>=0; $i--)
            {
                if ($lex[$i]['state'] == 'php')
                {
                    if ($lex[$i]['type'] == 'o_paren' || $lex[$i]['type'] == 'o_bracket')
                    {
                        $backdepth++;
                    }
                    if ($lex[$i]['type'] == 'c_paren' || $lex[$i]['type'] == 'c_bracket')
                    {
                        $backdepth--;
                    }
                    if ($lex[$i]['type'] == 'word' && $backdepth == 0)
                    {
                        $lastword = $word;
                        $lastwordindex = $wordindex;
                        $wordindex = $i;
                        $word = $lex[$i]['content'];
                        if (false !== array_search($word, $block_identifiers))
                        {
                            $type = $word;
                            $typeindex = $i;
                            if ($type == 'function') 
                            {
                                $name = $lastword;
                                $nameindex = $lastwordindex;
                            }
                            break;
                        }
                    }
                }
            }
            
            $blocks[] = array('start' => $lexeme['pos'], 'startindex' => $counter, 'type' => $type, 'typeindex' => $typeindex, 'depth' => $depth, 'end' => false, 'endindex' => false);
            if ($type == 'function') $blocks[count($blocks)-1]['name'] = $name;
            if ($type == 'function') $blocks[count($blocks)-1]['nameindex'] = $nameindex;
            $depth++;
        }
        
        if ($lexeme['state'] == 'php' && $lexeme['type'] == 'c_curly')
        {
            $depth--;
            for ($i=count($blocks)-1; $i>=0; $i--)
            {
                if ($blocks[$i]['depth'] == $depth)
                {
                    $blocks[$i]['end'] = $lexeme['pos'];
                    $blocks[$i]['endindex'] = $counter;
                    break;
                }
            }
        }        
        $counter++;
    }
    return($blocks);
}

// given an array of lexemes, this ruturns a list of all of the function references contained within,
// and their index in the array and character position

function fbx_get_function_references_from_lex($lex)
{
    $refs = array();
    $block_identifiers = array('if', 'else', 'while', 'do', 'for', 'foreach', 'function', 'switch', 'case', 'break');
    
    for ($i=0; $i<count($lex); $i++)
    {
        if ($lex[$i]['state'] == 'php' && $lex[$i]['type'] == 'word')
        {
            if ( !($lex[$i-1]['state'] == 'php' && $lex[$i-1]['type'] == 'dollar') &&
                 !($lex[$i-1]['state'] == 'php' && $lex[$i-1]['type'] == 'space' && $lex[$i-2]['state'] == 'php' && $lex[$i-2]['type'] == 'dollar') &&
                 (false === array_search($lex[$i]['content'], $block_identifiers)) &&
                 !($lex[$i-1]['state'] == 'php' && $lex[$i-1]['type'] == 'space' && $lex[$i-2]['state'] == 'php' && $lex[$i-2]['content'] == 'function')
               )
            {
                   $refs[] = array('function' => $lex[$i]['content'], 'index' => $i, 'pos' => $lex[$i]['pos']);
            }
        }
    }

    return($refs);
}

// this scans the libs directory, figures out which functions are defined in which files
// it then "compiles" then into parsed, by updating the paths in include and require

function fbx_load_libs()
{
    global $fbx;
    
    $libs = array();
    
    if (fbx_url_option('fbx_compiler_debug')) $content['debugpanes']['compiler'] .=    "Scanning for updated libraries<br>";
    
    $files = fbx_dir_tree_files($fbx['site_root'] . 'lib/', '/.*\.php$/');
    foreach ($files as $lib)
    {
        $output_file = $fbx['site_root'] . 'parsed/' . @$mode . 'lib.' . basename($lib);
        if (file_exists($output_file)) {
            if (filemtime($lib) > filemtime($output_file))
            {
                if (fbx_url_option('fbx_compiler_debug')) $content['debugpanes']['compiler'] .=    "Library $lib has changed.  Recompiling.<br>";
                $libs = array_merge($libs, fbx_get_function_names($lib));
            }
        }
        else {
            // MARKER
            if (fbx_url_option('fbx_compiler_debug')) $content['debugpanes']['compiler'] .= "Library $lib has never been compiled.  Compiling.<br>";
            $libs = array_merge($libs, fbx_get_function_names($lib));
        }
    }
    
    return($libs);
}

// this function compiles libraries, writes out the parsed file, and returns a list of functions defined, pointing to that parsed file

function fbx_get_function_names($filename)
{
    global $fbx;
    
    // figure out what filename we're writing to
    
    $outputfile = $fbx['site_root'] . 'parsed/' . ($fbx['production'] ? 'prod' : 'dev') . '/' . str_replace("/", ".", substr($filename, strlen($fbx['site_root'])));
    
    // if that file exists and is up-to-date, run it and return the results immediately
    
    if (file_exists($outputfile) && filemtime($filename) <= @filemtime($outputfile))
    {
        fbx_debug("Skipping compile of $filename because it's parsed copy is still current.", __FILE__, __LINE__);
        return(require($outputfile));
    }

    fbx_debug("Compiling library $filename", __FILE__, __LINE__);

    // read in the source file
    
    fbx_debug("Reading file", __FILE__, __LINE__);
    $input = join('', file($filename));
        
    // turn the file into lexemes
    
    fbx_debug("Running lexical parser", __FILE__, __LINE__);
    $lex = fbx_lexical_parser($input);
        
    if (fbx_url_option('fbx_compiler_debug'))
    {
        $content['debugpanes']['compiler'] .= "FBX_Lexical_Parser<br>";
        $content['debugpanes']['compiler'] .= "CurrentState        Pos   Type                Content<br>";
        $content['debugpanes']['compiler'] .= "------------------- ----- ------------------- ----------------------------------<br>";
        foreach ($lex as $lexeme) $content['debugpanes']['compiler'] .= str_pad($lexeme['state'], 20, ' ', STR_PAD_RIGHT) 
                                                                     . str_pad($lexeme['pos'], 6, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($lexeme['type'], 20, ' ', STR_PAD_RIGHT)
                                                                     . str_replace("\n", "<br>", htmlentities(wordwrap(str_replace("\n", ' ', $lexeme['content']), 80, "\n" . str_repeat(' ', 46)))) . "<br>";
    }

    // build a list of code blocks, and where they start and end
    
    fbx_debug("Identifying code blocks", __FILE__, __LINE__);
    $blocks = fbx_get_blocks_from_lex($lex);
        
    if (fbx_url_option('fbx_compiler_debug'))
    {
        $content['debugpanes']['compiler'] .= "<br>FBX_Get_Blocks_From_Lex<br>";
        $content['debugpanes']['compiler'] .= "BlockType      Detail           Depth StartIndex,Pos EndIndex,Pos<br>";
        $content['debugpanes']['compiler'] .= "-------------- ---------------- ----- -------------- ---------------<br>";
        foreach ($blocks as $block) $content['debugpanes']['compiler'] .= str_pad($block['typeindex'] . ':' . $block['type'], 15, ' ', STR_PAD_RIGHT) 
                                                                       . @str_pad($block['nameindex'] . ':' . $block['name'], 20, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($block['depth'], 3, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($block['startindex'] . ':' . $block['start'], 15, ' ', STR_PAD_RIGHT)
                                                                       . str_pad($block['endindex'] . ':' . $block['end'], 15, ' ', STR_PAD_RIGHT) . "<br>";
    }
    
    // identify all of the function definitions
    $functions = array();
    foreach ($blocks as $block)
    {
        if ($block['type'] == 'function')
        {
             $functions[$block['name']] = $filename;
        }
    }
    
    // write out the parsed file
    
    $fh = fopen($outputfile, 'w') or die("Couldn't open output file");
    fputs($fh, '<?php return(' . var_export($functions, true) . ');');
    fclose($fh);
    
    // return our array of functions
    
    return(require($outputfile));
        
}

// this generates an array of all the files under a directory

function fbx_dir_tree_files($dir, $match_regex='')
{
     $files = array();
     $dh = opendir($dir) or die("Couldn't open $dir");
     while ($rec = readdir($dh))
     {
         if ($rec !== false && $rec != '.' && $rec != '..')
         {
             if (is_dir($dir . $rec))
             {
                 $files = array_merge($files, fbx_dir_tree_files($dir . $rec . '/', $match_regex));
             }
             elseif (is_file($dir . $rec))
             {
                if (empty($match_regex) || preg_match($match_regex, $rec))
                {
                     $files[] = $dir . $rec;
                }
             }
         }
     }
     closedir($dh);
     return($files);
}

All files · Overview