This commit is contained in:
Pekka Laiho 2020-05-27 18:54:39 +07:00
parent 20f8aeab51
commit 766b162bc4
3 changed files with 194 additions and 110 deletions

View File

@ -2,21 +2,69 @@
class MadLispException extends Exception {} class MadLispException extends Exception {}
class Env abstract class MLCollection
{ {
private array $data = []; protected array $data = [];
private ?Env $parent;
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); 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) public function get(string $key)
{ {
if ($this->has($key)) { if ($this->has($key)) {
@ -27,9 +75,19 @@ class Env
throw new MadLispException("symbol $key not defined"); 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;
} }
} }

60
lib.php
View File

@ -2,16 +2,62 @@
require_once('classes.php'); require_once('classes.php');
function ml_get_env(): Env function ml_get_env(): MLEnv
{ {
$env = new Env(); $env = new MLEnv();
// basic arithmetic // basic arithmetic
$env->set('+', fn (...$args) => array_sum($args));
$env->set('-', fn ($a, $b) => $a - $b); $env->set('+', function (...$args) {
$env->set('*', fn ($a, $b) => $a * $b); return array_sum($args);
$env->set('/', fn ($a, $b) => $a / $b); });
$env->set('%', fn ($a, $b) => $a % $b);
$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; return $env;
} }

142
lisp.php
View File

@ -2,16 +2,6 @@
require_once('classes.php'); 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 function ml_tokenize(string $a): array
{ {
$tokens = []; $tokens = [];
@ -19,21 +9,9 @@ function ml_tokenize(string $a): array
$string = false; $string = false;
$parens = 0; $parens = 0;
$addCurrent = function ($string = false) use (&$tokens, &$current) { $addCurrent = function () use (&$tokens, &$current) {
if ($current !== '' || $string) { if ($current !== '') {
if ($string) { $tokens[] = $current;
$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];
}
$current = ''; $current = '';
} }
}; };
@ -41,38 +19,45 @@ function ml_tokenize(string $a): array
for ($i = 0; $i < strlen($a); $i++) { for ($i = 0; $i < strlen($a); $i++) {
$c = substr($a, $i, 1); $c = substr($a, $i, 1);
if ($c == '"') {
if ($string) { if ($string) {
// End of string // Inside string, add all characters
$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; $current .= $c;
} else {
// Stop at "
if ($c == '"') {
$addCurrent(); $addCurrent();
$string = false;
} }
} elseif ($c == '(') { } else {
// Not inside string
if ($c == '"') {
// Start of string
$addCurrent(); $addCurrent();
$tokens[] = [START]; $current .= $c;
$string = true;
} elseif ($c == ' ' || $c == "\t" || $c == "\n" || $c == "\r") {
// Whitespace is ignored
$addCurrent();
} elseif ($c == '(') {
// Start of list
$addCurrent();
$tokens[] = '(';
$parens++; $parens++;
} elseif ($c == ')') { } elseif ($c == ')') {
// End of list
if ($parens == 0) { if ($parens == 0) {
throw new MadLispException("unexpected closing parenthesis"); throw new MadLispException("unexpected closing parenthesis");
} }
$addCurrent(); $addCurrent();
$tokens[] = [END]; $tokens[] = ')';
$parens--; $parens--;
} else { } else {
// All other characters are included normally // All other characters
$current .= $c; $current .= $c;
} }
} }
}
// Add last also // Add last also
$addCurrent(); $addCurrent();
@ -89,48 +74,51 @@ function ml_tokenize(string $a): array
function ml_read_form(array $tokens, int &$index) function ml_read_form(array $tokens, int &$index)
{ {
$a = $tokens[$index]; if ($tokens[$index] == '(') {
if ($a[0] == START) {
return ml_read_list($tokens, $index); return ml_read_list($tokens, $index);
} else { } else {
return ml_read_atom($tokens, $index); 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 = []; $result = [];
// start tag // start tag
$index++; $index++;
while ($tokens[$index][0] != END) { while ($tokens[$index] != ')') {
$result[] = ml_read_form($tokens, $index); $result[] = ml_read_form($tokens, $index);
} }
// end tag // end tag
$index++; $index++;
return $result; return new MLList($result);
} }
function ml_read_atom(array $tokens, int &$index) function ml_read_atom(array $tokens, int &$index)
{ {
$a = $tokens[$index++]; $a = $tokens[$index++];
if ($a[0] == STRING) { if ($a === 'true') {
return $a[1]; return true;
} elseif ($a[0] == SYMBOL) { } elseif ($a === 'false') {
return MAGIC . $a[1]; return false;
} elseif ($a[0] == NUMBER) { } elseif ($a === 'null') {
if (filter_var($a[1], FILTER_VALIDATE_INT) !== false) { return null;
return intval($a[1]); } 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 { } else {
return floatval($a[1]); return floatval($a);
} }
} else { } else {
return $a[0]; return new MLSymbol($a);
} }
} }
@ -146,16 +134,6 @@ function ml_parse(array $tokens): array
return $result; 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 function ml_read(string $code): array
{ {
$tokens = ml_tokenize($code); $tokens = ml_tokenize($code);
@ -165,20 +143,22 @@ function ml_read(string $code): array
return $expressions; return $expressions;
} }
function ml_eval($expr, Env $env) function ml_eval($expr, MLEnv $env)
{ {
if (is_array($expr)) { if ($expr instanceof MLList && $expr->count() > 0) {
// Evaluate list items // Evaluate list contents
$expr = array_map(fn ($a) => ml_eval($a, $env), $expr); $results = array_map(fn ($a) => ml_eval($a, $env), $expr->getData());
if ($results[0] instanceof Closure) {
// If the first item is a function, call it // If the first item is a function, call it
$fn = $expr[0] ?? null; $args = array_slice($results, 1);
if ($fn && $fn instanceof Closure) { return ($results[0])(...$args);
$args = array_slice($expr, 1); } else {
return $fn(...$args); // Otherwise return new list with evaluated contents
return new MLList($results);
} }
} elseif (ml_is_symbol($expr)) { } elseif ($expr instanceof MLSymbol) {
return $env->get(ml_strip_symbol($expr)); return $env->get($expr->name());
} }
return $expr; return $expr;
@ -188,16 +168,16 @@ function ml_print($a): string
{ {
if ($a instanceof Closure) { if ($a instanceof Closure) {
return '<function>'; return '<function>';
} elseif (is_array($a)) { } elseif ($a instanceof MLList) {
return '(' . implode(' ', array_map('ml_print', $a)) . ')'; return '(' . implode(' ', array_map('ml_print', $a->getData())) . ')';
} elseif ($a instanceof MLSymbol) {
return $a->name();
} elseif ($a === true) { } elseif ($a === true) {
return 'true'; return 'true';
} elseif ($a === false) { } elseif ($a === false) {
return 'false'; return 'false';
} elseif ($a === null) { } elseif ($a === null) {
return 'null'; return 'null';
} elseif (ml_is_symbol($a)) {
return ml_strip_symbol($a);
} elseif (is_string($a)) { } elseif (is_string($a)) {
return '"' . $a . '"'; return '"' . $a . '"';
} else { } 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); $expressions = ml_read($input);