split code to multiple classes

This commit is contained in:
Pekka Laiho 2020-05-28 17:10:00 +07:00
parent 311d074546
commit 80faff86f5
7 changed files with 303 additions and 254 deletions

View File

@ -10,3 +10,13 @@ function ml_get_env(): MadLisp\Env
return $env; return $env;
} }
function ml_get_lisp(): MadLisp\Lisp
{
$tokenizer = new MadLisp\Tokenizer();
$reader = new MadLisp\Reader();
$eval = new MadLisp\Evaller();
$printer = new MadLisp\Printer();
return new MadLisp\Lisp($tokenizer, $reader, $eval, $printer);
}

View File

@ -2,7 +2,7 @@
require('bootstrap.php'); require('bootstrap.php');
$env = ml_get_env(); $env = ml_get_env();
$lisp = new MadLisp\Lisp(); $lisp = ml_get_lisp();
while (true) { while (true) {
$input = readline('> '); $input = readline('> ');

79
src/Evaller.php Normal file
View File

@ -0,0 +1,79 @@
<?php
namespace MadLisp;
use Closure;
class Evaller
{
public function eval(array $expressions, Env $env): array
{
$results = [];
foreach ($expressions as $expr) {
$results[] = $this->doEval($expr, $env);
}
return $results;
}
public function doEval($expr, Env $env)
{
if ($expr instanceof MList && $expr->count() > 0) {
$first = $expr->get(0);
if ($first instanceof Symbol) {
// Special built-in features
if ($first->getName() == 'env') {
return $env;
} elseif ($first->getName() == 'quote') {
if ($expr->count() != 2) {
throw new MadLispException("quote requires exactly 1 argument");
}
return $expr->get(1);
} elseif ($first->getName() == 'if') {
if ($expr->count() != 4) {
throw new MadLispException("if requires exactly 3 arguments");
}
// Eval condition
$result = $this->doEval($expr->get(1), $env);
// Eval true or false branch and return it
if ($result == true) {
return $this->doEval($expr->get(2), $env);
} else {
return $this->doEval($expr->get(3), $env);
}
}
// Normal symbol, fetch from env
$first = $env->get($first->getName());
}
if (!($first instanceof Closure)) {
throw new MadLispException("first argument of list is not function");
}
$args = array_slice($expr->getData(), 1);
// Evaluate args
$args = array_map(fn ($a) => $this->doEval($a, $env), $args);
// Call func and return result
return $first(...$args);
} elseif ($expr instanceof Hash) {
// Hash: return new hash with all items evaluated
$items = [];
foreach ($expr->getData() as $key => $val) {
$items[$key] = $this->doEval($val, $env);
}
return new Hash($items);
} elseif ($expr instanceof Symbol) {
return $env->get($expr->getName());
}
// Return the expression unchanged
return $expr;
}
}

View File

@ -1,268 +1,29 @@
<?php <?php
namespace MadLisp; namespace MadLisp;
use Closure;
class Lisp class Lisp
{ {
public function tokenize(string $a): array protected Tokenizer $tokenizer;
protected Reader $reader;
protected Evaller $eval;
protected Printer $printer;
public function __construct(Tokenizer $tokenizer, Reader $reader, Evaller $eval, Printer $printer)
{ {
$tokens = []; $this->tokenizer = $tokenizer;
$current = ''; $this->reader = $reader;
$string = false; $this->eval = $eval;
$parens = 0; $this->printer = $printer;
$addCurrent = function () use (&$tokens, &$current) {
if ($current !== '') {
$tokens[] = $current;
$current = '';
}
};
for ($i = 0; $i < strlen($a); $i++) {
$c = substr($a, $i, 1);
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;
}
}
}
// Add last also
$addCurrent();
// Check for errors
if ($parens != 0) {
throw new MadLispException("missing closing parenthesis");
} elseif ($string) {
throw new MadLispException("unterminated string");
}
return $tokens;
}
public function parse(array $tokens): array
{
$result = [];
$index = 0;
while ($index < count($tokens)) {
$result[] = $this->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) {
$first = $expr->get(0);
if ($first instanceof Symbol) {
// Special built-in features
if ($first->getName() == 'env') {
return $env;
} elseif ($first->getName() == 'quote') {
if ($expr->count() != 2) {
throw new MadLispException("quote requires exactly 1 argument");
}
return $expr->get(1);
} elseif ($first->getName() == 'if') {
if ($expr->count() != 4) {
throw new MadLispException("if requires exactly 3 arguments");
}
// Eval condition
$result = $this->eval($expr->get(1), $env);
// Eval true or false branch and return it
if ($result == true) {
return $this->eval($expr->get(2), $env);
} else {
return $this->eval($expr->get(3), $env);
}
}
// Normal symbol, fetch from env
$first = $env->get($first->getName());
}
if (!($first instanceof Closure)) {
throw new MadLispException("first argument of list is not function");
}
$args = array_slice($expr->getData(), 1);
// Evaluate args
$args = array_map(fn ($a) => $this->eval($a, $env), $args);
// Call func and return result
return $first(...$args);
} 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->getName());
}
// Return the expression unchanged
return $expr;
}
public function print(array $items): void
{
for ($i = 0; $i < count($items); $i++) {
if ($i > 0) {
print(' ');
}
$this->doPrint($items[$i]);
}
} }
public function rep(string $input, Env $env): void public function rep(string $input, Env $env): void
{ {
$expressions = $this->read($input); $tokens = $this->tokenizer->tokenize($input);
$results = array_map(fn ($expr) => $this->eval($expr, $env), $expressions); $expressions = $this->reader->read($tokens);
$this->print($results); $results = $this->eval->eval($expressions, $env);
}
private function doPrint($a): void $this->printer->print($results);
{
if ($a instanceof Closure) {
print('<function>');
} elseif ($a instanceof MList) {
print('(');
for ($i = 0; $i < $a->count(); $i++) {
if ($i > 0) {
print(' ');
}
$this->doPrint($a->get($i));
}
print(')');
} elseif ($a instanceof Hash) {
print('{');
$keys = array_keys($a->getData());
for ($i = 0; $i < count($keys); $i++) {
if ($i > 0) {
print(' ');
}
$this->doPrint($keys[$i]);
print(':');
$this->doPrint($a->get($keys[$i]));
}
print('}');
} elseif ($a instanceof Symbol) {
print($a->getName());
} elseif ($a === true) {
print('true');
} elseif ($a === false) {
print('false');
} elseif ($a === null) {
print('null');
} elseif (is_string($a)) {
print('"' . $a . '"');
} else {
print($a);
}
}
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);
}
} }
} }

57
src/Printer.php Normal file
View File

@ -0,0 +1,57 @@
<?php
namespace MadLisp;
use Closure;
class Printer
{
public function print(array $items): void
{
for ($i = 0; $i < count($items); $i++) {
if ($i > 0) {
print(' ');
}
$this->doPrint($items[$i]);
}
}
private function doPrint($a): void
{
if ($a instanceof Closure) {
print('<function>');
} elseif ($a instanceof MList) {
print('(');
for ($i = 0; $i < $a->count(); $i++) {
if ($i > 0) {
print(' ');
}
$this->doPrint($a->get($i));
}
print(')');
} elseif ($a instanceof Hash) {
print('{');
$keys = array_keys($a->getData());
for ($i = 0; $i < count($keys); $i++) {
if ($i > 0) {
print(' ');
}
$this->doPrint($keys[$i]);
print(':');
$this->doPrint($a->get($keys[$i]));
}
print('}');
} elseif ($a instanceof Symbol) {
print($a->getName());
} elseif ($a === true) {
print('true');
} elseif ($a === false) {
print('false');
} elseif ($a === null) {
print('null');
} elseif (is_string($a)) {
print('"' . $a . '"');
} else {
print($a);
}
}
}

67
src/Reader.php Normal file
View File

@ -0,0 +1,67 @@
<?php
namespace MadLisp;
class Reader
{
public function read(array $tokens): array
{
$result = [];
$index = 0;
while ($index < count($tokens)) {
$result[] = $this->readForm($tokens, $index);
}
return $result;
}
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);
}
}
}

75
src/Tokenizer.php Normal file
View File

@ -0,0 +1,75 @@
<?php
namespace MadLisp;
class Tokenizer
{
public function tokenize(string $a): array
{
$tokens = [];
$current = '';
$string = false;
$parens = 0;
$addCurrent = function () use (&$tokens, &$current) {
if ($current !== '') {
$tokens[] = $current;
$current = '';
}
};
for ($i = 0; $i < strlen($a); $i++) {
$c = substr($a, $i, 1);
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;
}
}
}
// Add last also
$addCurrent();
// Check for errors
if ($parens != 0) {
throw new MadLispException("missing closing parenthesis");
} elseif ($string) {
throw new MadLispException("unterminated string");
}
return $tokens;
}
}