Last active
August 29, 2015 14:14
-
-
Save ircmaxell/6252ae24340edf5ba757 to your computer and use it in GitHub Desktop.
A simple functional experimental language in PHP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$code = " | |
= print (-> #text ( | |
echo (chr text) | |
)) | |
= loop (-> #cb (-> #times ( | |
= #return (cb times) | |
coalesce (if times (loop cb (- times 1))) return | |
))) | |
= #text 72 | |
loop (-> #number (echo number (chr 10))) 100 | |
print text (+ 1 text) 10 | |
echo (chr text) (chr (+ 1 text)) (chr 10) | |
if 0 (print 45) | |
"; | |
execute($code); | |
function execute($code) { | |
$tokens = tokenize($code); | |
$ast = parse($tokens); | |
$ops = compile($ast); | |
$scope = new Scope; | |
registerInitialOperators($scope); | |
executeLine($ops, $scope); | |
} | |
function executeLine(Ast $op, Scope $scope) { | |
if ($op instanceof AstLiteral) { | |
return $scope->resolve($op->value); | |
} | |
if (empty($op->ops)) { | |
return; | |
} | |
if ($op instanceof AstList) { | |
$ret = null; | |
foreach ($op->ops as $subop) { | |
$ret = executeLine($subop, $scope); | |
} | |
return $ret; | |
} | |
// function call | |
$ops = $op->ops; | |
while (count($ops) > 1) { | |
$func = $funcdef = array_shift($ops); | |
$last = $func; | |
while ($func && !is_callable($func)) { | |
$func = $scope->resolve($func); | |
if ($last === $func) { | |
throw new \RuntimeException("Attempting to call a non-callable function $func"); | |
} | |
$last = $func; | |
} | |
if (!is_callable($func)) { | |
throw new \RuntimeException("Function is not callable"); | |
} | |
$ops[0] = $func($ops[0], new Scope($scope)); | |
} | |
return $ops[0]; | |
} | |
function registerInitialOperators(Scope $scope) { | |
$scope->implement("echo", function($arg, Scope $scope) { | |
echo $scope->resolve($arg); | |
return new AstLiteral("echo"); | |
}); | |
$scope->implement("=", function($arg, Scope $scope) { | |
$name = $scope->resolve($arg); | |
return function($value, Scope $scope) use ($name) { | |
if ($scope->has($name) || $scope->parentHas($name)) { | |
throw new \Exception("Cannot mutate variables: $name"); | |
} | |
$value = executeLine($value, $scope); | |
$scope->writeToParent($name, $value); | |
return $value; | |
}; | |
}); | |
$scope->implement("chr", function($value, Scope $scope) { | |
return chr($scope->resolve($value)); | |
}); | |
$scope->implement("+", function($value, Scope $scope) { | |
$left = $scope->resolve($value); | |
return function($value, Scope $scope) use ($left) { | |
return (string) ($left + $scope->resolve($value)); | |
}; | |
}); | |
$scope->implement("-", function($value, Scope $scope) { | |
$left = $scope->resolve($value); | |
return function($value, Scope $scope) use ($left) { | |
$right = $scope->resolve($value); | |
$result = ($left - $right); | |
return (string) $result; | |
}; | |
}); | |
$scope->implement("->", function($arg, Scope $scope) { | |
$argName = $scope->resolve($arg); | |
$closureScope = $scope; | |
return function($body, Scope $scope) use ($argName, $closureScope) { | |
return function($arg, Scope $scope) use ($body, $argName, $closureScope) { | |
$new = new Scope($closureScope); | |
// the argument comes from the caller, not the closure... | |
$argValue = $scope->resolve($arg); | |
$new->write($argName, $argValue); | |
$retval = executeLine($body, $new); | |
return $retval; | |
}; | |
}; | |
}); | |
$scope->implement("if", function($cond, Scope $scope) { | |
$condition = $scope->resolve($cond); | |
return function($body, Scope $scope) use ($condition) { | |
if ($condition) { | |
return executeLine($body, $scope); | |
} | |
}; | |
}); | |
$scope->implement("coalesce", function($a, Scope $scope) { | |
$a = $scope->resolve($a); | |
return function($b, Scope $scope) use ($a) { | |
if ($a) { | |
return $a; | |
} | |
return $scope->resolve($b); | |
}; | |
}); | |
} | |
class Scope { | |
protected $data = []; | |
protected $parent = null; | |
public function __construct(Scope $parent = null) { | |
$this->parent = $parent; | |
} | |
public function has($name) { | |
$name = $this->decode($name); | |
if (isset($this->data[$name])) { | |
return true; | |
} | |
return false; | |
} | |
public function parentHas($name) { | |
if ($this->parent) { | |
return $this->parent->has($name); | |
} | |
return false; | |
} | |
public function resolve($name) { | |
if ($name === null) { | |
return null; | |
} | |
$name = $this->decode($name); | |
if ($name instanceof Closure) { | |
return $name; | |
} | |
if ($name[0] == '#') { | |
return substr($name, 1); | |
} | |
return $this->lookup($name); | |
} | |
public function decode($name) { | |
if ($name === "" || $name === null) { | |
throw new \RuntimeException("Cannot resolve empty name"); | |
} | |
if ($name instanceof AstLiteral) { | |
$name = $name->value; | |
} elseif ($name instanceof Ast) { | |
$name = $this->resolve(executeLine($name, $this)); | |
} elseif (is_callable($name)) { | |
return $name; | |
} elseif (!is_string($name)) { | |
var_dump($name); | |
throw new \LogicException("Cannot resolve non-string or literal values"); | |
} | |
return $name; | |
} | |
public function lookup($name) { | |
if (isset($this->data[$name])) { | |
return $this->data[$name]; | |
} | |
if ($this->parent) { | |
return $this->parent->lookup($name); | |
} | |
return $name; | |
} | |
public function implement($name, callable $callback) { | |
$this->data[$name] = $callback; | |
} | |
public function write($name, $value) { | |
$this->data[$name] = $value; | |
} | |
public function writeToParent($name, $value) { | |
if ($this->parent) { | |
$this->parent->write($name, $value); | |
} | |
} | |
} | |
function compile(AstList $ast) { | |
return $ast; | |
} | |
class Ast {} | |
class AstList extends Ast { | |
public $ops = []; | |
} | |
class AstExpression extends Ast { | |
public $ops = []; | |
} | |
class AstLiteral extends Ast { | |
public $value = ''; | |
public function __construct($value) { | |
$this->value = $value; | |
} | |
} | |
function parse(array $tokens, &$offset = 0) { | |
$tree = new AstList; | |
$expr = new AstExpression; | |
while ($offset < count($tokens)) { | |
switch ($tokens[$offset]->type) { | |
case Token::STRING: | |
$expr->ops[] = new AstLiteral($tokens[$offset]->value); | |
break; | |
case Token::OPEN: | |
$offset++; | |
$expr->ops[] = parse($tokens, $offset); | |
break; | |
case Token::CLOSE: | |
goto sendreturn; | |
case Token::NEWLINE: | |
if ($expr->ops) { | |
$tree->ops[] = $expr; | |
$expr = new AstExpression; | |
} | |
break; | |
} | |
$offset++; | |
} | |
sendreturn: | |
if ($tree->ops) { | |
if ($expr->ops) { | |
$tree->ops[] = $expr; | |
} | |
return $tree; | |
} | |
return $expr; | |
} | |
class Token { | |
const STRING = 1; | |
const OPEN = 2; | |
const CLOSE = 3; | |
const NEWLINE = 4; | |
public $type = 0; | |
public $value; | |
public function __construct($type, $value) { | |
$this->type = $type; | |
$this->value = $value; | |
} | |
public function __toString() { | |
$r = new ReflectionClass(self::class); | |
foreach ($r->getConstants() as $name => $value) { | |
if ($value === $this->type) { | |
return "$name({$this->value})"; | |
} | |
} | |
return ''; | |
} | |
public static function __callStatic($name, array $args) { | |
$constant = __CLASS__ . '::' . $name; | |
if (defined($constant)) { | |
if (isset($args[0])) { | |
return new Token(constant($constant), $args[0]); | |
} | |
return new Token(constant($constant), null); | |
} | |
throw new \RuntimeException("Undefined Constant Used"); | |
} | |
} | |
function tokenize($code) { | |
$i = 0; | |
$len = strlen($code); | |
$buffer = ''; | |
$tokens = []; | |
while ($i < $len) { | |
$char = $code[$i++]; | |
switch ($char) { | |
case '(': | |
if ($buffer) { | |
$tokens[] = Token::STRING($buffer); | |
$buffer = ''; | |
} | |
$tokens[] = Token::OPEN(); | |
break; | |
case ')': | |
if ($buffer) { | |
$tokens[] = Token::STRING($buffer); | |
$buffer = ''; | |
} | |
$tokens[] = Token::CLOSE(); | |
break; | |
case "\n": | |
if ($buffer) { | |
$tokens[] = Token::STRING($buffer); | |
$buffer = ''; | |
} | |
$tokens[] = Token::NEWLINE(); | |
break; | |
case ' ': | |
case "\t": | |
if ($buffer !== '') { | |
$tokens[] = Token::STRING($buffer); | |
$buffer = ''; | |
} | |
break; | |
default: | |
$buffer .= $char; | |
} | |
} | |
if ($buffer) { | |
$tokens[] = Token::STRING($buffer); | |
} | |
return $tokens; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment