mirror of
https://github.com/peklaiho/madlisp.git
synced 2024-12-28 17:08:55 +00:00
611 lines
24 KiB
PHP
611 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* MadLisp language
|
|
* @link http://madlisp.com/
|
|
* @copyright Copyright (c) 2020 Pekka Laiho
|
|
*/
|
|
|
|
namespace MadLisp;
|
|
|
|
class Evaller
|
|
{
|
|
protected Tokenizer $tokenizer;
|
|
protected Reader $reader;
|
|
protected Printer $printer;
|
|
protected bool $safemode;
|
|
|
|
protected bool $debug = false;
|
|
|
|
// Keep cache of macro names so we can skip
|
|
// macro expansion when possible.
|
|
protected array $macros = [];
|
|
|
|
public function __construct(Tokenizer $tokenizer, Reader $reader, Printer $printer, bool $safemode)
|
|
{
|
|
$this->tokenizer = $tokenizer;
|
|
$this->reader = $reader;
|
|
$this->printer = $printer;
|
|
$this->safemode = $safemode;
|
|
}
|
|
|
|
public function eval($ast, Env $env, int $depth = 1)
|
|
{
|
|
// Loop for tail call optimization
|
|
$isTco = false;
|
|
while (true) {
|
|
|
|
// Show debug output
|
|
if ($this->debug) {
|
|
printf("%s %2d : ", $isTco ? ' tco' : 'eval', $depth);
|
|
$this->printer->print($ast, true);
|
|
print("\n");
|
|
$isTco = true;
|
|
}
|
|
|
|
// Return here after macro expansion
|
|
$expandMacros = true;
|
|
beginning:
|
|
|
|
// Handle response for anything but a list
|
|
if ($ast instanceof Symbol) {
|
|
return $env->get($ast->getName());
|
|
} elseif ($ast instanceof Vector || $ast instanceof Hash) {
|
|
$newData = [];
|
|
foreach ($ast->getData() as $key => $val) {
|
|
if ($val instanceof Symbol) {
|
|
$newData[$key] = $env->get($val->getName());
|
|
} elseif ($val instanceof Collection) {
|
|
$newData[$key] = $this->eval($val, $env, $depth + 1);
|
|
} else {
|
|
$newData[$key] = $val;
|
|
}
|
|
}
|
|
return $ast::new($newData);
|
|
} elseif ($ast instanceof MList == false) {
|
|
return $ast;
|
|
}
|
|
|
|
$astData = $ast->getData();
|
|
$astLength = count($astData);
|
|
|
|
// Empty list, we can return
|
|
if ($astLength == 0) {
|
|
return $ast;
|
|
}
|
|
|
|
// Handle special forms
|
|
if ($astData[0] instanceof Symbol) {
|
|
$symbolName = $astData[0]->getName();
|
|
|
|
// Handle macro expansion and go back to beginning to check
|
|
// if ast is still something we need to evaluate or not.
|
|
if ($expandMacros && array_key_exists($symbolName, $this->macros)) {
|
|
$ast = $this->macroexpand($ast, $env);
|
|
$expandMacros = false;
|
|
goto beginning;
|
|
}
|
|
|
|
if ($symbolName == 'and') {
|
|
if ($astLength == 1) {
|
|
return true;
|
|
}
|
|
|
|
for ($i = 1; $i < $astLength - 1; $i++) {
|
|
$value = $this->eval($astData[$i], $env, $depth + 1);
|
|
if ($value == false) {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
$ast = $astData[$astLength - 1];
|
|
continue; // tco
|
|
} elseif ($symbolName == 'case' || $symbolName == 'case-strict') {
|
|
if ($astLength < 3) {
|
|
throw new MadLispException("$symbolName requires at least 2 arguments");
|
|
}
|
|
|
|
$value = $this->eval($astData[1], $env, $depth + 1);
|
|
|
|
for ($i = 2; $i < $astLength; $i++) {
|
|
if (!($astData[$i] instanceof Seq)) {
|
|
throw new MadLispException("argument to $symbolName is not seq");
|
|
}
|
|
|
|
$data = $astData[$i]->getData();
|
|
|
|
if (count($data) < 2) {
|
|
throw new MadLispException("clause for $symbolName requires at least 2 arguments");
|
|
}
|
|
|
|
if ($data[0] instanceof Symbol && $data[0]->getName() == 'else') {
|
|
$test = true;
|
|
} elseif ($symbolName == 'case') {
|
|
$test = Util::valueForCompare($value) == Util::valueForCompare($data[0]);
|
|
} else {
|
|
// Strict comparison
|
|
$test = Util::valueForCompare($value) === Util::valueForCompare($data[0]);
|
|
}
|
|
|
|
if ($test) {
|
|
// Evaluate interval expressions
|
|
for ($j = 1; $j < count($data) - 1; $j++) {
|
|
$this->eval($data[$j], $env, $depth + 1);
|
|
}
|
|
|
|
// Evaluate last expression
|
|
$ast = $data[count($data) - 1];
|
|
continue 2; // tco
|
|
}
|
|
}
|
|
|
|
// No match
|
|
return null;
|
|
} elseif ($symbolName == 'cond') {
|
|
if ($astLength < 2) {
|
|
throw new MadLispException("cond requires at least 1 argument");
|
|
}
|
|
|
|
for ($i = 1; $i < $astLength; $i++) {
|
|
if (!($astData[$i] instanceof Seq)) {
|
|
throw new MadLispException("argument to cond is not seq");
|
|
}
|
|
|
|
$data = $astData[$i]->getData();
|
|
|
|
if (count($data) < 2) {
|
|
throw new MadLispException("clause for cond requires at least 2 arguments");
|
|
}
|
|
|
|
if ($data[0] instanceof Symbol && $data[0]->getName() == 'else') {
|
|
$test = true;
|
|
} else {
|
|
$test = $this->eval($data[0], $env, $depth + 1);
|
|
}
|
|
|
|
if ($test) {
|
|
// Evaluate interval expressions
|
|
for ($j = 1; $j < count($data) - 1; $j++) {
|
|
$this->eval($data[$j], $env, $depth + 1);
|
|
}
|
|
|
|
// Evaluate last expression
|
|
$ast = $data[count($data) - 1];
|
|
continue 2; // tco
|
|
}
|
|
}
|
|
|
|
// No match
|
|
return null;
|
|
} elseif ($symbolName == 'def') {
|
|
if ($astLength != 3) {
|
|
throw new MadLispException("def requires exactly 2 arguments");
|
|
} elseif (!($astData[1] instanceof Symbol)) {
|
|
throw new MadLispException("first argument to def is not symbol");
|
|
}
|
|
|
|
$name = $astData[1]->getName();
|
|
|
|
// Do not allow reserved symbols to be defined
|
|
$reservedSymbols = ['__FILE__', '__DIR__'];
|
|
if (in_array($name, $reservedSymbols)) {
|
|
throw new MadLispException("attempt to def reserved symbol $name");
|
|
}
|
|
|
|
$value = $this->eval($astData[2], $env, $depth + 1);
|
|
|
|
// Save macros in cache
|
|
if ($value instanceof Func && $value->isMacro()) {
|
|
// value does not matter, we check for key
|
|
$this->macros[$name] = 1;
|
|
}
|
|
|
|
return $env->set($name, $value);
|
|
} elseif ($symbolName == 'do') {
|
|
if ($astLength == 1) {
|
|
return null;
|
|
}
|
|
|
|
for ($i = 1; $i < $astLength - 1; $i++) {
|
|
$this->eval($astData[$i], $env, $depth + 1);
|
|
}
|
|
|
|
$ast = $astData[$astLength - 1];
|
|
continue; // tco
|
|
} elseif ($symbolName == 'env') {
|
|
if ($astLength != 1) {
|
|
throw new MadLispException("env does not take arguments");
|
|
}
|
|
|
|
return $env;
|
|
} elseif ($symbolName == 'eval') {
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("eval requires exactly 1 argument");
|
|
}
|
|
|
|
$ast = $this->eval($astData[1], $env, $depth + 1);
|
|
continue; // tco
|
|
} elseif ($symbolName == 'fn' || $symbolName == 'macro') {
|
|
if ($astLength != 3) {
|
|
throw new MadLispException("$symbolName requires exactly 2 arguments");
|
|
} elseif (!($astData[1] instanceof Seq)) {
|
|
throw new MadLispException("first argument to $symbolName is not seq");
|
|
}
|
|
|
|
$bindings = $astData[1]->getData();
|
|
foreach ($bindings as $bind) {
|
|
if (!($bind instanceof Symbol)) {
|
|
throw new MadLispException("binding key for $symbolName is not symbol");
|
|
}
|
|
}
|
|
|
|
$closure = function (...$args) use ($bindings, $env, $astData, $depth) {
|
|
$newEnv = new Env('closure', $env);
|
|
|
|
Util::bindArguments($newEnv, $bindings, $args);
|
|
|
|
return $this->eval($astData[2], $newEnv, $depth + 1);
|
|
};
|
|
|
|
return new UserFunc($closure, $astData[2], $env, $astData[1], $symbolName == 'macro');
|
|
} elseif ($symbolName == 'if') {
|
|
if ($astLength < 3 || $astLength > 4) {
|
|
throw new MadLispException("if requires 2 or 3 arguments");
|
|
}
|
|
|
|
$result = $this->eval($astData[1], $env, $depth + 1);
|
|
|
|
if ($result == true) {
|
|
$ast = $astData[2];
|
|
continue; // tco
|
|
} elseif ($astLength == 4) {
|
|
$ast = $astData[3];
|
|
continue; // tco
|
|
} else {
|
|
return null;
|
|
}
|
|
} elseif ($symbolName == 'let') {
|
|
if ($astLength < 3) {
|
|
throw new MadLispException("let requires at least 2 arguments");
|
|
} elseif (!($astData[1] instanceof Seq)) {
|
|
throw new MadLispException("first argument to let is not seq");
|
|
}
|
|
|
|
$bindings = $astData[1]->getData();
|
|
|
|
if (count($bindings) % 2 == 1) {
|
|
throw new MadLispException("uneven number of bindings for let");
|
|
}
|
|
|
|
$newEnv = new Env('let', $env);
|
|
|
|
for ($i = 0; $i < count($bindings) - 1; $i += 2) {
|
|
$key = $bindings[$i];
|
|
|
|
if (!($key instanceof Symbol)) {
|
|
throw new MadLispException("binding key for let is not symbol");
|
|
}
|
|
|
|
$val = $this->eval($bindings[$i + 1], $newEnv, $depth + 1);
|
|
$newEnv->set($key->getName(), $val);
|
|
}
|
|
|
|
// Eval interval expressions
|
|
for ($i = 2; $i < $astLength - 1; $i++) {
|
|
$this->eval($astData[$i], $newEnv, $depth + 1);
|
|
}
|
|
|
|
// Eval last expression
|
|
$ast = $astData[$astLength - 1];
|
|
$env = $newEnv;
|
|
continue; // tco
|
|
} elseif (!$this->safemode && $symbolName == 'load') {
|
|
// Load is here because we want to load into
|
|
// current $env which is hard otherwise.
|
|
|
|
// This is disabled now for safe-mode, but some
|
|
// use (maybe restricted) might need to be allowed.
|
|
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("load requires exactly 1 argument");
|
|
}
|
|
|
|
// We have to evaluate the argument, it could be a function
|
|
$filename = $this->eval($astData[1], $env, $depth + 1);
|
|
|
|
if (!is_string($filename)) {
|
|
throw new MadLispException("first argument to load is not string");
|
|
}
|
|
|
|
// Replace ~ with user home directory
|
|
// Expand relative path names into absolute
|
|
$targetFile = realpath(str_replace('~', $_SERVER['HOME'], $filename));
|
|
|
|
if (!$targetFile || !is_readable($targetFile)) {
|
|
throw new MadLispException("unable to read file $filename");
|
|
}
|
|
|
|
$input = @file_get_contents($targetFile);
|
|
|
|
// Remove #! from the beginning that is used by shell scripts
|
|
if (extension_loaded('pcre')) {
|
|
$input = preg_replace('/^#![^\n\r]*[\n\r]+/', '', $input, 1);
|
|
}
|
|
|
|
// Wrap input in a do to process multiple expressions
|
|
$input = "(do $input)";
|
|
|
|
$expr = $this->reader->read($this->tokenizer->tokenize($input));
|
|
|
|
// Handle special constants
|
|
$rootEnv = $env->getRoot();
|
|
$prevFile = $rootEnv->get('__FILE__');
|
|
$prevDir = $rootEnv->get('__DIR__');
|
|
$rootEnv->set('__FILE__', $targetFile);
|
|
$rootEnv->set('__DIR__', dirname($targetFile) . \DIRECTORY_SEPARATOR);
|
|
|
|
// Evaluate the contents
|
|
$ast = $this->eval($expr, $env, $depth + 1);
|
|
|
|
// Restore the special constants to previous values
|
|
$rootEnv->set('__FILE__', $prevFile);
|
|
$rootEnv->set('__DIR__', $prevDir);
|
|
|
|
continue; // tco
|
|
} elseif ($symbolName == 'macroexpand') {
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("macroexpand requires exactly 1 argument");
|
|
}
|
|
|
|
return $this->macroexpand($astData[1], $env);
|
|
} elseif ($symbolName == 'meta') {
|
|
if ($astLength != 3) {
|
|
throw new MadLispException("meta requires exactly 2 arguments");
|
|
} elseif (!is_string($astData[2])) {
|
|
throw new MadLispException("third argument to meta is not string");
|
|
}
|
|
|
|
$obj = $this->eval($astData[1], $env, $depth + 1);
|
|
$attribute = $astData[2];
|
|
|
|
if ($obj instanceof Env) {
|
|
if ($attribute == 'name') {
|
|
return $obj->getFullName();
|
|
} elseif ($attribute == 'parent') {
|
|
return $obj->getParent();
|
|
} else {
|
|
throw new MadLispException('unknown attribute for meta');
|
|
}
|
|
} elseif ($obj instanceof UserFunc) {
|
|
if ($attribute == 'args') {
|
|
return $obj->getBindings();
|
|
} elseif ($attribute == 'body') {
|
|
return $obj->getAst();
|
|
} elseif ($attribute == 'code') {
|
|
$name = $obj->isMacro() ? 'macro' : 'fn';
|
|
return new MList([new Symbol($name), $obj->getBindings(), $obj->getAst()]);
|
|
} elseif ($attribute == 'def') {
|
|
if ($astData[1] instanceof Symbol) {
|
|
$def = $obj->isMacro() ? 'defmacro' : 'defn';
|
|
return new MList([new Symbol($def), $astData[1], $obj->getBindings(), $obj->getAst()]);
|
|
} else {
|
|
throw new MadLispException('no name for def in meta');
|
|
}
|
|
} else {
|
|
throw new MadLispException('unknown attribute for meta');
|
|
}
|
|
} else {
|
|
throw new MadLispException('unknown entity for meta');
|
|
}
|
|
} elseif ($symbolName == 'or') {
|
|
if ($astLength == 1) {
|
|
return false;
|
|
}
|
|
|
|
for ($i = 1; $i < $astLength - 1; $i++) {
|
|
$value = $this->eval($astData[$i], $env, $depth + 1);
|
|
if ($value == true) {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
$ast = $astData[$astLength - 1];
|
|
continue; // tco
|
|
} elseif ($symbolName == 'quote') {
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("quote requires exactly 1 argument");
|
|
}
|
|
|
|
return $astData[1];
|
|
} elseif ($symbolName == 'quasiquote') {
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("quasiquote requires exactly 1 argument");
|
|
}
|
|
|
|
$ast = $this->quasiquote($astData[1]);
|
|
continue; // tco
|
|
} elseif ($symbolName == 'quasiquote-expand') {
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("quasiquote-expand requires exactly 1 argument");
|
|
}
|
|
|
|
return $this->quasiquote($astData[1]);
|
|
} elseif ($symbolName == 'try') {
|
|
if ($astLength != 3) {
|
|
throw new MadLispException("try requires exactly 2 arguments");
|
|
} elseif (!($astData[2] instanceof Seq)) {
|
|
throw new MadLispException("second argument to try is not seq");
|
|
}
|
|
|
|
$catch = $astData[2]->getData();
|
|
|
|
if (count($catch) == 3 && $catch[0] instanceof Symbol &&
|
|
$catch[0]->getName() == 'catch' && $catch[1] instanceof Symbol) {
|
|
|
|
try {
|
|
return $this->eval($astData[1], $env, $depth + 1);
|
|
} catch (\Throwable $ex) {
|
|
if ($ex instanceof MadLispUserException) {
|
|
$exVal = $ex->getValue();
|
|
} else {
|
|
// Return a hash-map for PHP exceptions
|
|
$exVal = new Hash([
|
|
'type' => get_class($ex),
|
|
'file' => $ex->getFile(),
|
|
'line' => $ex->getLine(),
|
|
'message' => $ex->getMessage()
|
|
]);
|
|
}
|
|
|
|
$newEnv = new Env('catch', $env);
|
|
$newEnv->set($catch[1]->getName(), $exVal);
|
|
|
|
$env = $newEnv;
|
|
$ast = $catch[2];
|
|
continue; // tco
|
|
}
|
|
} else {
|
|
throw new MadLispException("invalid form for catch");
|
|
}
|
|
} elseif ($symbolName == 'undef') {
|
|
if ($astLength != 2) {
|
|
throw new MadLispException("undef requires exactly 1 argument");
|
|
} elseif (!($astData[1] instanceof Symbol)) {
|
|
throw new MadLispException("first argument to undef is not symbol");
|
|
}
|
|
|
|
return $env->unset($astData[1]->getName());
|
|
} elseif ($symbolName == 'while') {
|
|
if ($astLength < 3) {
|
|
throw new MadLispException("while requires at least 2 arguments");
|
|
}
|
|
|
|
$result = null;
|
|
|
|
$test = $this->eval($astData[1], $env, $depth + 1);
|
|
while ($test) {
|
|
for ($i = 2; $i < $astLength; $i++) {
|
|
$result = $this->eval($astData[$i], $env, $depth + 1);
|
|
}
|
|
$test = $this->eval($astData[1], $env, $depth + 1);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
// Eval all items in list
|
|
$newData = [];
|
|
foreach ($astData as $a) {
|
|
if ($a instanceof Symbol) {
|
|
$newData[] = $env->get($a->getName());
|
|
} elseif ($a instanceof Collection) {
|
|
$newData[] = $this->eval($a, $env, $depth + 1);
|
|
} else {
|
|
$newData[] = $a;
|
|
}
|
|
}
|
|
|
|
// First item is function, rest are arguments
|
|
$func = $newData[0];
|
|
$args = array_slice($newData, 1);
|
|
|
|
if ($func instanceof CoreFunc) {
|
|
return $func->call($args);
|
|
} elseif ($func instanceof UserFunc) {
|
|
$ast = $func->getAst();
|
|
$env = $func->getEnv($args);
|
|
// tco
|
|
} else {
|
|
throw new MadLispException("eval: first item of list is not function");
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getDebug(): bool
|
|
{
|
|
return $this->debug;
|
|
}
|
|
|
|
public function setDebug(bool $val): void
|
|
{
|
|
$this->debug = $val;
|
|
}
|
|
|
|
private function macroexpand($ast, Env $env)
|
|
{
|
|
while ($ast instanceof MList) {
|
|
$data = $ast->getData();
|
|
if (count($data) > 0 && $data[0] instanceof Symbol) {
|
|
$fn = $env->get($data[0]->getName(), false);
|
|
if ($fn && $fn instanceof Func && $fn->isMacro()) {
|
|
$ast = $fn->call(array_slice($data, 1));
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $ast;
|
|
}
|
|
|
|
private function quasiquote($ast)
|
|
{
|
|
if ($ast instanceof MList) {
|
|
$data = $ast->getData();
|
|
|
|
// Check for unquote
|
|
if (count($data) > 0 && $data[0] instanceof Symbol && $data[0]->getName() == 'unquote') {
|
|
if (count($data) == 2) {
|
|
return $data[1];
|
|
} else {
|
|
throw new MadLispException("unquote requires exactly 1 argument");
|
|
}
|
|
}
|
|
|
|
return $this->quasiquoteLoop($data);
|
|
} elseif ($ast instanceof Vector) {
|
|
return new MList([
|
|
new Symbol('ltov'),
|
|
$this->quasiquoteLoop($ast->getData())
|
|
]);
|
|
} elseif ($ast instanceof Symbol || $ast instanceof Hash) {
|
|
// Quote other forms which are affected by evaluation
|
|
return new MList([
|
|
new Symbol('quote'),
|
|
$ast
|
|
]);
|
|
} else {
|
|
return $ast;
|
|
}
|
|
}
|
|
|
|
private function quasiquoteLoop(array $data): MList
|
|
{
|
|
$result = new MList();
|
|
|
|
for ($i = count($data) - 1; $i >= 0; $i--) {
|
|
$elt = $data[$i];
|
|
|
|
if ($elt instanceof MList && count($elt->getData()) > 0 && $elt->get(0) instanceof Symbol && $elt->get(0)->getName() == 'unquote-splice') {
|
|
if (count($elt->getData()) == 2) {
|
|
$result = new MList([
|
|
new Symbol('concat'),
|
|
$elt->get(1),
|
|
$result
|
|
]);
|
|
} else {
|
|
throw new MadLispException("unquote-splice requires exactly 1 argument");
|
|
}
|
|
} else {
|
|
$result = new MList([
|
|
new Symbol('cons'),
|
|
$this->quasiquote($elt),
|
|
$result
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|