diff --git a/classes.php b/classes.php index d99e032..7c35663 100644 --- a/classes.php +++ b/classes.php @@ -2,21 +2,69 @@ class MadLispException extends Exception {} -class Env +abstract class MLCollection { - private array $data = []; - private ?Env $parent; + protected array $data = []; - public function __construct(?Env $parent = null) + public function __construct(array $data = []) { - $this->parent = $parent; + $this->data = $data; } - public function has(string $key) + public function count(): int + { + return count($this->data); + } + + public function has(string $key): bool { return array_key_exists($key, $this->data); } + public function getData(): array + { + return $this->data; + } +} + +class MLList extends MLCollection +{ + public function get(int $index) + { + if ($this->has($index)) { + return $this->data[$index]; + } + + throw new MadLispException("list does not contain index $index"); + } +} + +class MLHash extends MLCollection +{ + public function get(string $key) + { + if ($this->has($key)) { + return $this->data[$key]; + } + + throw new MadLispException("hash does not contain key $key"); + } + + public function set(string $key, $value): void + { + $this->data[$key] = $value; + } +} + +class MLEnv extends MLHash +{ + protected ?MLEnv $parent; + + public function __construct(?MLEnv $parent = null) + { + $this->parent = $parent; + } + public function get(string $key) { if ($this->has($key)) { @@ -27,9 +75,19 @@ class Env throw new MadLispException("symbol $key not defined"); } +} - public function set(string $key, $value): void +class MLSymbol +{ + protected string $name; + + public function __construct(string $name) { - $this->data[$key] = $value; + $this->name = $name; + } + + public function name(): string + { + return $this->name; } } diff --git a/lib.php b/lib.php index 3eda6dd..f68e953 100644 --- a/lib.php +++ b/lib.php @@ -2,16 +2,62 @@ require_once('classes.php'); -function ml_get_env(): Env +function ml_get_env(): MLEnv { - $env = new Env(); + $env = new MLEnv(); // basic arithmetic - $env->set('+', fn (...$args) => array_sum($args)); - $env->set('-', fn ($a, $b) => $a - $b); - $env->set('*', fn ($a, $b) => $a * $b); - $env->set('/', fn ($a, $b) => $a / $b); - $env->set('%', fn ($a, $b) => $a % $b); + + $env->set('+', function (...$args) { + return array_sum($args); + }); + + $env->set('-', function (...$args) { + $result = $args[0] ?? null; + for ($i = 1; $i < count($args); $i++) { + $result -= $args[$i]; + } + return $result; + }); + + $env->set('*', function (...$args) { + $result = $args[0] ?? null; + for ($i = 1; $i < count($args); $i++) { + $result *= $args[$i]; + } + return $result; + }); + + $env->set('/', function (...$args) { + $result = $args[0] ?? null; + for ($i = 1; $i < count($args); $i++) { + $result /= $args[$i]; + } + return $result; + }); + + $env->set('%', function (...$args) { + $result = $args[0] ?? null; + for ($i = 1; $i < count($args); $i++) { + $result %= $args[$i]; + } + return $result; + }); + + // comparison + + $env->set('=', fn ($a, $b) => $a == $b); + $env->set('<', fn ($a, $b) => $a < $b); + $env->set('>', fn ($a, $b) => $a > $b); + $env->set('<=', fn ($a, $b) => $a <= $b); + $env->set('>=', fn ($a, $b) => $a >= $b); + $env->set('!=', fn ($a, $b) => $a != $b); + + // types + + $env->set('type?', function ($a) { + return gettype($a); + }); return $env; } diff --git a/lisp.php b/lisp.php index bdcb558..b7784a7 100644 --- a/lisp.php +++ b/lisp.php @@ -2,16 +2,6 @@ require_once('classes.php'); -// types -define("SYMBOL", "SYMBOL"); -define("STRING", "STRING"); -define("NUMBER", "NUMBER"); -define("START", "START"); -define("END", "END"); - -// special sign for symbols -define("MAGIC", "ยง"); - function ml_tokenize(string $a): array { $tokens = []; @@ -19,21 +9,9 @@ function ml_tokenize(string $a): array $string = false; $parens = 0; - $addCurrent = function ($string = false) use (&$tokens, &$current) { - if ($current !== '' || $string) { - if ($string) { - $tokens[] = [STRING, $current]; - } elseif ($current == 'true') { - $tokens[] = [true]; - } elseif ($current == 'false') { - $tokens[] = [false]; - } elseif ($current == 'null') { - $tokens[] = [null]; - } elseif (is_numeric($current)) { - $tokens[] = [NUMBER, $current]; - } else { - $tokens[] = [SYMBOL, $current]; - } + $addCurrent = function () use (&$tokens, &$current) { + if ($current !== '') { + $tokens[] = $current; $current = ''; } }; @@ -41,36 +19,43 @@ function ml_tokenize(string $a): array for ($i = 0; $i < strlen($a); $i++) { $c = substr($a, $i, 1); - if ($c == '"') { - if ($string) { - // End of string - $addCurrent(true); - $string = false; - } else { - // Start of string - $string = true; - } - } elseif ($c == ' ' || $c == "\t" || $c == "\n" || $c == "\r") { - if ($string) { - // Include whitespace only inside strings - $current .= $c; - } else { - $addCurrent(); - } - } elseif ($c == '(') { - $addCurrent(); - $tokens[] = [START]; - $parens++; - } elseif ($c == ')') { - if ($parens == 0) { - throw new MadLispException("unexpected closing parenthesis"); - } - $addCurrent(); - $tokens[] = [END]; - $parens--; - } else { - // All other characters are included normally + if ($string) { + // Inside string, add all characters $current .= $c; + + // Stop at " + if ($c == '"') { + $addCurrent(); + $string = false; + } + } else { + // Not inside string + + if ($c == '"') { + // Start of string + $addCurrent(); + $current .= $c; + $string = true; + } elseif ($c == ' ' || $c == "\t" || $c == "\n" || $c == "\r") { + // Whitespace is ignored + $addCurrent(); + } elseif ($c == '(') { + // Start of list + $addCurrent(); + $tokens[] = '('; + $parens++; + } elseif ($c == ')') { + // End of list + if ($parens == 0) { + throw new MadLispException("unexpected closing parenthesis"); + } + $addCurrent(); + $tokens[] = ')'; + $parens--; + } else { + // All other characters + $current .= $c; + } } } @@ -89,48 +74,51 @@ function ml_tokenize(string $a): array function ml_read_form(array $tokens, int &$index) { - $a = $tokens[$index]; - - if ($a[0] == START) { + if ($tokens[$index] == '(') { return ml_read_list($tokens, $index); } else { return ml_read_atom($tokens, $index); } } -function ml_read_list(array $tokens, int &$index): array +function ml_read_list(array $tokens, int &$index): MLList { $result = []; // start tag $index++; - while ($tokens[$index][0] != END) { + while ($tokens[$index] != ')') { $result[] = ml_read_form($tokens, $index); } // end tag $index++; - return $result; + return new MLList($result); } function ml_read_atom(array $tokens, int &$index) { $a = $tokens[$index++]; - if ($a[0] == STRING) { - return $a[1]; - } elseif ($a[0] == SYMBOL) { - return MAGIC . $a[1]; - } elseif ($a[0] == NUMBER) { - if (filter_var($a[1], FILTER_VALIDATE_INT) !== false) { - return intval($a[1]); + if ($a === 'true') { + return true; + } elseif ($a === 'false') { + return false; + } elseif ($a === 'null') { + return null; + } elseif (substr($a, 0, 1) === '"') { + // string + return substr($a, 1, strlen($a) - 2); + } elseif (is_numeric($a)) { + if (filter_var($a, FILTER_VALIDATE_INT) !== false) { + return intval($a); } else { - return floatval($a[1]); + return floatval($a); } } else { - return $a[0]; + return new MLSymbol($a); } } @@ -146,16 +134,6 @@ function ml_parse(array $tokens): array return $result; } -function ml_is_symbol($a) -{ - return substr($a, 0, strlen(MAGIC)) === MAGIC; -} - -function ml_strip_symbol($a) -{ - return substr($a, strlen(MAGIC)); -} - function ml_read(string $code): array { $tokens = ml_tokenize($code); @@ -165,20 +143,22 @@ function ml_read(string $code): array return $expressions; } -function ml_eval($expr, Env $env) +function ml_eval($expr, MLEnv $env) { - if (is_array($expr)) { - // Evaluate list items - $expr = array_map(fn ($a) => ml_eval($a, $env), $expr); + if ($expr instanceof MLList && $expr->count() > 0) { + // Evaluate list contents + $results = array_map(fn ($a) => ml_eval($a, $env), $expr->getData()); - // If the first item is a function, call it - $fn = $expr[0] ?? null; - if ($fn && $fn instanceof Closure) { - $args = array_slice($expr, 1); - return $fn(...$args); + if ($results[0] instanceof Closure) { + // If the first item is a function, call it + $args = array_slice($results, 1); + return ($results[0])(...$args); + } else { + // Otherwise return new list with evaluated contents + return new MLList($results); } - } elseif (ml_is_symbol($expr)) { - return $env->get(ml_strip_symbol($expr)); + } elseif ($expr instanceof MLSymbol) { + return $env->get($expr->name()); } return $expr; @@ -188,16 +168,16 @@ function ml_print($a): string { if ($a instanceof Closure) { return ''; - } elseif (is_array($a)) { - return '(' . implode(' ', array_map('ml_print', $a)) . ')'; + } elseif ($a instanceof MLList) { + return '(' . implode(' ', array_map('ml_print', $a->getData())) . ')'; + } elseif ($a instanceof MLSymbol) { + return $a->name(); } elseif ($a === true) { return 'true'; } elseif ($a === false) { return 'false'; } elseif ($a === null) { return 'null'; - } elseif (ml_is_symbol($a)) { - return ml_strip_symbol($a); } elseif (is_string($a)) { return '"' . $a . '"'; } else { @@ -205,7 +185,7 @@ function ml_print($a): string } } -function ml_rep(string $input, Env $env): string +function ml_rep(string $input, MLEnv $env): string { $expressions = ml_read($input);