aboutsummaryrefslogtreecommitdiffhomepage
path: root/index.php
diff options
context:
space:
mode:
Diffstat (limited to 'index.php')
-rw-r--r--index.php1213
1 files changed, 1213 insertions, 0 deletions
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..d8f9ce5
--- /dev/null
+++ b/index.php
@@ -0,0 +1,1213 @@
+<?php
+
+const KEYWORDS = [
+ 'break',
+ 'const',
+ 'continue',
+ 'echo',
+ 'else',
+ 'elseif',
+ 'for',
+ 'function',
+ 'if',
+ 'return',
+ 'throw',
+ 'while',
+];
+
+function tokenize(string $src): array
+{
+ $tokens = [];
+ $pos = 0;
+ $len = strlen($src);
+
+ while ($pos < $len) {
+ $c = $src[$pos];
+ ++$pos;
+ if (ctype_space($c)) {
+ while ($pos < $len && ctype_space($src[$pos])) {
+ ++$pos;
+ }
+ } elseif ($c === '+') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['+=', null];
+ } elseif ($src[$pos] === '+') {
+ ++$pos;
+ $tokens[] = ['++', null];
+ } else {
+ $tokens[] = ['+', null];
+ }
+ } elseif ($c === '-') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['-=', null];
+ } elseif ($src[$pos] === '-') {
+ ++$pos;
+ $tokens[] = ['--', null];
+ } else {
+ $tokens[] = ['-', null];
+ }
+ } elseif ($c === '&') {
+ if ($src[$pos] === '&') {
+ ++$pos;
+ $tokens[] = ['&&', null];
+ } else {
+ $tokens[] = ['&', null];
+ }
+ } elseif ($c === '*') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['=', null];
+ } else {
+ $tokens[] = ['*', null];
+ }
+ } elseif ($c === '{') {
+ $tokens[] = ['{', null];
+ } elseif ($c === '}') {
+ $tokens[] = ['}', null];
+ } elseif ($c === '(') {
+ $tokens[] = ['(', null];
+ } elseif ($c === ')') {
+ $tokens[] = [')', null];
+ } elseif ($c === '[') {
+ $tokens[] = ['[', null];
+ } elseif ($c === ']') {
+ $tokens[] = [']', null];
+ } elseif ($c === ':') {
+ $tokens[] = [':', null];
+ } elseif ($c === ';') {
+ $tokens[] = [';', null];
+ } elseif ($c === ',') {
+ $tokens[] = [',', null];
+ } elseif ($c === '.') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['.=', null];
+ } else {
+ $tokens[] = ['.', null];
+ }
+ } elseif ($c === '=') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['===', null];
+ } else {
+ $tokens[] = ['==', null];
+ }
+ } else {
+ $tokens[] = ['=', null];
+ }
+ } elseif ($c === '!') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['!==', null];
+ } else {
+ $tokens[] = ['!=', null];
+ }
+ } else {
+ $tokens[] = ['!', null];
+ }
+ } elseif ($c === '<') {
+ if ($src[$pos] === '?') {
+ ++$pos;
+ if ($src[$pos] === 'p') {
+ ++$pos;
+ if ($src[$pos] === 'h') {
+ ++$pos;
+ if ($src[$pos] === 'p') {
+ ++$pos;
+ $tokens[] = ['<?php', null];
+ } else {
+ throw new \RuntimeException('invalid char');
+ }
+ } else {
+ throw new \RuntimeException('invalid char');
+ }
+ } else {
+ throw new \RuntimeException('invalid char');
+ }
+ } elseif ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['<=', null];
+ } else {
+ $tokens[] = ['<', null];
+ }
+ } elseif ($c === '>') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['>=', null];
+ } else {
+ $tokens[] = ['>', null];
+ }
+ } elseif ($c === '%') {
+ if ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['%=', null];
+ } else {
+ $tokens[] = ['%', null];
+ }
+ } elseif ($c === '\\') {
+ $tokens[] = ['\\', null];
+ } elseif ($c === '|') {
+ if ($src[$pos] === '|') {
+ ++$pos;
+ $tokens[] = ['||', null];
+ } else {
+ $tokens[] = ['|', null];
+ }
+ } elseif ($c === '/') {
+ if ($src[$pos] === '/') {
+ ++$pos;
+ while ($pos < $len && $src[$pos] !== "\n") {
+ ++$pos;
+ }
+ } elseif ($src[$pos] === '*') {
+ ++$pos;
+ while ($pos < $len && ! ($src[$pos] === '*' && $src[$pos + 1] === '/')) {
+ ++$pos;
+ }
+ $pos += 2;
+ } elseif ($src[$pos] === '=') {
+ ++$pos;
+ $tokens[] = ['/=', null];
+ } else {
+ $tokens[] = ['/', null];
+ }
+ } elseif ($c === '"') {
+ $value = '';
+ while ($pos < $len && $src[$pos] !== '"') {
+ if ($src[$pos] === '\\') {
+ ++$pos;
+ if ($pos === $len) {
+ throw new \RuntimeException('invalid string');
+ }
+ $escape = $src[$pos];
+ if ($escape === 'n') {
+ $value .= "\n";
+ } elseif ($escape === 'r') {
+ $value .= "\r";
+ } elseif ($escape === 't') {
+ $value .= "\t";
+ } elseif ($escape === '\\') {
+ $value .= '\\';
+ } else {
+ $value .= $escape;
+ }
+ ++$pos;
+ } else {
+ $value .= $src[$pos];
+ ++$pos;
+ }
+ }
+ if ($src[$pos] !== '"') {
+ throw new \RuntimeException('invalid string');
+ }
+ ++$pos;
+ $tokens[] = ['constant_encapsed_string', $value];
+ } elseif ($c === "'") {
+ $value = '';
+ while ($pos < $len && $src[$pos] !== "'") {
+ if ($src[$pos] === '\\') {
+ ++$pos;
+ if ($pos === $len) {
+ throw new \RuntimeException('invalid string');
+ }
+ $escape = $src[$pos];
+ if ($escape === 'n') {
+ $value .= "\n";
+ } elseif ($escape === 'r') {
+ $value .= "\r";
+ } elseif ($escape === 't') {
+ $value .= "\t";
+ } elseif ($escape === '\\') {
+ $value .= '\\';
+ } else {
+ $value .= $escape;
+ }
+ ++$pos;
+ } else {
+ $value .= $src[$pos];
+ ++$pos;
+ }
+ }
+ if ($src[$pos] !== "'") {
+ throw new \RuntimeException('invalid string');
+ }
+ ++$pos;
+ $tokens[] = ['constant_encapsed_string', $value];
+ } elseif ($c === '$') {
+ $name = '';
+ while ($pos < $len && (ctype_alnum($src[$pos]) || $src[$pos] === '_')) {
+ $name .= $src[$pos];
+ ++$pos;
+ }
+ if ($name === '') {
+ throw new \RuntimeException('invalid var');
+ }
+ $tokens[] = ['variable', $name];
+ } elseif (ctype_digit($c)) {
+ $value = $c;
+ while ($pos < $len && ctype_digit($src[$pos])) {
+ $value .= $src[$pos];
+ ++$pos;
+ }
+ $tokens[] = ['lnumber', $value];
+ } elseif (ctype_alpha($c) || $c === '_') {
+ $name = $c;
+ while ($pos < $len && (ctype_alnum($src[$pos]) || $src[$pos] === '_')) {
+ $name .= $src[$pos];
+ ++$pos;
+ }
+ if (in_array(strtolower($name), KEYWORDS)) {
+ $tokens[] = [strtolower($name), null];
+ } else {
+ $tokens[] = ['string', $name];
+ }
+ } else {
+ throw new \RuntimeException("invalid char: {$c}");
+ }
+ }
+ return $tokens;
+}
+
+function parse(array $tokens): array
+{
+ expect_token($tokens, 0, '<?php');
+ return parse_statements($tokens, 1, '')[0];
+}
+
+function parse_statements(array $tokens, int $pos, string $delimiter): array
+{
+ $statements = [];
+ while ($pos < count($tokens) && $tokens[$pos][0] !== $delimiter) {
+ [$statement, $pos] = parse_statement($tokens, $pos);
+ $statements[] = $statement;
+ }
+ return [['statements', $statements], $pos];
+}
+
+function parse_statement(array $tokens, int $pos): array
+{
+ $t = $tokens[$pos];
+ if ($t[0] === 'const') {
+ return parse_const_declaration($tokens, $pos);
+ } elseif ($t[0] === 'function') {
+ return parse_function_declaration($tokens, $pos);
+ } elseif ($t[0] === 'while') {
+ return parse_while_statement($tokens, $pos);
+ } elseif ($t[0] === 'for') {
+ return parse_for_statement($tokens, $pos);
+ } elseif ($t[0] === 'if') {
+ return parse_if_statement($tokens, $pos);
+ } elseif ($t[0] === 'return') {
+ return parse_return_statement($tokens, $pos);
+ } elseif ($t[0] === 'break') {
+ return parse_break_statement($tokens, $pos);
+ } elseif ($t[0] === 'continue') {
+ return parse_continue_statement($tokens, $pos);
+ } elseif ($t[0] === 'echo') {
+ return parse_echo_statement($tokens, $pos);
+ } elseif ($t[0] === 'throw') {
+ return parse_throw_statement($tokens, $pos);
+ }
+ return parse_expression_statement($tokens, $pos);
+}
+
+function parse_const_declaration(array $tokens, int $pos): array
+{
+ $pos++; // const
+ $name = $tokens[$pos][1];
+ $pos++; // <name>
+ $pos++; // =
+ [$expr, $pos] = parse_expression($tokens, $pos);
+ $pos++; // ;
+ return [['const', $name, $expr], $pos];
+}
+
+function parse_function_declaration(array $tokens, int $pos): array
+{
+ $pos++; // function
+ $name = $tokens[$pos][1];
+ expect_token($tokens, $pos, 'string');
+ $pos++; // <name>
+ expect_token($tokens, $pos, '(');
+ $pos++; // (
+ [$parameters, $pos] = parse_parameters($tokens, $pos);
+ expect_token($tokens, $pos, ')');
+ $pos++; // )
+ if ($tokens[$pos][0] === ':') {
+ $pos++; // :
+ $pos++; // <type>
+ }
+ expect_token($tokens, $pos, '{');
+ $pos++; // {
+ [$statements, $pos] = parse_statements($tokens, $pos, '}');
+ expect_token($tokens, $pos, '}');
+ $pos++; // }
+ return [['function', $name, $parameters, $statements], $pos];
+}
+
+function parse_parameters(array $tokens, int $pos): array
+{
+ $parameters = [];
+ while (true) {
+ if ($tokens[$pos][0] === ')') {
+ break;
+ }
+ if ($tokens[$pos][0] === 'string') {
+ ++$pos;
+ }
+ expect_token($tokens, $pos, 'variable');
+ $parameters[] = $tokens[$pos][1];
+ ++$pos;
+ if ($tokens[$pos][0] === ',') {
+ ++$pos;
+ } elseif ($tokens[$pos][0] === ')') {
+ break;
+ } else {
+ throw new \RuntimeException("invalid token: {$tokens[$pos][0]}");
+ }
+ }
+ return [$parameters, $pos];
+}
+
+function parse_while_statement(array $tokens, int $pos): array
+{
+ expect_token($tokens, $pos, 'while');
+ ++$pos; // while
+ expect_token($tokens, $pos, '(');
+ ++$pos; // (
+ [$cond, $pos] = parse_expression($tokens, $pos);
+ expect_token($tokens, $pos, ')');
+ ++$pos; // )
+ expect_token($tokens, $pos, '{');
+ ++$pos; // {
+ [$body, $pos] = parse_statements($tokens, $pos, '}');
+ expect_token($tokens, $pos, '}');
+ ++$pos; // }
+ return [['while', $cond, $body], $pos];
+}
+
+function parse_for_statement(array $tokens, int $pos): array
+{
+ expect_token($tokens, $pos, 'for');
+ ++$pos; // for
+ expect_token($tokens, $pos, '(');
+ ++$pos; // (
+ [$init, $pos] = parse_expression($tokens, $pos);
+ expect_token($tokens, $pos, ';');
+ ++$pos; // ;
+ [$cond, $pos] = parse_expression($tokens, $pos);
+ expect_token($tokens, $pos, ';');
+ ++$pos; // ;
+ [$update, $pos] = parse_expression($tokens, $pos);
+ expect_token($tokens, $pos, ')');
+ ++$pos; // )
+ expect_token($tokens, $pos, '{');
+ ++$pos; // {
+ [$body, $pos] = parse_statements($tokens, $pos, '}');
+ expect_token($tokens, $pos, '}');
+ ++$pos; // }
+ return [['for', $init, $cond, $update, $body], $pos];
+}
+
+function parse_if_statement(array $tokens, int $pos): array
+{
+ ++$pos; // if or elseif
+ expect_token($tokens, $pos, '(');
+ ++$pos; // (
+ [$cond, $pos] = parse_expression($tokens, $pos);
+ expect_token($tokens, $pos, ')');
+ ++$pos; // )
+ expect_token($tokens, $pos, '{');
+ ++$pos; // {
+ [$then, $pos] = parse_statements($tokens, $pos, '}');
+ expect_token($tokens, $pos, '}');
+ ++$pos; // }
+ if ($tokens[$pos][0] === 'elseif') {
+ [$else, $pos] = parse_if_statement($tokens, $pos);
+ } elseif ($tokens[$pos][0] === 'else') {
+ expect_token($tokens, $pos, 'else');
+ ++$pos; // else
+ expect_token($tokens, $pos, '{');
+ ++$pos; // {
+ [$else, $pos] = parse_statements($tokens, $pos, '}');
+ expect_token($tokens, $pos, '}');
+ ++$pos; // }
+ } else {
+ $else = null;
+ }
+ return [['if', $cond, $then, $else], $pos];
+}
+
+function parse_return_statement(array $tokens, int $pos): array
+{
+ ++$pos; // return
+ if ($tokens[$pos][0] === ';') {
+ $ret = null;
+ } else {
+ [$ret, $pos] = parse_expression($tokens, $pos);
+ }
+ expect_token($tokens, $pos, ';');
+ ++$pos; // ;
+ return [['return', $ret], $pos];
+}
+
+function parse_break_statement(array $tokens, int $pos): array
+{
+ $pos += 2;
+ return [['break'], $pos];
+}
+
+function parse_continue_statement(array $tokens, int $pos): array
+{
+ $pos += 2;
+ return [['continue'], $pos];
+}
+
+function parse_echo_statement(array $tokens, int $pos): array
+{
+ ++$pos; // echo
+ [$expr, $pos] = parse_expression($tokens, $pos);
+ ++$pos; // ;
+ return [['echo', $expr], $pos];
+}
+
+function parse_throw_statement(array $tokens, int $pos): array
+{
+ ++$pos; // throw
+ ++$pos; // new
+ ++$pos; // \
+ ++$pos; // <name>
+ ++$pos; // (
+ [$args, $pos] = parse_arguments($tokens, $pos);
+ ++$pos; // )
+ expect_token($tokens, $pos, ';');
+ ++$pos; // ;
+ return [['throw'], $pos];
+}
+
+function parse_expression_statement(array $tokens, int $pos): array
+{
+ [$expr, $pos] = parse_expression($tokens, $pos);
+ $pos++; // ;
+ return [['expression', $expr], $pos];
+}
+
+function parse_expression(array $tokens, int $pos): array
+{
+ return parse_assign_expression($tokens, $pos);
+}
+
+function parse_assign_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_boolean_or_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['=', '+=', '-=', '.='])) {
+ ++$pos;
+ [$rhs, $pos] = parse_assign_expression($tokens, $pos);
+ return [['assign', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_boolean_or_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_boolean_and_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if ($op === '||') {
+ ++$pos;
+ [$rhs, $pos] = parse_boolean_or_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_boolean_and_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_equality_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if ($op === '&&') {
+ ++$pos;
+ [$rhs, $pos] = parse_boolean_and_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_equality_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_relational_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['==', '!=', '===', '!=='])) {
+ ++$pos;
+ [$rhs, $pos] = parse_equality_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_relational_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_concatenate_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['<', '<=', '>', '>='])) {
+ ++$pos;
+ [$rhs, $pos] = parse_relational_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_concatenate_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_additive_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if ($op === '.') {
+ ++$pos;
+ [$rhs, $pos] = parse_concatenate_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_additive_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_multiplicative_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['+', '-'])) {
+ ++$pos;
+ [$rhs, $pos] = parse_additive_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_multiplicative_expression(array $tokens, int $pos): array
+{
+ [$lhs, $pos] = parse_prefix_expression($tokens, $pos);
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['*', '/', '%'])) {
+ ++$pos;
+ [$rhs, $pos] = parse_multiplicative_expression($tokens, $pos);
+ return [['infix', $op, $lhs, $rhs], $pos];
+ }
+ return [$lhs, $pos];
+
+}
+
+function parse_prefix_expression(array $tokens, int $pos): array
+{
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['!', '+', '-', '++', '--'])) {
+ $pos++;
+ [$operand, $pos] = parse_prefix_expression($tokens, $pos);
+ return [['prefix', $op, $operand], $pos];
+ }
+ return parse_postfix_expression($tokens, $pos);
+}
+
+function parse_postfix_expression(array $tokens, int $pos): array
+{
+ [$operand, $pos] = parse_primary_expression($tokens, $pos);
+ while (true) {
+ $op = $tokens[$pos][0];
+ if (in_array($op, ['++', '--'])) {
+ $pos++;
+ $operand = ['postfix', $op, $operand];
+ } elseif ($op === '(') {
+ $pos++;
+ [$args, $pos] = parse_arguments($tokens, $pos);
+ $pos++;
+ $operand = ['call', $operand, $args];
+ } elseif ($op === '[') {
+ $pos++;
+ if ($tokens[$pos][0] === ']') {
+ $index = null;
+ } else {
+ [$index, $pos] = parse_expression($tokens, $pos);
+ }
+ expect_token($tokens, $pos, ']');
+ $pos++;
+ $operand = ['index', $operand, $index];
+ } else {
+ break;
+ }
+ }
+ return [$operand, $pos];
+}
+
+function parse_primary_expression(array $tokens, int $pos): array
+{
+ $t = $tokens[$pos][0];
+ if ($t === 'variable') {
+ $name = $tokens[$pos][1];
+ $pos++;
+ return [['variable', $name], $pos];
+ } elseif ($t === '[') {
+ $pos++; // [
+ [$elements, $pos] = parse_array_elements($tokens, $pos);
+ $pos++; // ]
+ return [['array', $elements], $pos];
+ } elseif ($t === 'constant_encapsed_string') {
+ $value = $tokens[$pos][1];
+ $pos++;
+ return [['string_literal', $value], $pos];
+ } elseif ($t === 'lnumber') {
+ $value = $tokens[$pos][1];
+ $pos++;
+ return [['number_literal', intval($value)], $pos];
+ } elseif ($t === 'string') {
+ $name = $tokens[$pos][1];
+ $pos++;
+ return [['string', $name], $pos];
+ } elseif ($t === '(') {
+ $pos++; // (
+ [$expr, $pos] = parse_expression($tokens, $pos);
+ $pos++; // )
+ return [$expr, $pos];
+ }
+ throw new \RuntimeException("invalid expr: {$t} at {$pos}");
+}
+
+function parse_array_elements(array $tokens, int $pos): array
+{
+ $elements = [];
+ while (true) {
+ if ($tokens[$pos][0] === ']') {
+ break;
+ }
+ if ($tokens[$pos][0] === ',') {
+ ++$pos;
+ if ($tokens[$pos][0] === ']') {
+ break;
+ }
+ }
+ [$element, $pos] = parse_array_element($tokens, $pos);
+ $elements[] = $element;
+ }
+ return [$elements, $pos];
+}
+
+function parse_array_element(array $tokens, int $pos): array
+{
+ $key = null;
+ [$value, $pos] = parse_expression($tokens, $pos);
+ return [['element', $key, $value], $pos];
+}
+
+function parse_arguments(array $tokens, int $pos): array
+{
+ $args = [];
+ while (true) {
+ if ($tokens[$pos][0] === ')') {
+ break;
+ }
+ if ($tokens[$pos][0] === ',') {
+ ++$pos;
+ }
+ [$arg, $pos] = parse_expression($tokens, $pos);
+ $args[] = $arg;
+ }
+ return [$args, $pos];
+}
+
+function expect_token(array $tokens, int $pos, string $token): void
+{
+ if ($tokens[$pos][0] !== $token) {
+ throw new \RuntimeException("expect {$token} but got {$tokens[$pos][0]} at {$pos}");
+ }
+}
+
+function evaluate(array $env, array $node): array
+{
+ if ($node[0] === 'statements') {
+ return evaluate_statements($env, $node[1]);
+ } elseif ($node[0] === 'while') {
+ return evaluate_while_statement($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'for') {
+ return evaluate_for_statement($env, $node[1], $node[2], $node[3], $node[4]);
+ } elseif ($node[0] === 'if') {
+ return evaluate_if_statement($env, $node[1], $node[2], $node[3]);
+ } elseif ($node[0] === 'continue') {
+ return evaluate_continue_statement($env);
+ } elseif ($node[0] === 'break') {
+ return evaluate_break_statement($env);
+ } elseif ($node[0] === 'return') {
+ return evaluate_return_statement($env, $node[1]);
+ } elseif ($node[0] === 'echo') {
+ return evaluate_echo_statement($env, $node[1]);
+ } elseif ($node[0] === 'expression') {
+ return evaluate_expression_statement($env, $node[1]);
+ } elseif ($node[0] === 'const') {
+ return evaluate_const_declaration($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'function') {
+ return evaluate_function_declaration($env, $node[1], $node[2], $node[3]);
+ } elseif ($node[0] === 'assign') {
+ return evaluate_assign_expr($env, $node[1], $node[2], $node[3]);
+ } elseif ($node[0] === 'infix') {
+ return evaluate_infix_expr($env, $node[1], $node[2], $node[3]);
+ } elseif ($node[0] === 'prefix') {
+ return evaluate_prefix_expr($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'postfix') {
+ return evaluate_postfix_expr($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'call') {
+ return evaluate_call_expr($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'array') {
+ return evaluate_array_expr($env, $node[1]);
+ } elseif ($node[0] === 'index') {
+ return evaluate_index_expr($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'element') {
+ return evaluate_array_element($env, $node[1], $node[2]);
+ } elseif ($node[0] === 'variable') {
+ return evaluate_variable_expr($env, $node[1]);
+ } elseif ($node[0] === 'string') {
+ return evaluate_string_expr($env, $node[1]);
+ } elseif ($node[0] === 'string_literal') {
+ return evaluate_literal_expr($env, $node[1]);
+ } elseif ($node[0] === 'number_literal') {
+ return evaluate_literal_expr($env, $node[1]);
+ }
+ throw new \RuntimeException("unknown node: {$node[0]}");
+}
+
+function evaluate_if_statement(
+ array $env,
+ array $condition,
+ mixed $then,
+ mixed $else,
+): array {
+ [$env, $condition_] = evaluate($env, $condition);
+ if ($condition_) {
+ if ($then !== null) {
+ return evaluate($env, $then);
+ }
+ }
+ if ($else !== null) {
+ return evaluate($env, $else);
+ }
+ return [$env, null];
+}
+
+function evaluate_break_statement(array $env): array
+{
+ return [$env, ['break']];
+}
+
+function evaluate_continue_statement(array $env): array
+{
+ return [$env, ['continue']];
+}
+
+function evaluate_return_statement(array $env, mixed $ret): array
+{
+ if ($ret === null) {
+ return [$env, ['return', null]];
+ }
+ [$env, $ret_] = evaluate($env, $ret);
+ return [$env, ['return', $ret_]];
+}
+
+function evaluate_echo_statement(array $env, array $expr): array
+{
+ [$env, $expr_] = evaluate($env, $expr);
+ echo $expr_;
+ return [$env, null];
+}
+
+function evaluate_statements(array $env, array $statements): array
+{
+ for ($i = 0; $i < count($statements); $i++) {
+ [$env, $result] = evaluate($env, $statements[$i]);
+ if ($result) {
+ return [$env, $result];
+ }
+ }
+ return [$env, null];
+}
+
+function evaluate_while_statement(array $env, array $cond, array $body): array
+{
+ while (true) {
+ [$env, $cond_] = evaluate($env, $cond);
+ if (! $cond_) {
+ break;
+ }
+ [$env, $result] = evaluate($env, $body);
+ if ($result) {
+ if ($result[0] === 'break') {
+ break;
+ }
+ if ($result[0] === 'continue') {
+ continue;
+ }
+ return [$env, $result];
+ }
+ }
+ return [$env, null];
+}
+
+function evaluate_for_statement(
+ array $env,
+ array $init,
+ array $cond,
+ array $update,
+ array $body,
+): array {
+ [$env, $init_] = evaluate($env, $init);
+ while (true) {
+ [$env, $cond_] = evaluate($env, $cond);
+ if (! $cond_) {
+ break;
+ }
+ [$env, $result] = evaluate($env, $body);
+ if ($result) {
+ if ($result[0] === 'break') {
+ break;
+ }
+ if ($result[0] === 'continue') {
+ [$env, $update_] = evaluate($env, $update);
+ continue;
+ }
+ return [$env, $result];
+ }
+ [$env, $update_] = evaluate($env, $update);
+ }
+ return [$env, null];
+}
+
+function evaluate_expression_statement(array $env, array $expr): array
+{
+ [$env, $_] = evaluate($env, $expr);
+ return [$env, null];
+}
+
+function evaluate_const_declaration(array $env, string $name, mixed $value): array
+{
+ [$env, $value_] = evaluate($env, $value);
+ $env['consts'][$name] = $value_;
+ return [$env, null];
+}
+
+function evaluate_function_declaration(
+ array $env,
+ string $name,
+ array $parameters,
+ array $body,
+): array {
+ $env['funcs'][$name] = [$parameters, $body];
+ return [$env, null];
+}
+
+function evaluate_assign_expr(array $env, string $operator, array $lhs, array $rhs): array
+{
+ [$env, $rhs_] = evaluate($env, $rhs);
+ if ($lhs[0] === 'variable') {
+ $name = $lhs[1];
+ if ($operator === '=') {
+ $env['vars'][$name] = $rhs_;
+ } else {
+ $lhs_ = $env['vars'][$name];
+ if ($operator === '.=') {
+ $rhs_ = $lhs_ . $rhs_;
+ } elseif ($operator === '+=') {
+ $rhs_ = $lhs_ + $rhs_;
+ } elseif ($operator === '-=') {
+ $rhs_ = $lhs_ - $rhs_;
+ } else {
+ throw new \RuntimeException("unsupported compound assignment: {$operator}");
+ }
+ $env['vars'][$name] = $rhs_;
+ }
+ } elseif ($lhs[0] === 'index') {
+ $keys = [];
+ while ($lhs[0] === 'index') {
+ if ($lhs[2] === null) {
+ $key = null;
+ } else {
+ [$env, $key] = evaluate($env, $lhs[2]);
+ }
+ $keys[] = $key;
+ $lhs = $lhs[1];
+ }
+ if ($lhs[0] !== 'variable') {
+ throw new \RuntimeException('unsupported');
+ }
+ $root_var_name = $lhs[1];
+ $keys = array_reverse($keys);
+ $n = count($keys);
+ $arrays = [];
+ $a = $env['vars'][$root_var_name];
+ for ($i = 0; $i < $n; $i++) {
+ $arrays[] = $a;
+ if ($keys[$i] === null) {
+ $keys[$i] = count($a);
+ $a = [];
+ } elseif ($i !== $n - 1) {
+ $a = $a[$keys[$i]];
+ }
+ }
+ $arrays[$n - 1][$keys[$n - 1]] = $rhs_;
+ for ($i = $n - 2; $i >= 0; $i--) {
+ $arrays[$i][$keys[$i]] = $arrays[$i + 1];
+ }
+ if ($operator === '=') {
+ $env['vars'][$root_var_name] = $arrays[0];
+ } else {
+ throw new \RuntimeException('unsupported');
+ }
+ } elseif ($lhs[0] === 'array') {
+ for ($i = 0; $i < count($lhs[1]); $i++) {
+ $element = $lhs[1][$i];
+ if ($element[0] === 'element') {
+ $value = $element[2];
+ if ($element[2][0] === 'variable') {
+ $name = $element[2][1];
+ $env['vars'][$name] = $rhs_[$i];
+ } else {
+ throw new \RuntimeException('unsupported');
+ }
+ } else {
+ throw new \RuntimeException('unsupported');
+ }
+ }
+ } else {
+ throw new \RuntimeException("unsupported: {$lhs[0]}");
+ }
+ return [$env, null];
+}
+
+function evaluate_call_expr(array $env, array $func, array $args): array
+{
+ if ($func[0] === 'string') {
+ $name = $func[1];
+ } else {
+ [$env, $func_] = evaluate($env, $func);
+ $name = $func_;
+ }
+ $args_ = [];
+ for ($i = 0; $i < count($args); $i++) {
+ [$env, $arg] = evaluate($env, $args[$i]);
+ $args_[$i] = $arg;
+ }
+ if (array_key_exists($name, $env['funcs'])) {
+ $fn = $env['funcs'][$name];
+ $params = $fn[0];
+ $body = $fn[1];
+ $local_env = $env;
+ $local_env['vars'] = [];
+ if (count($params) !== count($args_)) {
+ throw new \RuntimeException("wrong number of arguments ({$name}): expect " . count($params) . ' but got ' . count($args_));
+ }
+ for ($i = 0; $i < count($params); $i++) {
+ $local_env['vars'][$params[$i]] = $args_[$i];
+ }
+ [$local_env, $ret_] = evaluate($local_env, $body);
+ if ($ret_ && $ret_[0] === 'return') {
+ $ret = $ret_[1];
+ } else {
+ $ret = null;
+ }
+ } elseif ($name === 'defined') {
+ $ret = array_key_exists($args_[0], $env['consts']) || defined($args_[0]);
+ } else {
+ $ret = call_user_func_array($name, $args_);
+ }
+ return [$env, $ret];
+}
+
+function evaluate_array_expr(array $env, array $elements): array
+{
+ $elements_ = [];
+ for ($i = 0; $i < count($elements); $i++) {
+ [$env, $element] = evaluate($env, $elements[$i]);
+ $elements_[$i] = $element;
+ }
+ return [$env, $elements_];
+}
+
+function evaluate_index_expr(array $env, array $seq, array $index): array
+{
+ [$env, $seq_] = evaluate($env, $seq);
+ [$env, $index_] = evaluate($env, $index);
+ if (is_string($seq_)) {
+ if ($index_ >= 0 && $index_ < strlen($seq_)) {
+ return [$env, $seq_[$index_]];
+ }
+ return [$env, ''];
+ } elseif (is_array($seq_)) {
+ if (array_key_exists($index_, $seq_)) {
+ return [$env, $seq_[$index_]];
+ }
+ return [$env, null];
+ }
+ throw new \RuntimeException('unsupported: ' . gettype($seq_));
+}
+
+function evaluate_array_element(array $env, $_, array $value): array
+{
+ return evaluate($env, $value);
+}
+
+function evaluate_infix_expr(
+ array $env,
+ string $operator,
+ array $left,
+ array $right,
+): array {
+ if ($operator === '&&' || $operator === '||') {
+ return evaluate_short_circuit_expr($env, $operator, $left, $right);
+ }
+ [$env, $left_] = evaluate($env, $left);
+ [$env, $right_] = evaluate($env, $right);
+ if ($operator === '%') {
+ return [$env, $left_ % $right_];
+ } elseif ($operator === '+') {
+ return [$env, $left_ + $right_];
+ } elseif ($operator === '-') {
+ return [$env, $left_ - $right_];
+ } elseif ($operator === '*') {
+ return [$env, $left_ * $right_];
+ } elseif ($operator === '/') {
+ return [$env, $left_ / $right_];
+ } elseif ($operator === '.') {
+ return [$env, $left_ . $right_];
+ } elseif ($operator === '<') {
+ return [$env, $left_ < $right_];
+ } elseif ($operator === '>') {
+ return [$env, $left_ > $right_];
+ } elseif ($operator === '<=') {
+ return [$env, $left_ <= $right_];
+ } elseif ($operator === '>=') {
+ return [$env, $left_ >= $right_];
+ } elseif ($operator === '==') {
+ return [$env, $left_ == $right_];
+ } elseif ($operator === '!=') {
+ return [$env, $left_ != $right_];
+ } elseif ($operator === '===') {
+ return [$env, $left_ === $right_];
+ } elseif ($operator === '!==') {
+ return [$env, $left_ !== $right_];
+ }
+ throw new \RuntimeException("unsupported operator: {$operator}");
+}
+
+function evaluate_short_circuit_expr(
+ array $env,
+ string $operator,
+ array $left,
+ array $right,
+): array {
+ [$env, $left_] = evaluate($env, $left);
+ if ($operator === '&&') {
+ if (! $left_) {
+ return [$env, false];
+ }
+ } elseif ($operator === '||') {
+ if ($left_) {
+ return [$env, true];
+ }
+ }
+ [$env, $right_] = evaluate($env, $right);
+ return [$env, $right_];
+}
+
+function evaluate_prefix_expr(array $env, string $operator, array $operand): array
+{
+ [$env, $operand_] = evaluate($env, $operand);
+ if ($operator === '!') {
+ return [$env, ! $operand_];
+ } elseif ($operator === '+') {
+ return [$env, +$operand_];
+ } elseif ($operator === '-') {
+ return [$env, -$operand_];
+ } elseif ($operator === '++') {
+ if ($operand[0] === 'variable') {
+ $name = $operand[1];
+ $result = $operand_ + 1;
+ $env['vars'][$name] = $result;
+ return [$env, $result];
+ }
+ throw new \RuntimeException('unsupported');
+ } elseif ($operator === '--') {
+ if ($operand[0] === 'variable') {
+ $name = $operand[1];
+ $result = $operand_ - 1;
+ $env['vars'][$name] = $result;
+ return [$env, $result];
+ }
+ throw new \RuntimeException('unsupported');
+ } else {
+ throw new \RuntimeException("unsupported operator: {$operator}");
+ }
+}
+
+function evaluate_postfix_expr(array $env, string $operator, array $operand): array
+{
+ [$env, $operand_] = evaluate($env, $operand);
+ if ($operand[0] === 'variable') {
+ $name = $operand[1];
+ if ($operator === '++') {
+ $result = $operand_ + 1;
+ } elseif ($operator === '--') {
+ $result = $operand_ - 1;
+ } else {
+ throw new \RuntimeException("unsupported operator: {$operator}");
+ }
+ $env['vars'][$name] = $result;
+ return [$env, $operand_];
+ }
+ throw new \RuntimeException('unsupported');
+}
+
+function evaluate_variable_expr(array $env, string $name): array
+{
+ return [$env, $env['vars'][$name]];
+}
+
+function evaluate_string_expr(array $env, string $name): array
+{
+ if (array_key_exists($name, $env['consts'])) {
+ return [$env, $env['consts'][$name]];
+ }
+ return [$env, constant($name)];
+}
+
+function evaluate_literal_expr(array $env, mixed $value): array
+{
+ return [$env, $value];
+}
+
+$env = [];
+$env['vars'] = [];
+$env['funcs'] = [];
+$env['consts'] = [];
+
+if (defined('PHPHP')) {
+ if (PHPHP === 1) {
+ echo "Running on PHPHP on PHP\n";
+ $file = './index.php';
+ } else {
+ $file = './hello.php';
+ }
+ $env['consts']['PHPHP'] = PHPHP + 1;
+} else {
+ echo "Running on PHP\n";
+ $file = './index.php';
+ $env['consts']['PHPHP'] = 1;
+}
+
+$tokens = tokenize(file_get_contents($file));
+$ast = parse($tokens);
+evaluate($env, $ast);