<?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";
}
}