diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 0000000..229c1af --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,12 @@ +register($env); + + return $env; +} diff --git a/classes.php b/classes.php deleted file mode 100644 index 17ff3a8..0000000 --- a/classes.php +++ /dev/null @@ -1,93 +0,0 @@ -data = $data; - } - - 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"); - } -} - -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)) { - return $this->data[$key]; - } elseif ($this->parent) { - return $this->parent->get($key); - } - - throw new MadLispException("symbol $key not defined"); - } - - public function set(string $key, $value): void - { - $this->data[$key] = $value; - } -} - -class MLSymbol -{ - protected string $name; - - public function __construct(string $name) - { - $this->name = $name; - } - - public function name(): string - { - return $this->name; - } -} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9761dac --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "maddy83/madlisp", + "description": "Lisp interpreter for the MadLisp language.", + "license": "MIT", + "authors": [ + { + "name": "Pekka Laiho", + "email": "pekka.i.laiho@gmail.com" + } + ], + "require": { + + }, + "require-dev": { + + }, + "autoload": { + "psr-4": { + "MadLisp\\": "src/" + } + } +} diff --git a/lib.php b/lib.php deleted file mode 100644 index b55156a..0000000 --- a/lib.php +++ /dev/null @@ -1,151 +0,0 @@ -set('or', function (...$args) { - // return first true - for ($i = 0; $i < count($args) - 1; $i++) { - if ($args[$i] == true) { - return $args[$i]; - } - } - - // return last - return $args[count($args) - 1]; - }); - - $env->set('and', function (...$args) { - // return first false - for ($i = 0; $i < count($args) - 1; $i++) { - if ($args[$i] == false) { - return $args[$i]; - } - } - - // return last - return $args[count($args) - 1]; - }); - - $env->set('not', fn ($a) => !$a); - - // arithmetic - - $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) { - if ($a instanceof Closure) { - return 'function'; - } elseif ($a instanceof MLList) { - return 'list'; - } elseif ($a instanceof MLHash) { - return 'hash'; - } elseif ($a instanceof MLSymbol) { - return 'symbol'; - } elseif ($a === true || $a === false) { - return 'bool'; - } elseif ($a === null) { - return 'null'; - } elseif (is_int($a)) { - return 'int'; - } elseif (is_float($a)) { - return 'float'; - } else { - return 'string'; - } - }); - - $env->set('fn?', fn ($a) => $a instanceof Closure); - $env->set('list?', fn ($a) => $a instanceof MLList); - $env->set('hash?', fn ($a) => $a instanceof MLHash); - $env->set('sym?', fn ($a) => $a instanceof MLSymbol); - $env->set('bool?', fn ($a) => $a === true || $a === false); - $env->set('true?', fn ($a) => $a == true); // not strict - $env->set('false?', fn ($a) => $a == false); // not strict - $env->set('null?', fn ($a) => $a === null); - $env->set('int?', fn ($a) => is_int($a)); - $env->set('float?', fn ($a) => is_float($a)); - $env->set('str?', fn ($a) => is_string($a)); - - // collections - - $env->set('list', function (...$args) { - return new MLList($args); - }); - - $env->set('hash', function (...$args) { - if (count($args) % 2 == 1) { - throw new MadLispException('uneven number of arguments for hash'); - } - - $data = []; - - for ($i = 0; $i < count($args) - 1; $i += 2) { - $key = $args[$i]; - $val = $args[$i + 1]; - - if (!is_string($key)) { - throw new MadLispException('invalid key for hash (not string)'); - } - - $data[$key] = $val; - } - - return new MLHash($data); - }); - - // for debugging! - $env->set('eval', fn ($a) => ml_eval($a, $env)); - - return $env; -} diff --git a/lisp.php b/lisp.php deleted file mode 100644 index 1acd136..0000000 --- a/lisp.php +++ /dev/null @@ -1,210 +0,0 @@ -count() > 0) { - // Evaluate list contents - $results = array_map(fn ($a) => ml_eval($a, $env), $expr->getData()); - - $fn = $results[0]; - - if ($fn instanceof Closure) { - // If the first item is a function, call it - $args = array_slice($results, 1); - return $fn(...$args); - } else { - // Otherwise return new list with evaluated contents - return new MLList($results); - } - } elseif ($expr instanceof MLHash) { - // Hash: return new hash with all items evaluated - $items = []; - foreach ($expr->getData() as $key => $val) { - $items[$key] = ml_eval($val, $env); - } - return new MLHash($items); - } elseif ($expr instanceof MLSymbol) { - return $env->get($expr->name()); - } - - return $expr; -} - -function ml_print($a): string -{ - if ($a instanceof Closure) { - return ''; - } elseif ($a instanceof MLList) { - return '(' . implode(' ', array_map('ml_print', $a->getData())) . ')'; - } elseif ($a instanceof MLHash) { - $items = []; - foreach ($a->getData() as $key => $val) { - $items[] = ml_print($key) . ':' . ml_print($val); - } - return '{' . implode(' ', $items) . '}'; - } elseif ($a instanceof MLSymbol) { - return $a->name(); - } elseif ($a === true) { - return 'true'; - } elseif ($a === false) { - return 'false'; - } elseif ($a === null) { - return 'null'; - } elseif (is_string($a)) { - return '"' . $a . '"'; - } else { - return $a; - } -} - -function ml_rep(string $input, MLEnv $env): string -{ - $expressions = ml_read($input); - - $results = array_map(fn ($expr) => ml_eval($expr, $env), $expressions); - - return implode(" ", array_map('ml_print', $results)); -} diff --git a/repl.php b/repl.php index 3d9cfd4..3302b01 100644 --- a/repl.php +++ b/repl.php @@ -1,15 +1,14 @@ '); try { - print(ml_rep($input, $env)); + $lisp->rep($input, $env); } catch (MadLispException $ex) { print('error: ' . $ex->getMessage()); } diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 0000000..0a59b4c --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,27 @@ +data = $data; + } + + 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; + } +} diff --git a/src/Env.php b/src/Env.php new file mode 100644 index 0000000..eb3a368 --- /dev/null +++ b/src/Env.php @@ -0,0 +1,28 @@ +parent = $parent; + } + + public function get(string $key) + { + if ($this->has($key)) { + return $this->data[$key]; + } elseif ($this->parent) { + return $this->parent->get($key); + } + + throw new MadLispException("symbol $key not defined in env"); + } + + public function set(string $key, $value): void + { + $this->data[$key] = $value; + } +} diff --git a/src/Hash.php b/src/Hash.php new file mode 100644 index 0000000..0b4bcc0 --- /dev/null +++ b/src/Hash.php @@ -0,0 +1,14 @@ +has($key)) { + return $this->data[$key]; + } + + throw new MadLispException("hash does not contain key $key"); + } +} diff --git a/src/Lib/Core.php b/src/Lib/Core.php new file mode 100644 index 0000000..4ea88a7 --- /dev/null +++ b/src/Lib/Core.php @@ -0,0 +1,156 @@ +set('or', function (...$args) { + // return first true + for ($i = 0; $i < count($args) - 1; $i++) { + if ($args[$i] == true) { + return $args[$i]; + } + } + + // return last + return $args[count($args) - 1]; + }); + + $env->set('and', function (...$args) { + // return first false + for ($i = 0; $i < count($args) - 1; $i++) { + if ($args[$i] == false) { + return $args[$i]; + } + } + + // return last + return $args[count($args) - 1]; + }); + + $env->set('not', fn ($a) => !$a); + + // arithmetic + + $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) { + if ($a instanceof Closure) { + return 'function'; + } elseif ($a instanceof MList) { + return 'list'; + } elseif ($a instanceof Hash) { + return 'hash'; + } elseif ($a instanceof Symbol) { + return 'symbol'; + } elseif ($a === true || $a === false) { + return 'bool'; + } elseif ($a === null) { + return 'null'; + } elseif (is_int($a)) { + return 'int'; + } elseif (is_float($a)) { + return 'float'; + } else { + return 'string'; + } + }); + + $env->set('fn?', fn ($a) => $a instanceof Closure); + $env->set('list?', fn ($a) => $a instanceof MList); + $env->set('hash?', fn ($a) => $a instanceof Hash); + $env->set('sym?', fn ($a) => $a instanceof Symbol); + $env->set('bool?', fn ($a) => $a === true || $a === false); + $env->set('true?', fn ($a) => $a == true); // not strict + $env->set('false?', fn ($a) => $a == false); // not strict + $env->set('null?', fn ($a) => $a === null); + $env->set('int?', fn ($a) => is_int($a)); + $env->set('float?', fn ($a) => is_float($a)); + $env->set('str?', fn ($a) => is_string($a)); + + // collections + + $env->set('list', function (...$args) { + return new MList($args); + }); + + $env->set('hash', function (...$args) { + if (count($args) % 2 == 1) { + throw new MadLispException('uneven number of arguments for hash'); + } + + $data = []; + + for ($i = 0; $i < count($args) - 1; $i += 2) { + $key = $args[$i]; + $val = $args[$i + 1]; + + if (!is_string($key)) { + throw new MadLispException('invalid key for hash (not string)'); + } + + $data[$key] = $val; + } + + return new Hash($data); + }); + + // for debugging! + $env->set('eval', fn ($a) => ml_eval($a, $env)); + } +} diff --git a/src/Lib/ILib.php b/src/Lib/ILib.php new file mode 100644 index 0000000..2dfdd35 --- /dev/null +++ b/src/Lib/ILib.php @@ -0,0 +1,9 @@ +readForm($tokens, $index); + } + + return $result; + } + + public function read(string $code): array + { + $tokens = $this->tokenize($code); + + $expressions = $this->parse($tokens); + + return $expressions; + } + + public function eval($expr, Env $env) + { + if ($expr instanceof MList && $expr->count() > 0) { + // Evaluate list contents + $results = array_map(fn ($a) => $this->eval($a, $env), $expr->getData()); + + $fn = $results[0]; + + if ($fn instanceof Closure) { + // If the first item is a function, call it + $args = array_slice($results, 1); + return $fn(...$args); + } else { + // Otherwise return new list with evaluated contents + return new MList($results); + } + } elseif ($expr instanceof Hash) { + // Hash: return new hash with all items evaluated + $items = []; + foreach ($expr->getData() as $key => $val) { + $items[$key] = $this->eval($val, $env); + } + return new Hash($items); + } elseif ($expr instanceof Symbol) { + return $env->get($expr->name()); + } + + return $expr; + } + + public function print($a): string + { + $result = $a; + + if ($a instanceof Closure) { + $result = ''; + } elseif ($a instanceof MList) { + $items = []; + foreach ($a->getData() as $val) { + $items[] = $this->print($val); + } + $result = '(' . implode(' ', $items) . ')'; + } elseif ($a instanceof Hash) { + $items = []; + foreach ($a->getData() as $key => $val) { + $items[] = $this->print($key) . ':' . $this->print($val); + } + $result = '{' . implode(' ', $items) . '}'; + } elseif ($a instanceof Symbol) { + $result = $a->name(); + } elseif ($a === true) { + $result = 'true'; + } elseif ($a === false) { + $result = 'false'; + } elseif ($a === null) { + $result = 'null'; + } elseif (is_string($a)) { + $result = '"' . $a . '"'; + } + + return $result; + } + + public function rep(string $input, Env $env): void + { + $expressions = $this->read($input); + + $results = array_map(fn ($expr) => $this->eval($expr, $env), $expressions); + + $output = array_map(fn ($res) => $this->print($res), $results); + + print(implode(' ', $output)); + } + + private function readForm(array $tokens, int &$index) + { + if ($tokens[$index] == '(') { + return $this->readList($tokens, $index); + } else { + return $this->readAtom($tokens, $index); + } + } + + private function readList(array $tokens, int &$index): MList + { + $result = []; + + // start tag + $index++; + + while ($tokens[$index] != ')') { + $result[] = $this->readForm($tokens, $index); + } + + // end tag + $index++; + + return new MList($result); + } + + private function readAtom(array $tokens, int &$index) + { + $a = $tokens[$index++]; + + 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); + } + } else { + return new Symbol($a); + } + } +} diff --git a/src/MList.php b/src/MList.php new file mode 100644 index 0000000..74e296b --- /dev/null +++ b/src/MList.php @@ -0,0 +1,14 @@ +has($index)) { + return $this->data[$index]; + } + + throw new MadLispException("list does not contain index $index"); + } +} diff --git a/src/MadLispException.php b/src/MadLispException.php new file mode 100644 index 0000000..5715647 --- /dev/null +++ b/src/MadLispException.php @@ -0,0 +1,7 @@ +name = $name; + } + + public function name(): string + { + return $this->name; + } +}