Skip to content

Instantly share code, notes, and snippets.

@ircmaxell
Last active August 29, 2015 14:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ircmaxell/6252ae24340edf5ba757 to your computer and use it in GitHub Desktop.
Save ircmaxell/6252ae24340edf5ba757 to your computer and use it in GitHub Desktop.
A simple functional experimental language in PHP
<?php
$code = "
= print (-> #text (
echo (chr text)
print
))
= 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