Permalink
Browse files

Initial interpreter implementation

  • Loading branch information...
0 parents commit 69b6064b98a5f254cdc45de68cb0b6546a3dee2e @rspivak committed Dec 12, 2010
12 .gitignore
@@ -0,0 +1,12 @@
+*.pyc
+*.pyo
+.installed.cfg
+bin
+build
+develop-eggs
+dist
+downloads
+eggs
+parts
+src/*.egg-info
+var
131 README.md
@@ -0,0 +1,131 @@
+Serval - Simple Scheme interpreter in Python
+============================================
+
+Overview
+---------
+
+**Serval** is a high-level Scheme interpreter written in Python.
+It implements a subset of R5RS. The code closely follows
+Scheme meta-circular evaluator implementation from Ch.4 of the SICP book.
+
+
+The goal of the project
+------------------------
+
+1. Self-education (I've been always fascinated by language
+ design and implementation, it was about time I started learning about
+ interpreters and compilers by actually writing something).
+
+2. To serve as a potential example for other people
+ interested in interpreter implementation, particularly
+ Scheme interpreter.
+
+3. Not a goal per se, but I wanted the **Serval** to be able
+ to run all examples from "The Little Schemer" book (that was
+ my test target). The project has a test module test_the_little_schemer.py
+ which runs simple meta-circular evaluator from Ch.10 of the book.
+
+This is a current high-level overview of the interpreter:
+
+ +----------------+
+ | Scheme |
+ | source code |
+ | |
+ +-------+--------+
+ |
+ |
+ \|/
+ +----------------+
+ | High-Level |
+ | Interpreter |
+ | |
+ +-------+--------+
+ |
+ |
+ \|/
+
+ Output
+
+
+Session examples (REPL)
+----------------------
+
+1. Defining simple function
+
+ serval> (define add1 (lambda (x) (+ x 1)))
+ ok
+ serval> (add1 9)
+ 10
+
+2. Loading representation from a file
+
+ serval> (load "/home/alienoid/scheme/the_little_schemer/ch10.ss")
+ serval> (value '((lambda (x) (cons x x)) 5))
+ (5 . 5)
+
+
+Installation
+------------
+
+1. Using `buildout` (useful for local development and testing)
+
+ $ cd serval
+ $ python bootstrap.py
+ $ bin/buildout
+
+ Run the interpreter's REPL
+
+ $ bin/serval
+
+2. Using `easy_install` or `pip`
+
+ XXX
+
+
+Technical details
+-----------------
+
+**Serval** has a lexer which uses regular expressions to get a next token
+and a recursive-descent parser implementation.
+
+Some code parts are not *idiomatic* Python because I tried to follow SICP
+implementation as close as possible for this interpeter.
+
+There is no multiline editing support for the moment. If you need one,
+you'd be better off by saving the code into a file and loading it with
+`load` builtin function provided by REPL.
+
+Development
+-----------
+
+Install 'enscript' utility (optional).
+If you are on Ubuntu::
+
+ $ sudo apt-get install enscript
+
+Boostrap the buildout and run it:
+
+ $ cd serval
+ $ python bootstrap.py
+ $ bin/buildout
+
+Run tests, test coverage and produce coverage reports::
+
+ $ bin/test
+ $ bin/coverage-test
+ $ bin/coveragereport
+
+ Check ./var/report/serval.html out for coverage results.
+
+Run pep8 and pylint to check code style and search for potential bugs:
+
+ $ bin/pep8
+ $ bin/pylint
+
+
+Roadmap
+-------
+
+1. Scheme translator to high-level assembly language
+2. Bytecode assembler
+3. Register based bytecode interpretator (VM)
260 bootstrap.py
@@ -0,0 +1,260 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+ def quote(c):
+ if ' ' in c:
+ return '"%s"' % c # work around spawn lamosity on windows
+ else:
+ return c
+else:
+ quote = str
+
+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
+stdout, stderr = subprocess.Popen(
+ [sys.executable, '-Sc',
+ 'try:\n'
+ ' import ConfigParser\n'
+ 'except ImportError:\n'
+ ' print 1\n'
+ 'else:\n'
+ ' print 0\n'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+has_broken_dash_S = bool(int(stdout.strip()))
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded. This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient. However, we'll start with that:
+if not has_broken_dash_S and 'site' in sys.modules:
+ # We will restart with python -S.
+ args = sys.argv[:]
+ args[0:0] = [sys.executable, '-S']
+ args = map(quote, args)
+ os.execv(sys.executable, args)
+# Now we are running with -S. We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in sys.modules.items():
+ if k in ('setuptools', 'pkg_resources') or (
+ hasattr(v, '__path__') and
+ len(v.__path__)==1 and
+ not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+ # This is a namespace package. Remove it.
+ sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+ if value:
+ if '://' not in value: # It doesn't smell like a URL.
+ value = 'file://%s' % (
+ urllib.pathname2url(
+ os.path.abspath(os.path.expanduser(value))),)
+ if opt_str == '--download-base' and not value.endswith('/'):
+ # Download base needs a trailing slash to make the world happy.
+ value += '/'
+ else:
+ value = None
+ name = opt_str[2:].replace('-', '_')
+ setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+ help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+ action="store_true", dest="use_distribute", default=False,
+ help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+ callback=normalize_to_url, nargs=1, type="string",
+ help=("Specify a URL or file location for the setup file. "
+ "If you use Setuptools, this will default to " +
+ setuptools_source + "; if you use Distribute, this "
+ "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+ callback=normalize_to_url, nargs=1, type="string",
+ help=("Specify a URL or directory for downloading "
+ "zc.buildout and either Setuptools or Distribute. "
+ "Defaults to PyPI."))
+parser.add_option("--eggs",
+ help=("Specify a directory for storing eggs. Defaults to "
+ "a temporary directory that is deleted when the "
+ "bootstrap script completes."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+ dest='accept_buildout_test_releases',
+ action="store_true", default=False,
+ help=("Normally, if you do not specify a --version, the "
+ "bootstrap script and buildout gets the newest "
+ "*final* versions of zc.buildout and its recipes and "
+ "extensions for you. If you use this flag, "
+ "bootstrap and buildout will get the newest releases "
+ "even if they are alphas or betas."))
+parser.add_option("-c", None, action="store", dest="config_file",
+ help=("Specify the path to the buildout configuration "
+ "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+ args += ['-c', options.config_file]
+
+if options.eggs:
+ eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+ eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+ if options.use_distribute:
+ options.setup_source = distribute_source
+ else:
+ options.setup_source = setuptools_source
+
+if options.accept_buildout_test_releases:
+ args.append('buildout:accept-buildout-test-releases=true')
+args.append('bootstrap')
+
+try:
+ import pkg_resources
+ import setuptools # A flag. Sometimes pkg_resources is installed alone.
+ if not hasattr(pkg_resources, '_distribute'):
+ raise ImportError
+except ImportError:
+ ez_code = urllib2.urlopen(
+ options.setup_source).read().replace('\r\n', '\n')
+ ez = {}
+ exec ez_code in ez
+ setup_args = dict(to_dir=eggs_dir, download_delay=0)
+ if options.download_base:
+ setup_args['download_base'] = options.download_base
+ if options.use_distribute:
+ setup_args['no_fake'] = True
+ ez['use_setuptools'](**setup_args)
+ if 'pkg_resources' in sys.modules:
+ reload(sys.modules['pkg_resources'])
+ import pkg_resources
+ # This does not (always?) update the default working set. We will
+ # do it.
+ for path in sys.path:
+ if path not in pkg_resources.working_set.entries:
+ pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+ '-c',
+ quote('from setuptools.command.easy_install import main; main()'),
+ '-mqNxd',
+ quote(eggs_dir)]
+
+if not has_broken_dash_S:
+ cmd.insert(1, '-S')
+
+find_links = options.download_base
+if not find_links:
+ find_links = os.environ.get('bootstrap-testing-find-links')
+if find_links:
+ cmd.extend(['-f', quote(find_links)])
+
+if options.use_distribute:
+ setup_requirement = 'distribute'
+else:
+ setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+setup_requirement_path = ws.find(
+ pkg_resources.Requirement.parse(setup_requirement)).location
+env = dict(
+ os.environ,
+ PYTHONPATH=setup_requirement_path)
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+ # Figure out the most recent final version of zc.buildout.
+ import setuptools.package_index
+ _final_parts = '*final-', '*final'
+ def _final_version(parsed_version):
+ for part in parsed_version:
+ if (part[:1] == '*') and (part not in _final_parts):
+ return False
+ return True
+ index = setuptools.package_index.PackageIndex(
+ search_path=[setup_requirement_path])
+ if find_links:
+ index.add_find_links((find_links,))
+ req = pkg_resources.Requirement.parse(requirement)
+ if index.obtain(req) is not None:
+ best = []
+ bestv = None
+ for dist in index[req.project_name]:
+ distv = dist.parsed_version
+ if _final_version(distv):
+ if bestv is None or distv > bestv:
+ best = [dist]
+ bestv = distv
+ elif distv == bestv:
+ best.append(dist)
+ if best:
+ best.sort()
+ version = best[-1].version
+if version:
+ requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+if is_jython:
+ import subprocess
+ exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+ exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+ sys.stdout.flush()
+ sys.stderr.flush()
+ print ("An error occurred when trying to install zc.buildout. "
+ "Look above this message for any errors that "
+ "were output by easy_install.")
+ sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+ shutil.rmtree(eggs_dir)
87 buildout.cfg
@@ -0,0 +1,87 @@
+[buildout]
+extends = versions.cfg
+parts =
+ app
+ test
+ coverage-test
+ coverage-report
+ pep8-bin
+ pep8
+ pylint-bin
+ pylint
+develop =
+ .
+eggs =
+ serval
+allow-picked-versions = false
+versions = versions
+
+[app]
+recipe = zc.recipe.egg:scripts
+eggs =
+ ${buildout:eggs}
+
+[var]
+recipe = plone.recipe.command
+directory = ${buildout:directory}/var
+command = mkdir ${:directory}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = ${buildout:eggs}
+defaults = ['--tests-pattern', '^f?tests$', '-v']
+
+[coverage-test]
+recipe = zc.recipe.testrunner
+eggs = ${test:eggs}
+defaults = ['--coverage', '${var:directory}/coverage', '-v', '--auto-progress']
+
+[coverage-report]
+recipe = zc.recipe.egg
+eggs = z3c.coverage
+scripts = coveragereport
+arguments = ('${var:directory}/coverage', '${var:directory}/report')
+
+[pep8-bin]
+recipe = zc.recipe.egg
+eggs =
+ pep8
+entry-points = pep8-bin=pep8:_main
+
+[pep8]
+recipe = collective.recipe.template
+input = inline:
+ #!/bin/bash
+ find -L src -type f -regex ".*\.py" | xargs bin/pep8-bin
+
+output = ${buildout:directory}/bin/pep8
+mode = 755
+
+# :F0401: Unable to import %r (%s). Pylint has been unable to import a module.
+# :C0111: *Missing docstring*
+# :C0103: *Invalid name "%s" (should match %s)*
+# :W0142: *Used * or ** magic*
+# :W0232: *Class has no __init__ method*
+# :E1101: *%s %r has no %r member*
+# :R0911: Too many return statements (%s/%s)
+[pylint-bin]
+recipe = zc.recipe.egg
+eggs = pylint
+entry-points = pylint-bin=pylint.lint:Run
+arguments = [
+ '--output-format=parseable',
+ '--reports=y',
+ '--include-ids=y',
+ '--disable=F0401,C0111,C0103,W0142,W0232,E1101,R0911',
+ '--generated-members=objects',
+ '--min-public-methods=0',
+ '--max-public-methods=30',
+ ] + sys.argv[1:]
+
+[pylint]
+recipe = collective.recipe.template
+input = inline:
+ #!/bin/bash
+ find -L src -type f -regex ".*\.py" | xargs bin/pylint-bin $@
+output = ${buildout:directory}/bin/pylint
+mode = 755
19 setup.py
@@ -0,0 +1,19 @@
+from setuptools import setup, find_packages
+
+setup(
+ name='serval',
+ version='0.1',
+ url='http://github.com/rspivak/serval',
+ license='MIT',
+ description='Simple Scheme interpreter in Python',
+ author='Ruslan Spivak',
+ author_email='ruslan.spivak@gmail.com',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ install_requires=['setuptools'],
+ zip_safe=False,
+ entry_points="""\
+ [console_scripts]
+ serval = serval.interpreter:main
+ """
+ )
0 src/serval/__init__.py
No changes.
144 src/serval/builtin.py
@@ -0,0 +1,144 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+import operator
+import functools
+
+from serval.model import Number, Boolean, Pair, EmptyList
+from serval.expression.util import cdr
+
+
+def _perform_arithmetic_function(func, *args):
+ return Number(reduce(func, [int(arg.val) for arg in args]))
+
+builtin_add = functools.partial(_perform_arithmetic_function, operator.add)
+builtin_sub = functools.partial(_perform_arithmetic_function, operator.sub)
+builtin_mul = functools.partial(_perform_arithmetic_function, operator.mul)
+builtin_div = functools.partial(_perform_arithmetic_function, operator.div)
+
+def _perform_comparison(func, *args):
+ if len(args) == 1:
+ return Boolean(True)
+
+ prev, rest = args[0], args[1:]
+ for arg in rest:
+ if not func(prev, arg):
+ return Boolean(False)
+ prev = arg
+
+ return Boolean(True)
+
+builtin_eq = functools.partial(_perform_comparison, operator.eq)
+builtin_lt = functools.partial(_perform_comparison, operator.lt)
+builtin_le = functools.partial(_perform_comparison, operator.le)
+builtin_gt = functools.partial(_perform_comparison, operator.gt)
+builtin_ge = functools.partial(_perform_comparison, operator.ge)
+
+def builtin_pair_p(*args):
+ arg = args[0]
+ return Boolean(isinstance(arg, Pair))
+
+def builtin_null_p(*args):
+ return Boolean(args[0] is EmptyList)
+
+def builtin_cons(*args):
+ first, second = args
+ return Pair(first, second)
+
+def builtin_car(*args):
+ arg = args[0]
+ return arg.head
+
+def builtin_cdr(*args):
+ arg = args[0]
+ return arg.tail
+
+def builtin_list(*args):
+ def inner(args):
+ if not args:
+ return EmptyList
+ return Pair(args[0], inner(args[1:]))
+
+ return inner(args)
+
+def builtin_abs(*args):
+ return Number(abs(args[0].val))
+
+def builtin_not(*args):
+ return Boolean(not bool(args[0]))
+
+def builtin_eq_p(*args):
+ first, second = args
+ return Boolean(first == second)
+
+def builtin_zero_p(*args):
+ return Boolean(args[0] == Number(0))
+
+def builtin_number_p(*args):
+ return Boolean(isinstance(args[0], Number))
+
+def builtin_expt(*args):
+ first, second = args
+ return Number(first.val ** second.val)
+
+def builtin_length(*args):
+ alist = args[0]
+
+ def inner(arg, count):
+ if arg is EmptyList:
+ return count
+ return inner(cdr(arg), count + 1)
+
+ return inner(alist, 0)
+
+def builtin_even_p(*args):
+ return Boolean(args[0].val % 2 == 0)
+
+BUILTIN_PROCEDURES = [
+ ('pair?', builtin_pair_p),
+ ('eq?', builtin_eq_p),
+ ('cons', builtin_cons),
+ ('car', builtin_car),
+ ('cdr', builtin_cdr),
+ ('list', builtin_list),
+ ('abs', builtin_abs),
+ ('null?', builtin_null_p),
+ ('not', builtin_not),
+ ('zero?', builtin_zero_p),
+ ('number?', builtin_number_p),
+ ('expt', builtin_expt),
+ ('length', builtin_length),
+ ('even?', builtin_even_p),
+ ('+', builtin_add),
+ ('-', builtin_sub),
+ ('*', builtin_mul),
+ ('/', builtin_div),
+ ('=', builtin_eq),
+ ('<', builtin_lt),
+ ('<=', builtin_le),
+ ('>', builtin_gt),
+ ('>=', builtin_ge),
+ ]
0 src/serval/expression/__init__.py
No changes.
38 src/serval/expression/assignment.py
@@ -0,0 +1,38 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol
+from serval.expression.util import is_tagged_list, cadr, caddr
+
+
+def is_assignment(expr):
+ return is_tagged_list(expr, Symbol('set!'))
+
+def assignment_variable(expr):
+ return cadr(expr)
+
+def assignment_value(expr):
+ return caddr(expr)
52 src/serval/expression/binding.py
@@ -0,0 +1,52 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol, EmptyList
+from serval.expression.util import (
+ is_tagged_list, cons, cdr, cadr, cddr, caar, cadar)
+
+
+def is_let_binding(expr):
+ return is_tagged_list(expr, Symbol('let'))
+
+def binding_variables(expr):
+ def inner(expr):
+ if expr is EmptyList:
+ return EmptyList
+ return cons(caar(expr), inner(cdr(expr)))
+
+ return inner(cadr(expr))
+
+def binding_values(expr):
+ def inner(expr):
+ if expr is EmptyList:
+ return EmptyList
+ return cons(cadar(expr), inner(cdr(expr)))
+
+ return inner(cadr(expr))
+
+def binding_body(expr):
+ return cddr(expr)
90 src/serval/expression/conditional.py
@@ -0,0 +1,90 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol, Boolean, EmptyList
+from serval.expression.sequence import sequence_exp
+from serval.expression.util import (
+ is_tagged_list, car, cdr, cadr, caddr, cdddr, cadddr, tolist)
+
+
+def is_if(expr):
+ return is_tagged_list(expr, Symbol('if'))
+
+def if_predicate(expr):
+ return cadr(expr)
+
+def if_consequent(expr):
+ return caddr(expr)
+
+def if_alternative(expr):
+ if cdddr(expr) is not EmptyList:
+ return cadddr(expr)
+
+ return False
+
+def make_if(predicate, consequent, alternative):
+ return tolist(Symbol('if'), predicate, consequent, alternative)
+
+def is_and(expr):
+ return is_tagged_list(expr, Symbol('and'))
+
+def is_or(expr):
+ return is_tagged_list(expr, Symbol('or'))
+
+def is_cond(expr):
+ return is_tagged_list(expr, Symbol('cond'))
+
+def cond_clauses(expr):
+ return cdr(expr)
+
+def is_cond_else_clause(clause):
+ return cond_predicate(clause) == Symbol('else')
+
+def cond_predicate(clause):
+ return car(clause)
+
+def cond_actions(clause):
+ return cdr(clause)
+
+def cond_to_if(expr):
+ return expand_clauses(cond_clauses(expr))
+
+def expand_clauses(clauses):
+ if clauses is EmptyList:
+ return Boolean(False)
+
+ first = car(clauses)
+ rest = cdr(clauses)
+
+ if is_cond_else_clause(first):
+ if rest is EmptyList:
+ return sequence_exp(cond_actions(first))
+ else:
+ raise ValueError("ELSE clause isn't last: %s" % clauses)
+ else:
+ return make_if(cond_predicate(first),
+ sequence_exp(cond_actions(first)), expand_clauses(rest))
+
46 src/serval/expression/definition.py
@@ -0,0 +1,46 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol
+from serval.expression.lambdaexpr import make_lambda
+from serval.expression.util import (is_tagged_list, is_symbol,
+ cadr, caadr, caddr, cdadr, cddr)
+
+
+def is_definition(expr):
+ return is_tagged_list(expr, Symbol('define'))
+
+def definition_variable(expr):
+ if is_symbol(cadr(expr)):
+ return cadr(expr)
+
+ return caadr(expr)
+
+def definition_value(expr):
+ if is_symbol(cadr(expr)):
+ return caddr(expr)
+
+ return make_lambda(cdadr(expr), cddr(expr))
42 src/serval/expression/lambdaexpr.py
@@ -0,0 +1,42 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol
+from serval.expression.util import is_tagged_list, cadr, cddr, cons
+
+
+def is_lambda(expr):
+ return is_tagged_list(expr, Symbol('lambda'))
+
+def lambda_parameters(expr):
+ return cadr(expr)
+
+def lambda_body(expr):
+ return cddr(expr)
+
+def make_lambda(params, body):
+ return cons(Symbol('lambda'), cons(params, body))
+
96 src/serval/expression/procedure.py
@@ -0,0 +1,96 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol, Pair, EmptyList
+from serval.expression.util import (
+ is_tagged_list, pair_to_list, car, cdr, cons, cadr, caddr, cadddr)
+from serval.builtin import BUILTIN_PROCEDURES
+
+
+#################################
+# Procedure application
+#################################
+def is_application(expr):
+ return isinstance(expr, Pair)
+
+def operator(expr):
+ return car(expr)
+
+def operands(expr):
+ return cdr(expr)
+
+def no_operands(ops):
+ return ops is EmptyList
+
+def first_operand(ops):
+ return car(ops)
+
+def rest_operands(ops):
+ return cdr(ops)
+
+#################################
+# Compound procedures
+#################################
+def make_procedure(params, body, env):
+ """Create a list"""
+ return cons(Symbol('procedure'),
+ cons(params, cons(body, cons(env, EmptyList))))
+
+def is_compound_procedure(expr):
+ return is_tagged_list(expr, Symbol('procedure'))
+
+def procedure_parameters(expr):
+ return cadr(expr)
+
+def procedure_body(expr):
+ return caddr(expr)
+
+def procedure_environment(expr):
+ return cadddr(expr)
+
+def get_procedure_repr(expr):
+ return '#<procedure %s %s <procedure-env>' % (
+ procedure_parameters(expr), procedure_body(expr))
+
+#################################
+# Primitive procedures
+#################################
+def is_primitive_procedure(expr):
+ return is_tagged_list(expr, Symbol('primitive'))
+
+def primitive_implementation(expr):
+ return cadr(expr)
+
+def primitive_procedure_names():
+ return [name for name, _ in BUILTIN_PROCEDURES]
+
+def primitive_procedure_values():
+ return [Pair(Symbol('primitive'), Pair(proc, EmptyList))
+ for _, proc in BUILTIN_PROCEDURES]
+
+def apply_primitive_procedure(proc, args):
+ func = primitive_implementation(proc)
+ return func(*pair_to_list(args))
35 src/serval/expression/quote.py
@@ -0,0 +1,35 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol
+from serval.expression.util import is_tagged_list, cadr
+
+
+def is_quoted(expr):
+ return is_tagged_list(expr, Symbol('quote'))
+
+def text_of_quotation(expr):
+ return cadr(expr)
34 src/serval/expression/selfeval.py
@@ -0,0 +1,34 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Number, String, Boolean, Character
+
+
+def is_self_evaluating(expr):
+ if isinstance(expr, (Number, String, Boolean, Character)):
+ return True
+
+ return False
56 src/serval/expression/sequence.py
@@ -0,0 +1,56 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.model import Symbol, EmptyList
+from serval.expression.util import is_tagged_list, car, cdr, cons
+
+
+def is_begin(expr):
+ return is_tagged_list(expr, Symbol('begin'))
+
+def begin_actions(expr):
+ return cdr(expr)
+
+def is_last_expr(seq):
+ return cdr(seq) is EmptyList
+
+def first_expr(seq):
+ return car(seq)
+
+def rest_exprs(seq):
+ return cdr(seq)
+
+def make_begin(seq):
+ return cons(Symbol('begin'), seq)
+
+def sequence_exp(seq):
+ if seq is EmptyList:
+ return seq
+
+ if is_last_expr(seq):
+ return first_expr(seq)
+
+ return make_begin(seq)
98 src/serval/expression/util.py
@@ -0,0 +1,98 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+import os
+
+from serval.lexer import Lexer
+from serval.parser import Parser
+from serval.model import Pair, Symbol, EmptyList
+
+
+def is_tagged_list(expr, tag):
+ if isinstance(expr, Pair) and car(expr) == tag:
+ return True
+
+ return False
+
+def is_symbol(expr):
+ return isinstance(expr, Symbol)
+
+def cons(head, tail):
+ return Pair(head, tail)
+
+def car(pair):
+ return pair.head
+
+def cdr(pair):
+ return pair.tail
+
+def tolist(*expressions):
+ def inner(args):
+ if not args:
+ return EmptyList
+ return cons(args[0], inner(args[1:]))
+
+ return inner(expressions)
+
+caar = lambda pair: car(car(pair))
+cadr = lambda pair: car(cdr(pair))
+caddr = lambda pair: car(cdr(cdr(pair)))
+cadar = lambda pair: car(cdr(car(pair)))
+cdddr = lambda pair: cdr(cdr(cdr(pair)))
+cadddr = lambda pair: car(cdr(cdr(cdr(pair))))
+caadr = lambda pair: car(car(cdr(pair)))
+cdadr = lambda pair: cdr(car(cdr(pair)))
+cddr = lambda pair: cdr(cdr(pair))
+
+def pair_to_list(pair):
+ result = []
+ if pair is EmptyList:
+ return result
+
+ head, tail = pair.head, pair.tail
+ result.append(head)
+ while tail is not EmptyList:
+ head, tail = tail.head, tail.tail
+ result.append(head)
+
+ return result
+
+def is_load(expr):
+ return is_tagged_list(expr, Symbol('load'))
+
+def load(interpreter, expression):
+ """Read expression and definitions from the file.
+
+ After reading the procedure evaluates expressions
+ and definitions sequentially.
+ """
+ filepath = cadr(expression).val
+ data = open(os.path.abspath(filepath)).read()
+ parser = Parser(Lexer(data))
+
+ for expr in parser.parse():
+ interpreter.interpret(expr)
+
29 src/serval/expression/variable.py
@@ -0,0 +1,29 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.expression.util import is_symbol
+
+is_variable = is_symbol
220 src/serval/interpreter.py
@@ -0,0 +1,220 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+import readline
+
+from serval.lexer import Lexer
+from serval.parser import Parser
+from serval.model import Symbol, Boolean, EmptyList
+from serval.scope import (setup_environment, define_variable,
+ lookup_variable_value, extend_environment)
+from serval.expression import (
+ selfeval, quote, definition, variable,
+ assignment, conditional, lambdaexpr,
+ procedure, sequence, binding
+ )
+from serval.expression.util import cons, is_load, load
+
+
+class Interpreter(object):
+ """Serval interpreter.
+
+ Closely follows Eval/Apply from SICP Ch. 4
+ """
+
+ def __init__(self):
+ self.env = setup_environment()
+
+ def interpret(self, expr):
+ return self._eval(expr, self.env)
+
+ def _eval(self, expr, env):
+ if selfeval.is_self_evaluating(expr):
+ return expr
+
+ if quote.is_quoted(expr):
+ return quote.text_of_quotation(expr)
+
+ if definition.is_definition(expr):
+ return self._eval_definition(expr, env)
+
+ if sequence.is_begin(expr):
+ return self._eval_sequence(sequence.begin_actions(expr), env)
+
+ if binding.is_let_binding(expr):
+ return self._eval_binding(expr, env)
+
+ if variable.is_variable(expr):
+ return lookup_variable_value(expr, env)
+
+ if assignment.is_assignment(expr):
+ return self._eval_assignment(expr, env)
+
+ if conditional.is_if(expr):
+ return self._eval_if(expr, env)
+
+ if conditional.is_cond(expr):
+ return self._eval(conditional.cond_to_if(expr), env)
+
+ if conditional.is_and(expr):
+ return self._eval_and(expr, env)
+
+ if conditional.is_or(expr):
+ return self._eval_or(expr, env)
+
+ if lambdaexpr.is_lambda(expr):
+ return procedure.make_procedure(
+ lambdaexpr.lambda_parameters(expr),
+ lambdaexpr.lambda_body(expr),
+ env
+ )
+
+ if procedure.is_application(expr):
+ return self._apply(
+ self._eval(procedure.operator(expr), env),
+ self._list_of_values(procedure.operands(expr), env)
+ )
+
+ def _eval_definition(self, expr, env):
+ define_variable(
+ definition.definition_variable(expr),
+ self._eval(definition.definition_value(expr), env),
+ env
+ )
+ return Symbol('ok')
+
+ def _eval_assignment(self, expr, env):
+ env.set_variable_value(
+ assignment.assignment_variable(expr),
+ self._eval(assignment.assignment_value(expr), env)
+ )
+ return Symbol('ok')
+
+ def _eval_binding(self, expr, env):
+ # return an application
+ return self._eval(
+ cons(lambdaexpr.make_lambda(binding.binding_variables(expr),
+ binding.binding_body(expr)),
+ binding.binding_values(expr)),
+ env)
+
+ def _eval_if(self, expr, env):
+ if self._eval(conditional.if_predicate(expr), env):
+ return self._eval(conditional.if_consequent(expr), env)
+
+ return self._eval(conditional.if_alternative(expr), env)
+
+ def _eval_and(self, expr, env):
+ if procedure.no_operands(procedure.operands(expr)):
+ return Boolean(True)
+
+ def inner(expr, env):
+ if sequence.is_last_expr(expr):
+ return self._eval(sequence.first_expr(expr), env)
+
+ if not self._eval(sequence.first_expr(expr), env):
+ return Boolean(False)
+
+ return inner(sequence.rest_exprs(expr), env)
+
+ return inner(sequence.rest_exprs(expr), env)
+
+ def _eval_or(self, expr, env):
+ if procedure.no_operands(procedure.operands(expr)):
+ return Boolean(False)
+
+ def inner(expr, env):
+ if sequence.is_last_expr(expr):
+ return self._eval(sequence.first_expr(expr), env)
+
+ first_value = self._eval(sequence.first_expr(expr), env)
+ if first_value:
+ return first_value
+
+ return inner(sequence.rest_exprs(expr), env)
+
+ return inner(sequence.rest_exprs(expr), env)
+
+ def _eval_sequence(self, expressions, env):
+ if sequence.is_last_expr(expressions):
+ return self._eval(sequence.first_expr(expressions), env)
+ else:
+ self._eval(sequence.first_expr(expressions), env)
+ return self._eval_sequence(sequence.rest_exprs(expressions), env)
+
+ def _list_of_values(self, expressions, env):
+ if procedure.no_operands(expressions):
+ return EmptyList
+ else:
+ return cons(
+ self._eval(procedure.first_operand(expressions), env),
+ self._list_of_values(procedure.rest_operands(expressions), env)
+ )
+
+ def _apply(self, proc, args):
+ if procedure.is_primitive_procedure(proc):
+ return procedure.apply_primitive_procedure(proc, args)
+ elif procedure.is_compound_procedure(proc):
+ return self._eval_sequence(
+ procedure.procedure_body(proc),
+ extend_environment(
+ procedure.procedure_parameters(proc),
+ args,
+ procedure.procedure_environment(proc)
+ )
+ )
+
+
+def main():
+ interpreter = Interpreter()
+
+ while True:
+ try:
+ buffer = raw_input('serval> ')
+ except EOFError:
+ print
+ break
+ lexer = Lexer(buffer)
+
+ try:
+ expressions = Parser(lexer).parse()
+ if not expressions:
+ continue
+ expression = expressions[0]
+
+ if is_load(expression):
+ load(interpreter, expression)
+ continue
+
+ result_expr = interpreter.interpret(expression)
+ if procedure.is_compound_procedure(result_expr):
+ print procedure.get_procedure_repr(result_expr)
+ else:
+ print result_expr
+ except Exception as e:
+ print str(e)
+ continue
+
122 src/serval/lexer.py
@@ -0,0 +1,122 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+import re
+
+from serval import tokens
+
+
+class Token(object):
+
+ def __init__(self, type, text):
+ self.type = type
+ self.text = text
+
+ def __str__(self):
+ return "<'{text}', {name}>".format(text=self.text, name=self.type)
+
+
+class LexerException(Exception):
+
+ def __init__(self, msg, pos):
+ self.msg = msg
+ self.pos = pos
+
+ def __str__(self):
+ return 'Error at position: %s - %s' % (self.pos, self.msg)
+
+
+class Lexer(object):
+
+ RULES = [
+ (r'[+-]?\d+(?![a-zA-Z])', tokens.NUMBER),
+ (r'#t|#f', tokens.BOOLEAN),
+ (r'#\\(?:newline|space|[a-zA-Z])', tokens.CHARACTER),
+ (r'".*"', tokens.STRING),
+ (r'\(', tokens.LPAREN),
+ (r'\)', tokens.RPAREN),
+ (r'\'', tokens.QUOTE),
+ (r'(?<=\s)\.(?=\s)', tokens.DOT),
+ # TODO: this one is ugly - need to simplify it
+ (r'[+-](?!\w)|[<>]=|[=*/><]?\d*([a-zA-Z_]*\w*[!$%&*/:<=>?^_~+-.@]*)*',
+ tokens.ID),
+ ]
+
+ IS_WHITESPACE = re.compile(r'\s+').match
+ IS_COMMENT = re.compile(r';.*').match
+
+ def __init__(self, buffer):
+ self.buffer = buffer
+ self.pos = 0
+ self.regexp = self._build_master_regexp()
+
+ def _build_master_regexp(self):
+ result = []
+ for regexp, group_name in self.RULES:
+ result.append(r'(?P<%s>%s)' % (group_name, regexp))
+
+ master_regexp = re.compile('|'.join(result), re.MULTILINE)
+ return master_regexp
+
+ def token(self):
+ buffer, regexp = self.buffer, self.regexp
+ IS_WHITESPACE = self.IS_WHITESPACE
+ IS_COMMENT = self.IS_COMMENT
+ end = len(buffer)
+
+ while True:
+ match = (IS_WHITESPACE(buffer, self.pos) or
+ IS_COMMENT(buffer, self.pos))
+
+ if match is not None:
+ self.pos = match.end()
+ else:
+ break
+
+ # the end
+ if self.pos >= end:
+ return Token(tokens.EOF, 'EOF')
+
+ match = regexp.match(buffer, self.pos)
+ if match is None:
+ raise LexerException('No valid token', self.pos)
+
+ self.pos = match.end()
+
+ group_name = match.lastgroup
+ token = Token(group_name, match.group(group_name))
+ return token
+
+ def __iter__(self):
+ return self.next()
+
+ def next(self):
+ while True:
+ token = self.token()
+ if token.type == tokens.EOF:
+ yield token
+ return
+ yield token
178 src/serval/model.py
@@ -0,0 +1,178 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+
+class Number(object):
+
+ def __init__(self, val):
+ self.val = val
+
+ def __str__(self):
+ return str(self.val)
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.val == other.val
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __lt__(self, other):
+ return isinstance(other, self.__class__) and self.val < other.val
+
+ def __le__(self, other):
+ return self.__eq__(other) or self.__lt__(other)
+
+ def __gt__(self, other):
+ return not self.__le__(other)
+
+ def __ge__(self, other):
+ return self.__eq__(other) or self.__gt__(other)
+
+ def __nonzero__(self):
+ return True
+
+
+class Boolean(object):
+
+ def __init__(self, val):
+ self.val = val
+
+ def __str__(self):
+ return '#t' if self.val else '#f'
+
+ def __nonzero__(self):
+ return self.val
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.val == other.val
+
+
+class Character(object):
+
+ def __init__(self, val):
+ self.val = val
+
+ def __str__(self):
+ return '#\\' + self.val
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.val == other.val
+
+ def __nonzero__(self):
+ return True
+
+
+class String(object):
+
+ def __init__(self, val):
+ self.val = val
+
+ def __str__(self):
+ return '"%s"' % self.val
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.val == other.val
+
+ def __nonzero__(self):
+ return True
+
+
+class Symbol(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __str__(self):
+ return self.name
+
+ def __eq__(self, other):
+ if isinstance(other, Symbol) and self.name == other.name:
+ return True
+
+ return False
+
+ def __nonzero__(self):
+ return True
+
+
+class EmptyList(object):
+
+ def __str__(self):
+ return '()'
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ def __nonzero__(self):
+ return True
+
+EmptyList = EmptyList()
+
+
+class Pair(object):
+
+ def __init__(self, head, tail):
+ self.head = head
+ self.tail = tail
+
+ def __str__(self):
+ return '(%s)' % self._write_pair(self)
+
+ @staticmethod
+ def _write_pair(pair):
+ head, tail = pair.head, pair.tail
+
+ output = str(head)
+
+ if isinstance(tail, Pair):
+ output += ' %s' % Pair._write_pair(tail)
+ return output
+
+ if tail is EmptyList:
+ return output
+
+ output += ' . %s' % str(tail)
+ return output
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
201 src/serval/parser.py
@@ -0,0 +1,201 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+import itertools
+
+from serval import tokens
+from serval.model import (
+ Number, Boolean, Character, String, Symbol, Pair, EmptyList)
+
+# datum: simple_datum | compound_datum
+# simple_datum: boolean | number | character | string | symbol
+# symbol: identifier
+# compound_datum: list
+# list: ( datum *) | ( datum + . datum ) | abbreviation
+# abbreviation: abbrev_prefix datum
+# abbrev_prefix: '
+
+class ParserException(Exception):
+ pass
+
+
+class Parser(object):
+ """Parser for a Scheme subset.
+
+ Partially implements grammar definition from R5RS
+ """
+
+ def __init__(self, lexer):
+ self.lexer = lexer
+ self.marks = []
+ self.lookahead = []
+ self.pos = 0
+ self._sync(1)
+
+ def parse(self):
+ """Main entry method.
+
+ Returns a list of parsed expressions.
+ """
+ result = []
+ while self._lookahead_type(0) != tokens.EOF:
+ result.append(self._datum())
+ return result
+
+ def _datum(self):
+
+ if self._lookahead_type(0) in (tokens.LPAREN, tokens.QUOTE):
+ return self._list()
+
+ return self._simple_datum()
+
+ def _simple_datum(self):
+ token = self._lookahead_token(0)
+
+ if token.type == tokens.NUMBER:
+ expr = Number(int(token.text))
+
+ elif token.type == tokens.BOOLEAN:
+ expr = Boolean(True if token.text == '#t' else False)
+
+ elif token.type == tokens.CHARACTER:
+ expr = Character(token.text[2:])
+
+ elif token.type == tokens.STRING:
+ expr = String(token.text.strip('"'))
+
+ elif token.type == tokens.ID:
+ expr = Symbol(token.text)
+
+ else:
+ raise ParserException('No viable alternative')
+
+ self._match(token.type)
+ return expr
+
+ def _abbreviation(self):
+ self._match(tokens.QUOTE)
+
+ if self._lookahead_type(0) == tokens.LPAREN:
+ expr = self._list()
+ else:
+ expr = self._simple_datum()
+
+ return Pair(Symbol('quote'), Pair(expr, EmptyList))
+
+ def _list(self):
+ result = []
+
+ if self._lookahead_type(0) == tokens.QUOTE:
+ return self._abbreviation()
+
+ self._match(tokens.LPAREN)
+
+ # empty list
+ if self._lookahead_type(0) == tokens.RPAREN:
+ self._match(tokens.RPAREN)
+ return EmptyList
+
+ index, dot_index = 0, -1
+
+ while self._lookahead_type(0) != tokens.RPAREN:
+ head = self._datum()
+
+ if self._lookahead_type(0) == tokens.DOT:
+ self._match(tokens.DOT)
+ tail = self._datum()
+ result.append(Pair(head, tail))
+ dot_index = index
+ break
+ else:
+ result.append(head)
+
+ index += 1
+
+ self._match(tokens.RPAREN)
+
+ if dot_index > 0:
+ dot_index = len(result) - dot_index - 1
+
+ tail = EmptyList
+ for index, expr in enumerate(reversed(result)):
+ if index == dot_index:
+ tail = expr
+ else:
+ tail = Pair(expr, tail)
+
+ return tail
+
+ ##########################################################
+ # Parser helper methods
+ ##########################################################
+ def _sync(self, index):
+ if index + self.pos > len(self.lookahead):
+ number = index + self.pos - len(self.lookahead)
+ self._fill(number)
+
+ def _fill(self, number):
+ self.lookahead.extend(itertools.islice(self.lexer, 0, number))
+
+ def _lookahead_type(self, number):
+ return self._lookahead_token(number).type
+
+ def _lookahead_token(self, number):
+ self._sync(number)
+ return self.lookahead[self.pos + number]
+
+ def _match(self, token_type):
+ if self._lookahead_type(0) == token_type:
+ self._consume()
+ else:
+ raise ParserException(
+ 'Expecting %s; found %s' % (
+ token_type, self._lookahead_token(0))
+ )
+
+ def _consume(self):
+ self.pos += 1
+ if self.pos == len(self.lookahead) and not self._is_speculating():
+ self.lookahead = []
+ self.pos = 0
+
+ self._sync(1)
+
+ def _is_speculating(self):
+ return bool(self.marks)
+
+ def _mark(self):
+ self.marks.append(self.pos)
+
+ def _release(self):
+ self._seek(self.marks.pop())
+
+ def _index(self):
+ return self.pos
+
+ def _seek(self, index):
+ self.pos = index
+
84 src/serval/scope.py
@@ -0,0 +1,84 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+from serval.expression.procedure import (primitive_procedure_names,
+ primitive_procedure_values)
+from serval.expression.util import pair_to_list
+
+
+class Environment(object):
+
+ def __init__(self, parent=None, bindings=None):
+ self.parent = parent
+ self.bindings = dict() if bindings is None else bindings
+
+ def define_variable(self, symbol, val):
+ self.bindings[symbol.name] = val
+
+ def set_variable_value(self, symbol, val):
+ name = symbol.name
+ if name in self.bindings:
+ self.bindings[name] = val
+
+ elif self.parent is not None:
+ self.parent.set_variable_value(symbol, val)
+
+ else:
+ raise NameError('Unbound variable - SET! %s' % name)
+
+ def load(self, symbol):
+ name = symbol.name
+ if name in self.bindings:
+ return self.bindings[name]
+
+ if self.parent is not None:
+ return self.parent.load(symbol)
+
+ return None
+
+
+def setup_environment():
+ bindings = dict(
+ zip(primitive_procedure_names(), primitive_procedure_values())
+ )
+
+ return Environment(bindings=bindings)
+
+def define_variable(var, val, env):
+ env.define_variable(var, val)
+
+def lookup_variable_value(var, env):
+ val = env.load(var)
+ if val is None:
+ raise NameError('Unbound variable: %s' % var)
+ return val
+
+def extend_environment(variables, values, env):
+ bindings = dict(zip([var.name for var in pair_to_list(variables)],
+ pair_to_list(values)))
+
+ env = Environment(parent=env, bindings=bindings)
+ return env
0 src/serval/tests/__init__.py
No changes.
611 src/serval/tests/test_interpreter.py
@@ -0,0 +1,611 @@
+###############################################################################
+#
+# Copyright (c) 2010 Ruslan Spivak
+#
+# 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.
+#
+###############################################################################
+
+__author__ = 'Ruslan Spivak <ruslan.spivak@gmail.com>'
+
+import os
+import shutil
+import unittest
+
+THIS_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+class BaseTestCase(unittest.TestCase):
+
+ def _interpret(self, text, interpreter=None):
+ from serval.lexer import Lexer
+ from serval.parser import Parser
+ from serval.interpreter import Interpreter
+ from serval.expression.procedure import (
+ is_compound_procedure, get_procedure_repr)
+
+ if interpreter is None:
+ interpreter = Interpreter()
+
+ expressions = Parser(Lexer(text)).parse()
+ for expr in expressions:
+ result_expr = interpreter.interpret(expr)
+ if is_compound_procedure(result_expr):
+ result = get_procedure_repr(result_expr)
+ else:
+ result = str(result_expr)
+
+ return result
+
+
+class InterpreterRepresentationTestCase(BaseTestCase):
+
+ def test_constant_boolean_true_repr(self):
+ result = self._interpret('#t')
+ self.assertEquals(result, '#t')
+
+ def test_constant_boolean_false_repr(self):
+ result = self._interpret('#f')
+ self.assertEquals(result, '#f')
+
+ def test_constant_character_repr(self):
+ result = self._interpret(r'#\a')
+ self.assertEquals(result, r'#\a')
+
+ def test_constant_character_newline_repr(self):
+ result = self._interpret(r'#\newline')
+ self.assertEquals(result, r'#\newline')
+
+ def test_quoted_list_repr(self):
+ result = self._interpret("'(1 2 3)")
+ self.assertEquals(result, '(1 2 3)')
+
+ def test_quoted_list_quote_repr(self):
+ result = self._interpret('(quote (1 2 3))')
+ self.assertEquals(result, '(1 2 3)')
+
+ def test_quoted_list_repr_quote(self):
+ result = self._interpret('(quote (1 2 3))')
+ self.assertEquals(result, '(1 2 3)')
+
+ def test_improper_list_repr(self):
+ result = self._interpret("'(1 2 . 3)")
+ self.assertEquals(result, '(1 2 . 3)')
+
+ def test_proper_list_with_pair_repr(self):
+ result = self._interpret("'(1 (2 . 3))")
+ self.assertEquals(result, '(1 (2 . 3))')
+
+ def test_nested_pairs_repr(self):
+ result = self._interpret("'(1 . (2 . 3))")
+ self.assertEquals(result, '(1 2 . 3)')
+
+ def test_pair_with_empty_list_repr(self):
+ result = self._interpret("'(1 . (2 . (3 . ())))")
+ self.assertEquals(result, '(1 2 3)')
+
+ def test_list_of_lists_repr(self):
+ result = self._interpret("'((1 2) (3 4))")
+ self.assertEquals(result, '((1 2) (3 4))')
+
+ def test_empty_list_repr(self):
+ result = self._interpret("'()")
+ self.assertEquals(result, '()')
+
+ def test_list_with_cdr_as_empty_list_repr(self):
+ result = self._interpret("'(1 ())")
+ self.assertEquals(result, '(1 ())')
+
+ def test_pair_with_cdr_as_empty_list_repr(self):
+ result = self._interpret("'(1 . ())")
+ self.assertEquals(result, '(1)')
+
+ def test_pair_with_car_as_empty_list_repr(self):
+ result = self._interpret("'(() . 1)")
+ self.assertEquals(result, '(() . 1)')
+
+ def test_quoted_list_with_car_as_empty_list_repr(self):
+ result = self._interpret('(quote (() 1))')
+ self.assertEquals(result, '(() 1)')
+
+
+class IFSpecialFormTestCase(BaseTestCase):
+
+ def test_if_positive_number(self):
+ result = self._interpret(
+ """
+ (define a 5)
+ (if a
+ '(1 2 3 4 5)
+ '(0))
+ """)
+ self.assertEquals(result, '(1 2 3 4 5)')
+
+ def test_if_zero_number(self):
+ result = self._interpret(
+ """
+ (define a 0)
+ (if a
+ '(1 2 3 4 5)
+ '(0))
+ """)
+ self.assertEquals(result, '(1 2 3 4 5)')
+
+ def test_if_true_boolean(self):
+ result = self._interpret(
+ """
+ (define a #t)
+ (if a
+ '(1 2 3 4 5)
+ '(0))
+ """)
+ self.assertEquals(result, '(1 2 3 4 5)')
+
+ def test_if_false_boolean(self):
+ result = self._interpret(
+ """
+ (define a #f)
+ (if a
+ '(1 2 3 4 5)
+ '(0))
+ """)
+ self.assertEquals(result, '(0)')
+
+
+class CONDDerivedFormTestCase(BaseTestCase):
+
+ def test_cond(self):
+ result = self._interpret(
+ """
+ (let ((x -1))
+ (cond
+ ((< x 0) (list 'minus (abs x)))
+ ((> x 0) (list 'plus x))
+ (else (list 'zero x))))
+ """)
+ self.assertEquals(result, '(minus 1)')
+
+ def test_cond_else_clause_with_multiple_expressions(self):
+ result = self._interpret(
+ """
+ (let ((x 0))
+ (cond
+ ((< x 0) (list 'minus (abs x)))
+ ((> x 0) (list 'plus x))
+ (else (cons x x) (list 'zero x))))
+ """)
+ self.assertEquals(result, '(zero 0)')
+
+
+class ANDSpecialFormTestCase(BaseTestCase):
+
+ def test_and_conditional(self):
+ self.assertEquals(self._interpret('(and 1 2 3)'), '3')
+
+ def test_and_conditional_no_expressions(self):
+ self.assertEquals(self._interpret('(and)'), '#t')
+
+ def test_and_conditional_zero_number(self):
+ self.assertEquals(self._interpret('(and 1 0 3)'), '3')
+
+ def test_and_conditional_false(self):
+ self.assertEquals(self._interpret('(and 1 #f 3)'), '#f')
+
+ def test_and_conditional_true(self):
+ self.assertEquals(self._interpret('(and 1 2 #t)'), '#t')
+
+ def test_and_conditional_true_nested_expressions(self):
+ self.assertEquals(self._interpret('(and (< 1 2) (> 3 1))'), '#t')
+
+ def test_and_conditional_false_nested_expressions(self):
+ self.assertEquals(self._interpret('(and (>= 1 2) (> 3 1))'), '#f')
+
+
+class ORSpecialFormTestCase(BaseTestCase):
+
+ def test_or_conditional(self):
+ self.assertEquals(self._interpret('(or #f 1 2)'), '1')
+
+ def test_or_conditional_no_expressions(self):
+ self.assertEquals(self._interpret('(or)'), '#f')
+
+ def test_or_conditional_zero_number(self):
+ self.assertEquals(self._interpret('(or 0 1 2)'), '0')
+
+ def test_or_conditional_true(self):
+ self.assertEquals(self._interpret('(or (< 1 2) (> 4 3))'), '#t')
+
+ def test_or_conditional_return_expr(self):
+ self.assertEquals(self._interpret("(or #f '(1 2) '(3 4))"), '(1 2)')
+
+ def test_or_conditional_false(self):
+ self.assertEquals(self._interpret('(or #f (> 3 4))'), '#f')
+
+
+class AssignmentTestCase(BaseTestCase):
+
+ def test_assignment(self):
+ result = self._interpret(
+ """
+ (define a 5)
+ (set! a 7)
+
+ a
+ """)
+ self.assertEquals(result, '7')
+
+ def test_assignment_outer_scope(self):
+ result = self._interpret(
+ """
+ ((lambda (x)
+ (define y x)
+ ((lambda (z)
+ (set! y z))
+ 3)
+ y)
+ 10)
+ """)
+ self.assertEquals(result, '3')
+
+ def test_assignment_exception(self):
+ self.assertRaises(NameError, self._interpret, '(set! a 5)')
+
+
+class BindingTestCase(BaseTestCase):
+
+ def test_binding_let_selfeval_expressions(self):
+ result = self._interpret(
+ """
+ (let ((x 1) (y 2))
+ (+ x y))
+ """)
+ self.assertEquals(result, '3')
+
+ def test_binding_let_builtin_procedures_as_expressions(self):
+ result = self._interpret(
+ """
+ (let ((x (* 3 3)) (y (+ 1 10)))
+ (+ x y))
+ """)
+ self.assertEquals(result, '20')
+
+ def test_binding_let_quoted_expressions(self):
+ result = self._interpret(
+ """
+ (let ((x 'a) (y '(b c)))
+ (cons x y))
+ """)