Sat, 08 Oct 2005

"""Very basic object-calculus evaluator.

No parser yet, though there's a pretty-printer (well, that's
stretching 'pretty'...).  Horrible hacks for abusing Python's parser
though.  See the bottom.

Thanks to Martin Abadi and Luca Cardelli for the formalism, David
Gibson for lending me their mindblowing book, Dave Long for
encouragement and mentoring, Mark Lentczner for giving me a clue about
the internals of prototype-based object systems (although clue
probably doesn't show here), Jonathan Edwards for building something
that actually worked and teaching me about it, DeLesley Hutchins for
friendship, ideas, and Ohmu.

TODO:
- write a pretty-printer (done in another module now):
  - x{...}.'()'  ->  x(...)
  - x = an_expr  ->  someunusedselfvar.x = an_expr
- write a GUI
- write a parser
- add more shortcuts to the pretty-printer, like those used in my recent
  kragen-hacks post with more software in this language:
  - {a, b, c}    ->  { arg1 = a, arg2 = b, arg3 = c }
  - infix operators: x + y -> x.'+'(y) (i.e. x.'+'{arg1=y}.'()'
  - x[y] -> x.'[]'(y)
- add primitive operations so it could possibly compute something in a
  useful way
- add a 'lint' to check for calls to nonexistent methods.  This isn't
  necessarily an error, since the methods may be added in subclasses,
  but it's always an error when it happens in my code.

"""

import types

def any(alist):
    for item in alist:
        if item: return True
    return False

class Record:
    """An object that has some fields."""
    # Reimplemented more or less from memory from MetaPy.Record.
    def __init__(self, *args):
        assert len(args) == len(self.fields)
        for name, value in zip(self.fields, args):
            setattr(self, name, value)
    def __repr__(self):
        comma = ', '
        return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
                              comma.join([repr(getattr(self, name))
                                          for name in self.fields]))

class OCObject(Record):
    """Object-calculus object."""
    fields = ['methods']
    def methodnames(self):
        return [name for name, method in self.methods]
    def call_method(self, methodname):
        """Evaluate method's return value."""
        for name, method in self.methods:
            if name == methodname: return method.call(self)
        raise "Method not found", methodname
    def derive(self, more_methods):
        """Create derivative object with me as prototype."""
        return OCObject(more_methods + self.methods)

class OCMethod(Record):
    """Object-calculus method."""
    fields = 'selfname', 'body', 'environment'
    def augment(self, name, val):
        rv = self.environment.copy()
        rv[name] = val
        return rv
    def call(self, obj):
        return self.body.evaluate(self.augment(self.selfname, obj))

class SyntaxNode(Record):
    """Abstract class: tree of symbols you might write out on paper."""
    # syntactic sugar below
    def __getitem__(self, other): return MethodCall(self, other)
    def __call__(self, *overrides):
        return ObjectDerivation(self, overrides)

class Quoter(Record):
    """Helper object to quote strings customizably."""
    fields = ['delimiter']
    def escape_char(self, char):
        if char in '\\' + self.delimiter: return '\\' + char
        else: return char
    def quote(self, value):
        return '%s%s%s' % (self.delimiter,
                           ''.join(map(self.escape_char, value)),
                           self.delimiter)

class Constant(SyntaxNode):
    """A syntactic string or number or something."""
    # Ultimately these should evaluate to OCObjects...
    fields = ['value']
    quoter = Quoter('"')
    def evaluate(self, environment): return self.value
    def __str__(self):
        if type(self.value) in types.StringTypes:
            return self.quoter.quote(self.value)
        else:
            return repr(self.value)

class NameQuoter:
    """Helper for quoting identifiers with '' if necessary."""
    quoter = Quoter("'")
    namechars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'
    middlenamechars = namechars + '0123456789'
    def has_funny_name(self, name):
        if len(name) == 0: return True
        if name[0] not in self.namechars: return True
        if any([char not in self.middlenamechars for char in name]):
            return True
        return False
    def quote(self, name):
        if self.has_funny_name(name): return self.quoter.quote(name)
        else: return name

namequoter = NameQuoter()

class Variable(SyntaxNode):
    fields = 'name', 'origin'
    def evaluate(self, environment): return environment[self]
    def __str__(self): return namequoter.quote(self.name)
    # Note Record's __repr__ isn't quite right here because it doesn't
    # preserve identity, and because we don't have an __eq__, identity
    # matters!


class ObjectExpression(SyntaxNode):
    """Abstract base class for objects that contain methods."""
    def stringify_methods(self):
        return self.indent('\n'.join(map(str, self.methods)))
    def indent(self, astr):
        lines = astr.split('\n')
        if not lines[-1]: lines = lines[:-1]
        return ''.join('  ' + line + '\n' for line in lines)

class ObjectLiteral(ObjectExpression):
    """An expression for an object that doesn't inherit from anything."""
    fields = ['methods']
    def evaluate(self, environment):
        return OCObject([m.evaluate(environment) for m in self.methods])
    def __str__(self): return '{\n%s}' % self.stringify_methods()

class ObjectDerivation(ObjectExpression):
    """An expression for an object derived from another."""
    fields = 'baseobj', 'methods'
    def evaluate(self, environment):
        return self.baseobj.evaluate(environment).derive(
            [method.evaluate(environment) for method in self.methods])
    def __str__(self):
        return '%s {\n%s}' % (str(self.baseobj), self.stringify_methods())


# This one is a little funny... it's not lexically an expression, it
# "evaluates" to something that isn't a value, its callers
# (ObjectLiteral and ObjectDerivation) know they're evaluating method
# definitions, not expressions, and it doesn't benefit from any of
# SyntaxNode's methods.  So it's not a SyntaxNode.
class MethodDefinition(Record):
    """Part of an object definition defining a single method."""
    fields = 'selfname', 'methodname', 'methodbody'
    def evaluate(self, environment):
        return (self.methodname,
                OCMethod(self.selfname, self.methodbody, environment))
    def __str__(self):
        return '%s.%s = %s' % (self.selfname,
                               namequoter.quote(self.methodname),
                               self.methodbody)

class MethodCall(SyntaxNode):
    """An expression such as {}.bar, calling a method."""
    fields = 'objexpr', 'methodname'
    def evaluate(self, environment):
        return self.objexpr.evaluate(environment).call_method(self.methodname)
    def __str__(self):
        return "%s.%s" % (self.objexpr, namequoter.quote(self.methodname))
    def __rshift__(self, other):
        """Syntactic sugar for constructing a method definition."""
        return MethodDefinition(self.objexpr, self.methodname, other)


### Unit tests below

def ok(a, b): assert a == b, (a, b)
def ensure_exception(thunk):
    try: thunk()
    except: pass
    else: assert 0, "ick"
    
def test():
    # Evaluating object-literal expressions
    emptyobjlit = ObjectLiteral([])
    emptyobj = emptyobjlit.evaluate({})
    ok(emptyobj.methodnames(), [])

    ensure_exception(lambda: emptyobj.call_method('bob'))

    # Defining some methods
    bobself = Variable('self', 'in bob')
    maryself = Variable('self', 'in mary')
    freevar = Variable('freevar', 'from outside')
    fvself = Variable('self', 'in fv')
    assert bobself != maryself
    nonemptyobjlit = ObjectLiteral(
        [MethodDefinition(bobself, 'bob', bobself),
         MethodDefinition(maryself, 'mary', Constant(3)),
         MethodDefinition(fvself, 'fv', freevar)])

    nonemptyobj = nonemptyobjlit.evaluate({freevar: 1973})
    ok(nonemptyobj.methodnames(), ['bob', 'mary', 'fv'])
    ok(nonemptyobj.call_method('bob'), nonemptyobj)
    ok(nonemptyobj.call_method('mary'), 3)
    ok(nonemptyobj.call_method('fv'), 1973)
    ensure_exception(lambda: nonemptyobj.call_method('nonexistent'))

    # Deriving an object; scoping.
    nonemptyobjvar = Variable('nonemptyobj', 'from outside')

    derivedobjexpr = ObjectDerivation(nonemptyobjvar, [
        MethodDefinition(fvself, 'fv2', freevar),
        MethodDefinition(maryself, 'mary', Constant(15))
    ])

    derivedobj = derivedobjexpr.evaluate(
        {nonemptyobjvar: nonemptyobj, freevar: 1976})
    ok(derivedobj.call_method('mary'), 15)
    # see, two values for the same variable from different contours.
    ok(derivedobj.call_method('fv2'), 1976)
    ok(derivedobj.call_method('fv'), 1973)

    # Evaluating method-call expressions.
    methodcallexpr = MethodCall(derivedobjexpr, 'mary')
    ok(methodcallexpr.evaluate({nonemptyobjvar: nonemptyobj}), 15)

    # OK, now for booleans.  From kragen-tol post "How to take
    # parameters in the object calculus?",
    #http://lists.canonical.org/pipermail/kragen-tol/2005-September/000792.html
    # 2005-09-28.
    booleans = Variable('booleans', 'outermost')
    self = Variable('self', 'innermore')
    x = Variable('x', 'innermost')
    truenegated = MethodDefinition(self, 'negated',
                                   MethodCall(booleans, 'false'))
    trueiftrue = MethodDefinition(self, 'ifTrue', ObjectLiteral([
        MethodDefinition(x, 'then', Constant(1)),
        MethodDefinition(x, 'else', Constant(0)),
        MethodDefinition(x, 'val', MethodCall(x, 'then')),
        ]))
    trueiffalse = MethodDefinition(self, 'ifFalse',
                                   MethodCall(MethodCall(self, 'negated'),
                                              'ifTrue'))
    truedef = MethodDefinition(booleans, 'true', ObjectLiteral([
        truenegated,
        trueiftrue,
        trueiffalse,
        ]))

    falsenegated = MethodDefinition(self, 'negated',
                                    MethodCall(booleans, 'true'))
    falseiftrue = MethodDefinition(self, 'ifTrue', ObjectDerivation(
        # Note that this expression erroneously read booleans.ifTrue
        # in the original; it should have read booleans.true.ifTrue
        MethodCall(MethodCall(booleans, 'true'), 'ifTrue'), [
            MethodDefinition(x, 'val', MethodCall(x, 'else')),
            ]
        ))
                                   
    falsedef = MethodDefinition(booleans, 'false', ObjectDerivation(
        MethodCall(booleans, 'true'), [falsenegated, falseiftrue]
        ))

    booleansdef = ObjectLiteral([truedef, falsedef])
    booleans_obj = booleansdef.evaluate({})
    booleans_true = booleans_obj.call_method('true')
    booleans_false = booleans_obj.call_method('false')

    hungry = Variable('hungry', 'original post')
    hungryexpr = MethodCall(ObjectDerivation(MethodCall(hungry, 'ifTrue'), [
        MethodDefinition(x, 'then', Constant("eat")),
        MethodDefinition(x, 'else', Constant("don't eat")),
        ]), 'val')

    ok(hungryexpr.evaluate({hungry: booleans_true}), "eat")
    ok(hungryexpr.evaluate({hungry: booleans_false}), "don't eat")

    # Python syntactic sugar and debug display
    desired_hungry_string = ("hungry.ifTrue {\n" +
                             "  x.then = \"eat\"\n" +
                             "  x.else = \"don't eat\"\n" +
                             "}.val")
    ok(str(hungryexpr), desired_hungry_string)

    # The following is pretty sour as syntactic sugar goes, but it's
    # terser than the earlier expression, and IMHO in a clearer order;
    # it's just that all the operators mean something almost, but not
    # quite, entirely unlike their usual meaning.
    
    other_hungry_expr = hungry['ifTrue'](
        x['then'] >> Constant("eat"),
        x['else'] >> Constant("don't eat")
    )['val']

    ok(str(other_hungry_expr), str(hungryexpr))

    gross_booleans_def = ObjectLiteral([
        booleans['true'] >> ObjectLiteral([
            self['negated'] >> booleans['false'],
            self['ifTrue'] >> ObjectLiteral([
                x['then'] >> Constant(1),
                x['else'] >> Constant(0),
                x['val'] >> x['then'],
            ]),
            self['ifFalse'] >> self['negated']['ifTrue'],
        ]),
        booleans['false'] >> booleans['true'](
            self['negated'] >> booleans['true'],
            self['ifTrue'] >> booleans['true']['ifTrue'](
                x['val'] >> x['else'],
            ),
        ),
    ])

    ok(str(gross_booleans_def), str(booleansdef))

    # Nested indentation
    ok(str(booleans(self['true'] >> booleans['true'](
        self['and'] >> self['ifTrue']))),
    "booleans {\n" +
    "  self.true = booleans.true {\n" +
    "    self.and = self.ifTrue\n" +
    "  }\n}")

    # Strings should always be displayed with double quotes.
    ok(str(Constant("boo")), '"boo"')
    ok(str(Constant("doesn't")), '"doesn\'t"')
    ok(str(Constant('"yes"')), r'"\"yes\""')
    ok(str(Constant(r'\"yes\"')), r'"\\\"yes\\\""')

    # Funny names should be displayed with single quotes.
    ok(str(Variable("haha", 'py')), "haha")
    ok(str(Variable("ha ha", 'py')), "'ha ha'")
    ok(str(Variable("don't", 'py')), r"'don\'t'")
    ok(str(Variable("\\", 'py')), "'\\\\'")
    ok(str(Variable('a0', 'py')), "a0")
    # Really evil things to name your variables:
    ok(str(Variable('', 'py')), "''")
    ok(str(Variable('0', 'py')), "'0'")

    # Funny method names too.
    ok(str(hungry['man']), 'hungry.man')
    ok(str(hungry['m n']), "hungry.'m n'")

    # Also in method definitions.
    ok(str(hungry['man'] >> Constant("pancake")),
        'hungry.man = "pancake"')
    ok(str(hungry['m n'] >> Constant("pancake")),
        'hungry.\'m n\' = "pancake"')
    haha = Variable("ha ha", 'py')
    ok(str(haha['ha ha'] >> haha['ha ha']),
        "'ha ha'.'ha ha' = 'ha ha'.'ha ha'")

test()