Program.php

<?php

namespace Tlf\Lexer2;

/**
 * Manages state & executes code
 */
class Program {


    /**
     * Array of Program commands to call when execute_commands has been set false. 
     * This is cleared after each instruction set is run.
     * If a directive executes all of its commands and is not explicitly stopped, these will NOT be run.
     */
    public array $end_execute_commands_hooks = [];

    /**
     * Array of callables to call prior to executing an instruction set.
     */
    public array $will_execute_command_hooks = [];

    /**
     * Objects available to a Parser program.
     */
    public array $objects = [];
    /**
     * Local Variables, accessible by the active instruction set.
     * array<string var_name, mixed value>
     */
    public array $local_vars = [];

    /**
     * Namespaced code that performs parsing
     *
     * array<string namespace, array directive>
     * directive: array<string name, array instruction_sets>
     * instruction_sets: array<string name, array commands>
     * commands: array<int index, array command>
     * command: contains string object, string method, and array args.
     */
    public array $directives = [];

    /**
     * Stack of started & unstarted directives
     * array<int stack_index, array started_and_unstarted_directives>
     * started_and_unstarted_directives: contains array unstarted & array started
     */
    public array $directive_stack = [];

    /**
     * Stack of ASTs 
     * array<int index, array Asts>
     * Asts is array<int index, \Tlf\Lexer2\Ast ast>
     */
    public array $ast_stack = [];

    /**
     * Stack of state information
     *
     * array<int index, mixed state> state should usually be a string
     */
    public array $state_stack = [];

    /**
     * Whichever Directive is being currently processed. Null if not currently executing.
     */
    public ?array $active_directive = null;

    /**
     * Instruction sets will be run when this is true. 
     * Set this false to prevent the next instruction set from running.
     * To halt the current instruction set, so no more commands are executed, set execute_commands = false.
     */
    public bool $execute_instructions = true;

    /**
     * The current instruction set will continue to run when this is true.
     * Set false to halt the current instruction set from running more commands.
     * To prevent any remaining instruction sets from running, set execute_instructions = false.
     */
    public bool $execute_commands = true;

    /**
     * Array of languages & initial directives
     * key language is the language object
     * key initial_directives is an array of directive names within the language
     */
    public array $languages = [];

    /**
     * Directives to add to the 'unstarted' stack before execution begins, after other setup is complete.
     * array<int index, array info> each index contains the paramaters for `addUnstartedDirectiveToStack($fqn, $directive)`
     */ 
    protected array $directives_to_initialize = [];

    /**
     * 
     */
    public function setObject(string $object_name, object|array $object){
        $this->objects[$object_name] = $object;
    }

    public function addUnstartedDirectiveToStack(string $fully_qualified_name, array $directive){
        $parts = explode(":",$fully_qualified_name);
        $namespace = $parts[0];
        $name = $parts[1];
        if (isset($directive['is'])){
            foreach ($directive['is'] as $directive_command){
                if ($directive_command['args'][0] == null){
                    $directive_command['args'][0] = $namespace;
                }
                $this->execute_command($directive_command);
            }
            return;
        }

        if (count($this->directive_stack)==0)$this->directive_stack[] = ['unstarted'=>[], 'started'=>[]];
        $head = &$this->directive_stack[count($this->directive_stack)-1];
        $directive['--context--'] = [
            'namespace'=>$namespace,
            'name'=>$name,
        ];
        $head['unstarted'][] = $directive;
    }

    /**
     *
     * @param $directives_to_add_to_stack array<int index, string directive_name>
     */
    public function addLanguage(\Tlf\Lexer2\Language $language, array $directives_to_add_to_stack = ['main']){
        $this->languages[] = ['language'=>$language, 'initial_directives' => $directives_to_add_to_stack];
        $directives = $language->get_directives();
        $this->directives[$language->get_namespace()] = $directives;

        
        foreach ($directives_to_add_to_stack as $directive_name){
            if (!isset($directives[$directive_name]))continue;
            //echo "ADD $directive_name TO ".$language->get_namespace();
            $this->directives_to_initialize[] = [$language->get_namespace().':'.$directive_name, $directives[$directive_name]];
        }
    }


    /**
     * Setup initial directives
     */
    public function initialize(){
        foreach ($this->directives_to_initialize as $index => $to_init){
            $this->addUnstartedDirectiveToStack($to_init[0], $to_init[1]);
        }
        $this->directives_to_initialize = [];
    }

    /**
     * Executes the program in its current state.
     */
    public function execute(){
        $this->execute_instructions = true;

        $this->print_directive_stack_info();
        $this->print_ast_stack_info();

        echo "\n  \033[4;32mEXECUTE DIRECTIVES\033[0m  ";
        foreach ($this->get_active_directives() as $directive){
            $this->local_vars = [];
            $this->active_directive = $directive;
            $this->execute_commands = true;

            if (!$this->execute_instructions)break;
            $active_instruction_set = $this->get_active_instruction_set_name();
            if (!isset($directive[$active_instruction_set]))continue;
            $this->will_execute_commands($directive, $active_instruction_set);

            
            $directive_name = $directive['--context--']['namespace'].':'.$directive['--context--']['name'];

            echo "\n   \033[1;35m$directive_name\033[0m  ";
            foreach ($directive[$active_instruction_set] as $command){
                if (!$this->execute_commands){
                    echo "\n     \033[44mEXECUTION STOPPED\033[0m";
                    
                    if (count($this->end_execute_commands_hooks) > 0){
                        echo "\n     \033[44mSTOP EXECUTION HOOKS\033[0m";
                        //echo " HOOKS";
                        //echo "\n   \033[4;32mHOOKS ON EXECUTION STOPPED\033[0m  ";
                    }
                    echo ""; // end color
                    foreach ($this->end_execute_commands_hooks as $command_to_run){
                        $this->execute_command($command_to_run, 2);
                    }
                    break;
                }
                $this->execute_command($command, 1);
            }
        }
        $this->end_execute_commands_hooks = [];
    }

    /**
     * Call any will_execute hooks
     */
    public function will_execute_commands(array $directive, string $instruction_set_name){
        foreach ($this->will_execute_command_hooks as $callable){
            $callable($this, $directive, $instruction_set_name);
        }
    }

    /**
     * Execute a program command
     * @param $command an array with entry '--is_command--' => true + the other stufff
     * @return the return value of the command.
     */
    public function execute_command(array $command, int $recursion_level = 0): mixed {

        // PRINT COMMAND INFORMATION
        echo "\n   ".$this->get_printable_command($command, $recursion_level);

        $method = $command['method'];
        $args = $command['args'];

        $object_name = $command['object'];
        $parts = explode(".", $object_name);
        if (count($parts)==1){
            if (!isset($this->objects[$object_name])){
                echo "\n\n\033[0;31mERROR:\033[0m Object '{$object_name}' does not exist. Cannot call {$object_name}.{$method}";
                echo "\n\n";
                //return;
            }
            $object = $this->objects[$object_name];
        }else {
            reset($parts);
            $object = $this->objects[current($parts)];
            while ($prop = next($parts)){
                $object = $object->$prop;
            }
        }

        $recursion_level++;
        // BUILD FINAL ARGS LIST
        $send_args = [];
        foreach ($args as $index=>$value){
            $send_args[] = 
                $this->is_command($value) 
                ? $this->execute_command($value, $recursion_level) 
                : $value;
        }


        try {
            if (!is_object($object)){
                var_dump($object);
                throw new \Exception("WHAT");
            }
            if (!method_exists($object, $method)){
                echo "\n\n\033[0;31mERROR:\033[0m Method '{$method}' does not exist on {$object_name}. Cannot call {$object_name}.{$method}";
                echo "\n\n";
                //return;
            }
            $ret = $object->$method(...$send_args);
        } catch (\ArgumentCountError $e) {
            $actual_num = count($send_args);
            $expected_num = preg_filter('/^.+and exactly ([0-9]+) expected.*$/','$1',$e->getMessage());
            if (!is_numeric($expected_num))$expected_num = "[error]";
            echo "\n\n\033[0;31mERROR:\033[0m {$object_name}.{$method} expects {$expected_num} paramaters, but received {$actual_num}";

            $refMethod = new \ReflectionMethod($object, $method);
            $file = $refMethod->getFileName();
            $line = $refMethod->getStartLine();
            $params = "    ".implode("\n    ", $refMethod->getParameters());

            echo "\nMethod defined at:"
                ."\nLine: $line"
                ."\nFile: $file"
                ."\nWith Paramaters: "
                ."\n$params"
                . "\nWith Docblock: "
                .$refMethod->getDocComment();

            echo "\n\n";
            throw $e;
        }
        return $ret;
    }


    protected function print_ast_stack_info(){
        echo "\n  \033[4;32mAST STACK\033[0m  ";
        //$get_directive_names = function(array $directive){
            //return $directive['--context--']['namespace'].':'.$directive['--context--']['name'];
        //};
        $get_ast_keys = function(string $key, mixed $value){
            if (is_array($value)){
                return $key.'['.count($value).']';     
            } else if (is_object($value)) {
                return "{$key}";
            } else {
                return $key;
            }
        };
        foreach ($this->ast_stack as $layer_num => $ast){
            $tree = $ast->getTree();
            $class = get_class($ast);
            $type = $tree['type'];
            //$keys = implode(", ",array_keys($tree));
            $keys = array_map($get_ast_keys, array_keys($tree), $tree);
            echo "\n    \033[1;35mStack Index $layer_num\033[0m  ";
            echo "\n      Class: ".$class;
            echo "\n      Type: ".$type;
            echo "\n      Keys: ".implode(", ",$keys);
            //echo "\n      A: ".implode(", ",$unstarted);
            //echo "\n      Started: ".implode(", ",$started);
        }
    }
    protected function print_directive_stack_info(){
        echo "\n  \033[4;32mDIRECTIVE STACK\033[0m  ";
        $get_directive_names = function(array $directive){
            return $directive['--context--']['namespace'].':'.$directive['--context--']['name'];
        };
        foreach ($this->directive_stack as $layer_num => $directives){
            $started = array_map($get_directive_names,$directives['started']);
            $unstarted = array_map($get_directive_names,$directives['unstarted']);
            echo "\n    \033[1;35mLayer $layer_num\033[0m  ";
            echo "\n      Unstarted: ".implode(", ",$unstarted);
            echo "\n      Started: ".implode(", ",$started);
        }
    }
    protected function get_printable_command(array $command, int $recursion_level = 0): string {
        $object_name = $command['object'];
        $method = $command['method'];
        $args = $command['args'];


        
        $printable_cmd = str_repeat("  ", $recursion_level);
        $printable_cmd .= $object_name.'.'.$method;


        $recursion_level++;
        foreach ($args as $index=>$arg){
            if (!is_array($arg))$printable_cmd .= " ".$arg;
            else if ($this->is_command($arg)){
                $printable_cmd .= " !".$arg['object'].'.'.$arg['method'].' ...';
            } else {
                $printable_cmd .= ":";
                $prefix = str_repeat("  ", $recursion_level+1);
                foreach ($arg as $key=>$value){
                    if ($this->is_command($value)){
                        $printable_cmd .= "\n $prefix$key=!". trim($this->get_printable_command($value, $recursion_level));
                    } else {
                        $printable_cmd .= "\n $prefix$key=".$value;
                    }
                }
                //$printable_cmd .= "    ".print_r($arg, true);
            }
        }
        return $printable_cmd;
    }

    /**
     * Check if the value is a program command.
     *
     * @return bool true if $value is a program command. false otherwise
     */
    public function is_command(mixed $value): bool {
        if (is_array($value) 
            && isset($value['--is_command--'])
            && $value['--is_command--'] === true
        ){
            return true;
        }
        return false;
    }

    public function are_directives_started(): bool {
        $head = $this->directive_stack[count($this->directive_stack)-1];

        if (count($head['started']) > 0){
            return true;
        } else {
            return false;
        }

    }

    /**
     * Get an array of 'started' directives, if any. Otherwise, return array of 'unstarted' directives.
     */
    public function get_active_directives(): array {
        if (($stack_size = count($this->directive_stack))==0){

            echo "\n   \033[0;101m\033[1;30m ERROR \033[0m   \n";
            echo "    \033[4;31mCannot get the list of active directives, because the directive stack is empty.\033[0m";
            echo "\n\n\n";


            throw new \Exception("Cannot get_active_directive() because the directive stack is empty.");
        }
        $head = $this->directive_stack[$stack_size-1];

        $active_directives = [];
        if (count($head['started']) > 0){
            return $head['started'];
        } else {
            return $head['unstarted'];
        }

        return $active_directives;
    }

    /**
     * Return 'if_started' or 'if_unstarted' depdning on which instruction set list is active
     */
    public function get_active_instruction_set_name(): string {
        $head = $this->directive_stack[count($this->directive_stack)-1];

        $active_directives = [];
        if (count($head['started']) > 0){
            return 'if_started';
        } else {
            return 'if_unstarted';
        }
    }
    /**
     * Get an array of 'started' instruction sets for the active directives, if any. Otherwise, get 'unstarted' instruction sets.
     *
     */
    public function get_active_instruction_sets(): array {
        $head = $this->directive_stack[count($this->directive_stack)-1];

        $active_directives = [];
        if (count($head['started']) > 0){
            foreach ($head['started'] as $directive_name => $directive){
                if (isset($directive['if_started'])){
                    $active_directives[] = $directive['if_started'];
                }
            }
        } else {
            foreach ($head['unstarted'] as $directive_name => $directive){
                if (isset($directive['if_unstarted'])){
                    $active_directives[] = $directive['if_unstarted'];
                }
            }
        }

        return $active_directives;
    }


    public function set_local_var(string $var_name, mixed $value){
        $this->local_vars[$var_name] = $value;
    }
    

    public function error(string $error_message){
        echo "\n  Error: $error_message";
    }
}