mirror of
https://github.com/peklaiho/madlisp.git
synced 2024-11-22 21:35:03 +00:00
initial support for macros
This commit is contained in:
parent
2369dbeac7
commit
c6193b6ea2
10
src/Env.php
10
src/Env.php
@ -21,15 +21,19 @@ class Env extends Hash
|
|||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $key)
|
public function get(string $key, bool $throw = true)
|
||||||
{
|
{
|
||||||
if (array_key_exists($key, $this->data)) {
|
if (array_key_exists($key, $this->data)) {
|
||||||
return $this->data[$key];
|
return $this->data[$key];
|
||||||
} elseif ($this->parent) {
|
} elseif ($this->parent) {
|
||||||
return $this->parent->get($key);
|
return $this->parent->get($key, $throw);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new MadLispException("symbol $key not defined in env");
|
if ($throw) {
|
||||||
|
throw new MadLispException("symbol $key not defined in env");
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getParent(): ?Env
|
public function getParent(): ?Env
|
||||||
|
@ -37,7 +37,15 @@ class Evaller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not list
|
// Return fast for optimization if not list
|
||||||
|
if (!($ast instanceof MList)) {
|
||||||
|
return $this->evalAst($ast, $env);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform macro expansion
|
||||||
|
$ast = $this->macroexpand($ast, $env);
|
||||||
|
|
||||||
|
// After macro expansion we have to check for not-a-list again
|
||||||
if (!($ast instanceof MList)) {
|
if (!($ast instanceof MList)) {
|
||||||
return $this->evalAst($ast, $env);
|
return $this->evalAst($ast, $env);
|
||||||
}
|
}
|
||||||
@ -135,23 +143,23 @@ class Evaller
|
|||||||
|
|
||||||
$ast = $this->eval($astData[1], $env);
|
$ast = $this->eval($astData[1], $env);
|
||||||
continue; // tco
|
continue; // tco
|
||||||
} elseif ($symbolName == 'fn') {
|
} elseif ($symbolName == 'fn' || $symbolName == 'macro') {
|
||||||
if ($astLength != 3) {
|
if ($astLength != 3) {
|
||||||
throw new MadLispException("fn requires exactly 2 arguments");
|
throw new MadLispException("$symbolName requires exactly 2 arguments");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!($astData[1] instanceof Seq)) {
|
if (!($astData[1] instanceof Seq)) {
|
||||||
throw new MadLispException("first argument to fn is not seq");
|
throw new MadLispException("first argument to $symbolName is not seq");
|
||||||
}
|
}
|
||||||
|
|
||||||
$bindings = $astData[1]->getData();
|
$bindings = $astData[1]->getData();
|
||||||
foreach ($bindings as $bind) {
|
foreach ($bindings as $bind) {
|
||||||
if (!($bind instanceof Symbol)) {
|
if (!($bind instanceof Symbol)) {
|
||||||
throw new MadLispException("binding key for fn is not symbol");
|
throw new MadLispException("binding key for $symbolName is not symbol");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$closure = function (...$args) use ($bindings, $ast, $env, $astData) {
|
$closure = function (...$args) use ($bindings, $env, $astData) {
|
||||||
$newEnv = new Env('closure', $env);
|
$newEnv = new Env('closure', $env);
|
||||||
|
|
||||||
for ($i = 0; $i < count($bindings); $i++) {
|
for ($i = 0; $i < count($bindings); $i++) {
|
||||||
@ -161,7 +169,7 @@ class Evaller
|
|||||||
return $this->eval($astData[2], $newEnv);
|
return $this->eval($astData[2], $newEnv);
|
||||||
};
|
};
|
||||||
|
|
||||||
return new UserFunc($closure, $astData[2], $env, $astData[1]);
|
return new UserFunc($closure, $astData[2], $env, $astData[1], $symbolName == 'macro');
|
||||||
} elseif ($symbolName == 'if') {
|
} elseif ($symbolName == 'if') {
|
||||||
if ($astLength < 3 || $astLength > 4) {
|
if ($astLength < 3 || $astLength > 4) {
|
||||||
throw new MadLispException("if requires 2 or 3 arguments");
|
throw new MadLispException("if requires 2 or 3 arguments");
|
||||||
@ -257,6 +265,12 @@ class Evaller
|
|||||||
$rootEnv->set('__DIR__', $prevDir);
|
$rootEnv->set('__DIR__', $prevDir);
|
||||||
|
|
||||||
continue; // tco
|
continue; // tco
|
||||||
|
} elseif ($symbolName == 'macroexpand') {
|
||||||
|
if ($astLength != 2) {
|
||||||
|
throw new MadLispException("macroexpand requires exactly 1 argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->macroexpand($astData[1], $env);
|
||||||
} elseif ($symbolName == 'or') {
|
} elseif ($symbolName == 'or') {
|
||||||
if ($astLength == 1) {
|
if ($astLength == 1) {
|
||||||
return false;
|
return false;
|
||||||
@ -344,6 +358,31 @@ class Evaller
|
|||||||
return $ast;
|
return $ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getMacroFn($ast, Env $env): ?Func
|
||||||
|
{
|
||||||
|
if ($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()) {
|
||||||
|
return $fn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function macroexpand($ast, Env $env)
|
||||||
|
{
|
||||||
|
while (($fn = $this->getMacroFn($ast, $env))) {
|
||||||
|
// We know ast is a list
|
||||||
|
$ast = $fn->call(array_slice($ast->getData(), 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ast;
|
||||||
|
}
|
||||||
|
|
||||||
private function quasiquote($ast)
|
private function quasiquote($ast)
|
||||||
{
|
{
|
||||||
if ($ast instanceof MList) {
|
if ($ast instanceof MList) {
|
||||||
|
@ -7,11 +7,13 @@ abstract class Func
|
|||||||
{
|
{
|
||||||
protected Closure $closure;
|
protected Closure $closure;
|
||||||
protected ?string $doc;
|
protected ?string $doc;
|
||||||
|
protected bool $macro;
|
||||||
|
|
||||||
public function __construct(Closure $closure, ?string $doc = null)
|
public function __construct(Closure $closure, ?string $doc = null, bool $macro = false)
|
||||||
{
|
{
|
||||||
$this->closure = $closure;
|
$this->closure = $closure;
|
||||||
$this->doc = $doc;
|
$this->doc = $doc;
|
||||||
|
$this->macro = $macro;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClosure(): Closure
|
public function getClosure(): Closure
|
||||||
@ -24,6 +26,11 @@ abstract class Func
|
|||||||
return $this->doc;
|
return $this->doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isMacro(): bool
|
||||||
|
{
|
||||||
|
return $this->macro;
|
||||||
|
}
|
||||||
|
|
||||||
public function setDoc(?string $val): void
|
public function setDoc(?string $val): void
|
||||||
{
|
{
|
||||||
$this->doc = $val;
|
$this->doc = $val;
|
||||||
|
@ -156,6 +156,9 @@ class Collections implements ILib
|
|||||||
|
|
||||||
$env->set('concat', new CoreFunc('concat', 'Concatenate multiple sequences together.', 1, -1,
|
$env->set('concat', new CoreFunc('concat', 'Concatenate multiple sequences together.', 1, -1,
|
||||||
function (Seq ...$args) {
|
function (Seq ...$args) {
|
||||||
|
// This is used by quasiquote, so we need to always return
|
||||||
|
// a list for it to work properly.
|
||||||
|
|
||||||
$data = array_map(fn ($a) => $a->getData(), $args);
|
$data = array_map(fn ($a) => $a->getData(), $args);
|
||||||
return new MList(array_merge(...$data));
|
return new MList(array_merge(...$data));
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,8 @@ class Core implements ILib
|
|||||||
} elseif ($attribute == 'body') {
|
} elseif ($attribute == 'body') {
|
||||||
return $obj->getAst();
|
return $obj->getAst();
|
||||||
} elseif ($attribute == 'code') {
|
} elseif ($attribute == 'code') {
|
||||||
return new MList([new Symbol('fn'), $obj->getBindings(), $obj->getAst()]);
|
$name = $obj->isMacro() ? 'macro' : 'fn';
|
||||||
|
return new MList([new Symbol($name), $obj->getBindings(), $obj->getAst()]);
|
||||||
} else {
|
} else {
|
||||||
throw new MadLispException('unknown attribute for meta');
|
throw new MadLispException('unknown attribute for meta');
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,11 @@ class Printer
|
|||||||
private function doPrint($a, bool $readable): string
|
private function doPrint($a, bool $readable): string
|
||||||
{
|
{
|
||||||
if ($a instanceof Func) {
|
if ($a instanceof Func) {
|
||||||
return '<function>';
|
if ($a->isMacro()) {
|
||||||
|
return '<macro>';
|
||||||
|
} else {
|
||||||
|
return '<function>';
|
||||||
|
}
|
||||||
} elseif ($a instanceof MList) {
|
} elseif ($a instanceof MList) {
|
||||||
return '(' . implode(' ', array_map(fn ($b) => $this->doPrint($b, $readable), $a->getData())) . ')';
|
return '(' . implode(' ', array_map(fn ($b) => $this->doPrint($b, $readable), $a->getData())) . ')';
|
||||||
} elseif ($a instanceof Vector) {
|
} elseif ($a instanceof Vector) {
|
||||||
|
@ -9,9 +9,9 @@ class UserFunc extends Func
|
|||||||
protected Env $tempEnv;
|
protected Env $tempEnv;
|
||||||
protected Seq $bindings;
|
protected Seq $bindings;
|
||||||
|
|
||||||
public function __construct(Closure $closure, $ast, Env $tempEnv, Seq $bindings)
|
public function __construct(Closure $closure, $ast, Env $tempEnv, Seq $bindings, bool $macro = false)
|
||||||
{
|
{
|
||||||
parent::__construct($closure, null);
|
parent::__construct($closure, null, $macro);
|
||||||
|
|
||||||
$this->ast = $ast;
|
$this->ast = $ast;
|
||||||
$this->tempEnv = $tempEnv;
|
$this->tempEnv = $tempEnv;
|
||||||
|
Loading…
Reference in New Issue
Block a user