Permalink
Browse files

Implemented evaluator

  • Loading branch information...
0 parents commit c625a14b4e5ec90a9a4f9b3f301ba5892f6c6f0c @dahlia dahlia committed Nov 10, 2009
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2009 Hong, MinHee <http://dahlia.kr/>
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
7 Lisphp/Applicable.php
@@ -0,0 +1,7 @@
+<?php
+require_once 'Lisphp/Scope.php';
+
+interface Lisphp_Applicable {
+ function apply(Lisphp_Scope $scope, Lisphp_List $arguments);
+}
+
7 Lisphp/Form.php
@@ -0,0 +1,7 @@
+<?php
+require_once 'Lisphp/Scope.php';
+
+interface Lisphp_Form {
+ function evaluate(Lisphp_Scope $scope);
+}
+
56 Lisphp/List.php
@@ -0,0 +1,56 @@
+<?php
+require_once 'Lisphp/Form.php';
+require_once 'Lisphp/Scope.php';
+require_once 'Lisphp/Applicable.php';
+
+class Lisphp_List extends ArrayObject implements Lisphp_Form {
+ function evaluate(Lisphp_Scope $scope) {
+ $function = $this->car()->evaluate($scope);
+ $applicable = $function instanceof Lisphp_Applicable;
+ if ($invokable = version_compare(phpversion(), '5.3.0', '>=')) {
+ $invokable = method_exists($function, '__invoke');
+ }
+ if ($invokable) {
+ $parameters = array();
+ foreach ($this->cdr() as $arg) {
+ $parameters[] = $arg->evaluate($scope);
+ }
+ return call_user_func_array($function, $parameters);
+ }
+ if ($applicable) return $function->apply($scope, $this->cdr());
+ throw new InvalidApplicationException($function);
+ }
+
+ function car() {
+ return $this[0];
+ }
+
+ function cdr() {
+ return new self(array_slice($this->getArrayCopy(), 1));
+ }
+
+ function __toString() {
+ foreach ($this as $form) {
+ if ($form instanceof Lisphp_Form) {
+ $strs[] = $form->__toString();
+ } else {
+ $strs[] = '...';
+ }
+ }
+ return '(' . join(' ', $strs) . ')';
+ }
+}
+
+class InvalidApplicationException extends BadFunctionCallException {
+ public $valueToApply;
+
+ function __construct($valueToApply) {
+ $this->valueToApply = $valueToApply;
+ $type = is_object($this->valueToApply)
+ ? get_class($this->valueToApply)
+ : (is_null($this->valueToApply) ? 'nil'
+ : gettype($this->valueToApply));
+ $msg = "$type cannot be applied; see Lisphp_Applicable interface";
+ parent::__construct($msg);
+ }
+}
36 Lisphp/Literal.php
@@ -0,0 +1,36 @@
+<?php
+require_once 'Lisphp/Form.php';
+require_once 'Lisphp/Scope.php';
+
+final class Lisphp_Literal implements Lisphp_Form {
+ public $value;
+
+ function __construct($value) {
+ if (!in_array(gettype($value), array('integer', 'double', 'string'))) {
+ $msg = 'it accepts only numbers or strings';
+ throw new UnexpectedValueException($msg);
+ }
+ $this->value = $value;
+ }
+
+ function evaluate(Lisphp_Scope $scope) {
+ return $this->value;
+ }
+
+ function isInteger() {
+ return is_int($this->value);
+ }
+
+ function isReal() {
+ return is_float($this->value);
+ }
+
+ function isString() {
+ return is_string($this->value);
+ }
+
+ function __toString() {
+ return var_export($this->value, true);
+ }
+}
+
102 Lisphp/Parser.php
@@ -0,0 +1,102 @@
+<?php
+require_once 'Lisphp/List.php';
+require_once 'Lisphp/Quote.php';
+require_once 'Lisphp/Symbol.php';
+require_once 'Lisphp/Literal.php';
+
+final class Lisphp_Parser {
+ const PARENTHESES = '(){}[]';
+ const QUOTE_PREFIX = ':';
+ const WHITESPACES = " \t\n\r\f\v\0";
+ const REAL_PATTERN = '{^
+ [+-]? ((\d+ | (\d* \. \d+ | \d+ \. \d*)) e [+-]? \d+
+ | \d* \. \d+ | \d+ \. \d*)
+ }ix';
+ const INTEGER_PATTERN = '/^([+-]?)(0x([0-9a-f]+)|0([0-7]+)|[1-9]\d*|0)/i';
+ const STRING_PATTERN = '/^"([^"\\\\]|\\\\.)*"|\'([^\'\\\\]|\\\\.)\'/';
+ const STRING_ESCAPE_PATTERN = '/\\\\(([0-7]{1,3})|x([0-9A-Fa-f]{1,2})|.)/';
+ const SYMBOL_PATTERN = '{^
+ [^ \s \d () {} \[\] : +-] [^\s () {} \[\] :]*
+ | [+-] ([^ \s \d () {} \[\] :] [^ \s () {} \[\]]*)?
+ }x';
+
+ static function parseForm($form, &$offset) {
+ static $parentheses = null;
+ if (is_null($parentheses)) {
+ $_parentheses = self::PARENTHESES;
+ $parentheses = array();
+ for ($i = 0, $len = strlen($_parentheses); $i < $len; $i += 2) {
+ $parentheses[$_parentheses[$i]] = $_parentheses[$i + 1];
+ }
+ unset($_parentheses);
+ }
+ if (isset($form[0], $parentheses[$form[0]])) {
+ $end = $parentheses[$form[0]];
+ $values = array();
+ $i = 1;
+ $len = strlen($form);
+ while ($i < $len && $form[$i] != $end) {
+ if (strpos(self::WHITESPACES, $form[$i]) !== false) {
+ ++$i;
+ continue;
+ }
+ try {
+ $values[] = self::parseForm(substr($form, $i), $_offset);
+ $i += $_offset;
+ } catch (Lisphp_ParsingException $e) {
+ throw new Lisphp_ParsingException($form, $i + $e->offset);
+ }
+ }
+ if (isset($form[$i]) && $form[$i] == $end) {
+ $offset = $i + 1;
+ return new Lisphp_List($values);
+ }
+ throw new ParsingException($form, $i);
+ } else if (isset($form[0]) && $form[0] == self::QUOTE_PREFIX) {
+ $parsed = self::parseForm(substr($form, 1), $_offset);
+ $offset = $_offset + 1;
+ return new Lisphp_Quote($parsed);
+ } else if (preg_match(self::REAL_PATTERN, $form, $matches)) {
+ $offset = strlen($matches[0]);
+ return new Lisphp_Literal((float) $matches[0]);
+ } else if (preg_match(self::INTEGER_PATTERN, $form, $matches)) {
+ $offset = strlen($matches[0]);
+ $sign = $matches[1] == '-' ? -1 : 1;
+ $value = !empty($matches[3]) ? hexdec($matches[3])
+ : (!empty($matches[4]) ? octdec($matches[4]) : $matches[2]);
+ return new Lisphp_Literal($sign * $value);
+ } else if (preg_match(self::STRING_PATTERN, $form, $matches)) {
+ list($parsed) = $matches;
+ $offset = strlen($parsed);
+ return new Lisphp_Literal(
+ preg_replace_callback(self::STRING_ESCAPE_PATTERN,
+ array(__CLASS__, '_unescapeString'),
+ substr($parsed, 1, -1))
+ );
+ } else if (preg_match(self::SYMBOL_PATTERN, $form, $matches)) {
+ $offset = strlen($matches[0]);
+ return new Lisphp_Symbol($matches[0]);
+ } else {
+ throw new Lisphp_ParsingException($form, 0);
+ }
+ }
+
+ protected static function _unescapeString($matches) {
+ static $map = array('n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v",
+ 'f' => "\f");
+ if (!empty($matches[2])) return chr(octdec($matches[2]));
+ else if (!empty($matches[3])) return chr(hexdec($matches[3]));
+ else if (isset($map[$matches[1]])) return $map[$matches[1]];
+ else return $matches[1];
+ }
+}
+
+class Lisphp_ParsingException extends Exception {
+ public $code, $offset;
+
+ function __construct($code, $offset) {
+ $this->code = $code;
+ $this->offset = $offset;
+ }
+}
+
20 Lisphp/Quote.php
@@ -0,0 +1,20 @@
+<?php
+require_once 'Lisphp/Form.php';
+require_once 'Lisphp/Scope.php';
+
+final class Lisphp_Quote implements Lisphp_Form {
+ public $form;
+
+ function __construct(Lisphp_Form $form) {
+ $this->form = $form;
+ }
+
+ function evaluate(Lisphp_Scope $scope) {
+ return $this;
+ }
+
+ function __toString() {
+ return ':' . $this->form->__toString();
+ }
+}
+
54 Lisphp/Scope.php
@@ -0,0 +1,54 @@
+<?php
+require_once 'Lisphp/Symbol.php';
+
+final class Lisphp_Scope implements ArrayAccess {
+ public $values = array(), $superscope;
+
+ function __construct(self $superscope = null) {
+ $this->superscope = $superscope;
+ }
+
+ protected static function _symbol($symbol) {
+ if ($symbol instanceof Lisphp_Symbol) return $symbol->symbol;
+ else if (is_string($symbol)) return $symbol;
+ $type = is_object($symbol) ? get_class($symbol) : gettype($symbol);
+ throw new UnexpectedValueException("expected symbol, but $type given");
+ }
+
+ function let($symbol, $value) {
+ $this->values[self::_symbol($symbol)] = $value;
+ }
+
+ function offsetGet($symbol) {
+ $sym = self::_symbol($symbol);
+ if (array_key_exists($sym, $this->values)) return $this->values[$sym];
+ else if ($this->superscope) return $this->superscope[$sym];
+ }
+
+ function offsetExists($symbol) {
+ return true;
+ }
+
+ function offsetSet($symbol, $value) {
+ $symbol = self::_symbol($symbol);
+ $defined = false;
+ for ($scope = $this; $scope; $scope = $scope->superscope) {
+ if (!array_key_exists($symbol, $scope->values)) continue;
+ $scope->values[$symbol] = $value;
+ $defined = true;
+ break;
+ }
+ if (!$defined) {
+ $this->values[$symbol] = $value;
+ }
+ }
+
+ function offsetUnset($symbol) {
+ $symbol = self::_symbol($symbol);
+ unset($this->values[$symbol]);
+ if ($this->superscope) {
+ unset($this->superscope[$symbol]);
+ }
+ }
+}
+
23 Lisphp/Symbol.php
@@ -0,0 +1,23 @@
+<?php
+require_once 'Lisphp/Form.php';
+require_once 'Lisphp/Scope.php';
+
+final class Lisphp_Symbol implements Lisphp_Form {
+ public $symbol;
+
+ function __construct($symbol) {
+ if (!is_string($symbol)) {
+ throw new UnexpectedValueException('expected string');
+ }
+ $this->symbol = $symbol;
+ }
+
+ function evaluate(Lisphp_Scope $scope) {
+ return $scope[$this];
+ }
+
+ function __toString() {
+ return $this->symbol;
+ }
+}
+
50 Lisphp/Test/ListTest.php
@@ -0,0 +1,50 @@
+<?php
+require_once 'PHPUnit/Framework.php';
+require_once 'Lisphp/List.php';
+require_once 'Lisphp/Scope.php';
+require_once 'Lisphp/Symbol.php';
+require_once 'Lisphp/Literal.php';
+
+class Lisphp_Test_ListTest_DefineMacro implements Lisphp_Applicable {
+ function apply(Lisphp_Scope $scope, Lisphp_List $arguments) {
+ $scope[$arguments[0]] = $retval = $arguments[1]->evaluate($scope);
+ return $retval;
+ }
+}
+
+class Lisphp_Test_ListTest extends PHPUnit_Framework_TestCase {
+ function setUp() {
+ $this->list = new Lisphp_List(array(
+ new Lisphp_Symbol('define'),
+ new Lisphp_Symbol('pi'),
+ new Lisphp_Literal(3.14)
+ ));
+ }
+
+ function testInvalidApplication() {
+ $this->setExpectedException('InvalidApplicationException');
+ $this->list->evaluate(new Lisphp_Scope);
+ }
+
+ function testEvaluate() {
+ $scope = new Lisphp_Scope;
+ $scope['define'] = new Lisphp_Test_ListTest_DefineMacro;
+ $this->assertEquals(3.14, $this->list->evaluate($scope));
+ $this->assertEquals(3.14, $scope['pi']);
+ }
+
+ function testCar() {
+ $this->assertSame($this->list[0], $this->list->car());
+ }
+
+ function testCdr() {
+ $this->assertEquals(new Lisphp_List(array(new Lisphp_Symbol('pi'),
+ new Lisphp_Literal(3.14))),
+ $this->list->cdr());
+ }
+
+ function testToString() {
+ $this->assertEquals('(define pi 3.14)', $this->list->__toString());
+ }
+}
+
35 Lisphp/Test/LiteralTest.php
@@ -0,0 +1,35 @@
+<?php
+require_once 'PHPUnit/Framework.php';
+require_once 'Lisphp/Literal.php';
+require_once 'Lisphp/Scope.php';
+
+class Lisphp_Test_LiteralTest extends PHPUnit_Framework_TestCase {
+ static $values = array('integer' => 123, 'real' => 3.14, 'string' => 'abc');
+
+ function testUnexpectedValue() {
+ $this->setExpectedException('UnexpectedValueException');
+ new Lisphp_Literal(new stdClass);
+ }
+
+ function testValue() {
+ foreach (self::$values as $_ => $value) {
+ $literal = new Lisphp_Literal($value);
+ $this->assertEquals($value, $literal->value);
+ }
+ }
+
+ function testEvaluate() {
+ foreach (self::$values as $_ => $value) {
+ $literal = new Lisphp_Literal($value);
+ $this->assertEquals($value, $literal->evaluate(new Lisphp_Scope));
+ }
+ }
+
+ function testPredicate() {
+ foreach (self::$values as $type => $value) {
+ $literal = new Lisphp_Literal($value);
+ $this->assertTrue($literal->{"is$type"}());
+ }
+ }
+}
+
91 Lisphp/Test/ParserTest.php
@@ -0,0 +1,91 @@
+<?php
+require_once 'PHPUnit/Framework.php';
+require_once 'Lisphp/Parser.php';
+require_once 'Lisphp/Symbol.php';
+require_once 'Lisphp/Literal.php';
+
+class Lisphp_Test_ParserTest extends PHPUnit_Framework_TestCase {
+ function assertForm($value, $offset, $expression) {
+ $actual = Lisphp_Parser::parseForm($expression, $pos);
+ $this->assertEquals($value, $actual);
+ $this->assertEquals($offset, $pos);
+ }
+
+ function testParseForm_list() {
+ $expected = new Lisphp_List(array(
+ new Lisphp_Symbol('define'),
+ new Lisphp_Symbol('add'),
+ new Lisphp_List(array(
+ new Lisphp_Symbol('lambda'),
+ new Lisphp_List(array(
+ new Lisphp_Symbol('a'),
+ new Lisphp_Symbol('b')
+ )),
+ new Lisphp_List(array(
+ new Lisphp_Symbol('+'),
+ new Lisphp_Symbol('a'),
+ new Lisphp_Symbol('b')
+ ))
+ ))
+ ));
+ $this->assertForm($expected, 35,
+ '(define add {lambda [a b] (+ a b)})');
+ try {
+ Lisphp_Parser::parseForm('(abc d ])', $offset);
+ $this->fails();
+ } catch(Lisphp_ParsingException $e) {
+ $this->assertEquals('(abc d ])', $e->code);
+ $this->assertEquals(7, $e->offset);
+ }
+ }
+
+ function testParseForm_quote() {
+ $this->assertForm(new Lisphp_Quote(new Lisphp_Symbol('abc')), 4,
+ ':abc');
+ $this->assertForm(new Lisphp_Quote(new Lisphp_List(array(
+ new Lisphp_Symbol('add'),
+ new Lisphp_Literal(2),
+ new Lisphp_Literal(3)
+ ))),
+ 10,
+ ':(add 2 3)');
+ }
+
+ function testParseForm_integer() {
+ $this->assertForm(new Lisphp_Literal(123), 3, '123');
+ $this->assertForm(new Lisphp_Literal(123), 4, '+123 ');
+ $this->assertForm(new Lisphp_Literal(-123), 4, '-123');
+ $this->assertForm(new Lisphp_Literal(0xff), 4, '0xff');
+ $this->assertForm(new Lisphp_Literal(0xff), 5, '+0XFF');
+ $this->assertForm(new Lisphp_Literal(-0xff), 5, '-0xFf');
+ $this->assertForm(new Lisphp_Literal(0765), 4, '0765');
+ $this->assertForm(new Lisphp_Literal(0765), 5, '+0765');
+ $this->assertForm(new Lisphp_Literal(-0765), 5, '-0765');
+ }
+
+ function testParseForm_real() {
+ $this->assertForm(new Lisphp_Literal(1.234), 5, '1.234');
+ $this->assertForm(new Lisphp_Literal(1.23), 5, '+1.23');
+ $this->assertForm(new Lisphp_Literal(-1.23), 5, '-1.23');
+ $this->assertForm(new Lisphp_Literal(.1234), 5, '.1234');
+ $this->assertForm(new Lisphp_Literal(.123), 5, '+.123');
+ $this->assertForm(new Lisphp_Literal(-.123), 5, '-.123');
+ $this->assertForm(new Lisphp_Literal(1.2e3), 5, '1.2e3');
+ $this->assertForm(new Lisphp_Literal(1.2e3), 6, '+1.2e3');
+ $this->assertForm(new Lisphp_Literal(-1.2e3), 6, '-1.2e3');
+ }
+
+ function testParseForm_string() {
+ $this->assertForm(new Lisphp_Literal("abcd efg \"q1\"\n\t'q2'"),
+ 27,
+ '"abcd efg \\"q1\\"\n\\t\\\'q2\\\'"');
+ }
+
+ function testParseForm_symbol() {
+ $this->assertForm(new Lisphp_Symbol('abc'), 3, 'abc');
+ $this->assertForm(new Lisphp_Symbol('-abcd'), 5, '-abcd ');
+ $this->assertForm(new Lisphp_Symbol('-'), 1, '-');
+ $this->assertForm(new Lisphp_Symbol('+'), 1, '+');
+ }
+}
+
23 Lisphp/Test/QuoteTest.php
@@ -0,0 +1,23 @@
+<?php
+require_once 'PHPUnit/Framework.php';
+require_once 'Lisphp/Quote.php';
+require_once 'Lisphp/Scope.php';
+
+class Lisphp_Test_QuoteTest extends PHPUnit_Framework_TestCase {
+ function testEvaluate() {
+ $quote = new Lisphp_Quote(new Lisphp_Symbol('abc'));
+ $this->assertEquals($quote, $quote->evaluate(new Lisphp_Scope));
+ }
+
+ function testToString() {
+ $quote = new Lisphp_Quote(new Lisphp_Symbol('abc'));
+ $this->assertEquals(':abc', $quote->__toString());
+ $quote = new Lisphp_Quote(new Lisphp_List(array(
+ new Lisphp_Symbol('define'),
+ new Lisphp_Symbol('pi'),
+ new Lisphp_Literal(3.14)
+ )));
+ $this->assertEquals(':(define pi 3.14)', $quote->__toString());
+ }
+}
+
57 Lisphp/Test/ScopeTest.php
@@ -0,0 +1,57 @@
+<?php
+require_once 'PHPUnit/Framework.php';
+require_once 'Lisphp/Scope.php';
+require_once 'Lisphp/Symbol.php';
+
+class Lisphp_Test_ScopeTest extends PHPUnit_Framework_TestCase {
+ function setUp() {
+ $this->scope = new Lisphp_Scope;
+ $this->scope['abc'] = 1;
+ $this->scope['def'] = true;
+ $this->scope[new Lisphp_Symbol('ghi')] = null;
+ }
+
+ function testGet() {
+ $this->assertEquals(1, $this->scope['abc']);
+ $this->assertEquals(true, $this->scope[new Lisphp_Symbol('def')]);
+ $this->assertNull($this->scope['ghi']);
+ $this->assertNull($this->scope['x']);
+ }
+
+ function testExists() {
+ $this->assertTrue(isset($this->scope['abc']));
+ $this->assertTrue(isset($this->scope['x']));
+ }
+
+ function testUnset() {
+ unset($this->scope['abc']);
+ $this->assertNull($this->scope['abc']);
+ }
+
+ function testSuperscope() {
+ $scope = new Lisphp_Scope($this->scope);
+ $this->assertSame($this->scope, $scope->superscope);
+ $this->assertEquals(1, $scope['abc']);
+ $this->assertNull($scope['x']);
+ $this->scope['abc'] = 2;
+ $this->assertEquals(2, $this->scope['abc']);
+ $this->assertEquals(2, $scope['abc']);
+ $scope['abc'] = 3;
+ $this->assertEquals(3, $this->scope['abc']);
+ $this->assertEquals(3, $scope['abc']);
+ $scope['abc'] = null;
+ $this->assertNull($scope['abc']);
+ $this->assertNull($this->scope['abc']);
+ $scope['def'] = false;
+ unset($scope['def']);
+ $this->assertNull($scope['def']);
+ $this->assertNull($this->scope['def']);
+ }
+
+ function testLet() {
+ $scope = new Lisphp_Scope($this->scope);
+ $scope->let('abc', 'overridden');
+ $this->assertEquals('overridden', $scope['abc']);
+ }
+}
+
26 Lisphp/Test/SymbolTest.php
@@ -0,0 +1,26 @@
+<?php
+require_once 'PHPUnit/Framework.php';
+require_once 'Lisphp/Symbol.php';
+require_once 'Lisphp/Scope.php';
+
+class Lisphp_Test_SymbolTest extends PHPUnit_Framework_TestCase {
+ function testUnexpectedValue() {
+ $this->setExpectedException('UnexpectedValueException');
+ new Lisphp_Symbol(123);
+ }
+
+ function testEvaluate() {
+ $scope = new Lisphp_Scope;
+ $scope['abc'] = 123;
+ $symbol = new Lisphp_Symbol('abc');
+ $this->assertEquals(123, $symbol->evaluate($scope));
+ $symbol = new Lisphp_Symbol('def');
+ $this->assertNull($symbol->evaluate($scope));
+ }
+
+ function testToString() {
+ $symbol = new Lisphp_Symbol('abc');
+ $this->assertEquals('abc', $symbol->__toString());
+ }
+}
+

0 comments on commit c625a14

Please sign in to comment.