Ein Interpreter für Io in mehreren Teilen

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Ich habe angeregt durch die kürzliche Diskussion über OO und Programmiersprachen hier im Forum vor einigen Tagen begonnen, einen Interpreter für ein Subset der Programmiersprache Io zu bauen. In der wagen Hoffnung, es könnte jemanden interessieren, möchte ich den Code hier vorstellen.

Wenn das nicht das richtige Forum hier ist, bitte Bescheid sagen, ich höre dann auch auf ;)

Ich wollte das ganze eigentlich mit einem Tutorial und einer Erklärung zu der Sprache begleiten, denn es war doch kniffeliger, als ich dachte. Andererseits habe ich nicht so viel Zeit und werde mich daher sehr kurz fassen. Fragen zum Code beantworte ich aber gerne.

Ich werde das in hoffentlich handlichen Bröckchen posten, beginnend mit der internen Repräsentation von Messages, dem Parser dazu, der internen Repräsentation von Slot-Objekten, dem eigentlichen Interpreter und ein paar Methoden der Objektbibliothek - die ich aber selbst noch nicht geschrieben habe... mal schauen.

1. Nachrichten

Io ist extrem objektorientiert in dem Sinn, dass es nur Objekte gibt und die einzige Operation auf Nachrichten das Senden ist. Io ist zudem reflektiv, d.h. Nachrichten sind wieder Objekte.

Jedes Io-Programm kann als eine (mehr oder weniger komplexe) Nachricht dargestellt werden.

Nachrichten haben immer einen Empfänger (target genannt), einen Namen (symbol) und tragen optional ein oder mehr Argumente (arguments) in Form weiterer Nachrichten. Es gibt zudem literale Nachrichten in Form von Zahlen und Zeichenketten, mir fällt es leichter, diese einfach als Literale zu verstehen.

In dem folgenden Beispiel sieht man 7 Nachrichten (die literalen Nachrichten 'a' und 1 fasse ich mal als Literal auf, sonst wären es 9):

Code: Alles auswählen

setSlot('a', List clone append(1)); a count
Die Nachricht "setSlot" hat zwei Argumente "'a'" und "List clone append(1)" und geht an den aktuellen Kontext (auf oberster Ebene wäre dies das lobby-Objekt) und erzeugt dort einen neuen Slot "a", der enthält, was das Auswerten des anderen Arguments ergibt. "List" geht an den selben Kontext und liefert auf diese Weise den List-Prototypen, dem dann die Nachricht "clone" geschickt wird, um ein neues Listenobjekt zu erzeugen, welches das Verhalten der prototypischen Liste erbt. Mit "append" kann ich der Liste ein Element hinzufügen, Ergebnis von "append" sei wieder das Listenobjekt, welches dann also in Slot "a" landet. Die Nachricht ";" unterbricht die Kette der Aufrufe indem sie den ursprünglichen impliziten Empfänger, den Kontext, zurückgibt und prompt wird die Nachricht "a" an diesen Kontext geschickt, was dazu führt, dass der Objekt aus dem gleichnamigen Slots zurückgegeben wird - die Liste - und "count" zählt deren Elemente.

Nachrichten repräsentiere ich in Python mit der folgenden Klasse:

Code: Alles auswählen

class Message(object):
    """
    Represent a message, either a literal or a message send expression.
    """
    def __init__(self, symbol, arguments=None, next=None):
        self.symbol, self.arguments, self.next = symbol, arguments, next

    def isliteral(self):
        return self.arguments is None
Eine Nachricht ohne arguments-Liste fasse ich als Literal auf. Der Name (symbol) ist dann ein int oder str. Andernfalls muss der Name ein String sein und die arguments-Liste muss weitere Nachrichten enthalten. Der dritte optionale Parameter dient dazu, Nachrichten zu verketten.

Obigen Ausdruck kann ich folglich so erzeugen:

Code: Alles auswählen

Message(
  "setSlot", [
    Message("a"), 
    Message("List", [], Message("clone", [], Message("append", [Message(1)])))],
  Message(";", [], Message("a", [], Message("count", []))))
Um zu sehen, dass das stimmt, habe ich mir noch eine repr-Methode geschrieben:

Code: Alles auswählen

    def __repr__(self):
        if self.isliteral():
            s = repr(self.symbol)
        else:
            s = self.symbol
            if self.arguments:
                s += "(" + ", ".join(repr(a) for a in self.arguments) + ")"
        if self.next:
            return s + " " + repr(self.next)
        return s
Damit kann ich Io-Programm in Python definieren - zugegeben ist jede Strafarbeit schöner als ein nicht-trivialen Nachrichtenaudruck auf diese Weise zu bauen. Daher stelle ich das nächste Mal einen Parser vor, der Strings in Nachrichtenausdrücke verwandeln kann.

Stefan
BlackJack

Ich weiss ja nicht in wie weit sich die Klasseninterna mit den Io Attributen decken soll(t)en, aber was Du `symbol` nennst, ist auf Io-Nachrichten als `name` verfügbar.

Code: Alles auswählen

Io> m := message(setSlot('a', List clone append(1)); a count)
==> setSlot('(a) ', List clone append(1)) ;
a count
Io> m name
==> setSlot
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

BlackJack hat geschrieben:was Du `symbol` nennst, ist auf Io-Nachrichten als `name` verfügbar.
Ich hatte mich an der Grammatik orientiert. Dort heißt es `symbol`. Den echten Interpreter habe ich nicht herangezogen, sondern mich bislang ausschließlich an der Dokumentation orientiert.

Stefan
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

2. Der Parser

Ein Io-Programm kann durch folgende Grammatik beschrieben werden:

Code: Alles auswählen

exp = {message | ";"}
message = symbol [arguments]
symbol = name | integer | string
arguments = "(" [exp {"," exp}] ")"
Ich betrachte name, integer und string als Terminalsymbole der Grammatik, die typischerweise von einem Lexer (oder Scanner) erzeugt werden. Der Parser soll den Rest in Nachrichtenausdrücke unter Verwendung der im ersten Teil definierten Klasse "Message" verwandeln.

Ich nutze den folgenden regulären Ausdruck, um den Quelltext in Token zu zerhacken:

Code: Alles auswählen

_token_re = re.compile(r"\s*([(),;]|'.*?'|\d+|\w+|[-+*/=<>:]+|)")
In nur einer Zeile kann ich so führende Leerzeichen überspringen und dann entweder Klammern, Komma oder Semikolon (die spärliche Syntax von Io), einen String, eine Zahl, einen Namen oder eine Folge von Operatorzeichen erkennen. Wenn überhaupt nichts passt, liefert der reguläre Ausdruck den leeren String (ganz hinten, das letzte "|") und das werte ich als Ende der Eingabe.

Parser kann man auf verschiedene Art bauen. Ich habe mal einen extremen Ansatz ausprobiert, der statt einer Klasse und Methoden mit einer Funktion mit inneren Funktionen auskommt. Die Grammatikregeln lassen sich nahezu 1:1 in innere Funktionen zum Erkennen der Sprache verwandeln (Stichwort LL(1), recursive descent parser), da die Grammatik ein bisschen zu einfach ist, was den Einsatz von Operatoren in "echtem Io" angeht, musste ich im Code weiter unten zusätzlichen Aufwand treiben.

Io fasst "a := 1" und "b = a" als Makros für die Nachrichten "setSlot('a', 1)" und "updateSlot('b', a)" auf und erlaubt Nachrichten wie "3 + 4", die allesamt keine Klammern um ihr einziges Argument haben. Diese Regel ist noch recht leicht einzubauen, jedoch ist zu beachten, dass "a := 1; b" sicherlich als "a :=(1); b" und nicht als "a :=(1 ; b)" aufgefasst werden soll.

Das Umwandeln der Makros mache ich übrigens in einem zweiten Schritt - in der Hoffnung, der Parser ist dadurch einfacher. Geprüft habe ich es nicht.

Hier ist der Parser - zunächst ohne die inneren Funktionen:

Code: Alles auswählen

def parse(s):
    """
    Parse source `s` into a `Message` according the following grammar:
    (sahen wir weiter oben schon)
    """
    next = (m.group(1) for m in _token_re.finditer(s)).next
    
    ...
    
    return expand(exp(next())[0])
In der ersten Zeile sehen wir meinen Lexer, jeder Aufruf von "next()" liefert das nächste Token. Das aktuelle Token reiche ich in allen Funktionen herum, da Python nicht erlaubt, schreibend auf eine Variable "t" in der äußeren Methode zuzugreifen. Schade eigentlich. Meine Funktionen haben daher alle zwei Rückgabewerte und daher muss ich mit "[0]" den ersten Rückgabewert im finalen "return" auswählen. Die Funktion "expand()" kümmert sich um das Ersetzen von ":=" und "=".

Es folgt die eigentliche Funktion zum Erzeugen der (verketteten) Nachrichten:

Code: Alles auswählen

    def exp(t, tt=None):
       m, t = message(t)
       if t not in ('', ',', ')') and t != tt:
           m.next, t = exp(t, tt)
       return m, t
Mit dem optionalen Parameter "tt" löse ich das Problem, dass in dem Fall, dass ich das Argument hinter dem Operator auswerte, auch schon bei ";" abgebrochen werden soll. Ansonsten verkette ich solange Nachrichten, bis ich auf eine Klammer-zu, Komma oder das Ende der Eingabe stoße.

Eine einzelne Nachricht übersetzt diese Funktion:

Code: Alles auswählen

    def message(t):
        if t == '':
            return None, t
        if t[0] in "0123456789":
            return Message(int(t)), next()
        if t[0] == "'":
            return Message(t[1:-1]), next()
        if t == '(':
            s = ""
            a, t = arguments(t)
        elif t == ')':
            raise Exception("unexpected )")
        elif t == ',':
            raise Exception("unexpected ,")
        elif t == ';':
            return Message(t, []), next()
        elif 'A' <= t[0] <= 'Z' or 'a' <= t[0] <= 'z' or t[0] == '_':
            s = t
            a, t = arguments(next())
        else:
            s, t = t, next()
            if t == '(':
                a, t = arguments(t)
            else:
                e, t = exp(t, ';')
                a = [e]
        return Message(s, a), t
Es rächt sich mein simpler Lexer. Ich muss hier zu Fuß Zahlen, String, Namen und Operatoren erkennen. Für jedes mögliche Token gibt es einen Fall. Bemerkenswert ist der Fall, dass ich sofort auf eine Klammer stoße, in diesem Fall gibt es die leere Nachricht, ein Trick von Io, um das Klammern von Ausdrücken zu erlauben. Bei Namen nur aus Operatorzeichen darf man die Klammern weglassen.

Die optionalen Argumente erzeugt "arguments()":

Code: Alles auswählen

    def arguments(t):
        a = []
        if t == '(':
            t = next()
            while t != ')':
                e, t = exp(t)
                a.append(e)
                if t != ')':
                    if t != ',':
                        raise Exception(", expected")
                    t = next()
            t = next()
        return a, t
Und das war dann auch schon der Parser. Das echte Io ist etwas aufwendiger, denn dort gilt auch der Zeilenumbruch (aber nicht jeder) als ";" und es gibt glaube ich noch besondere Syntax für eckige und geschweifte Klammern und Operatoren haben eine bestimmte Rangfolge - meine sind einfach alle linksassoziativ.

Es reicht aber, um dies (und ähnliches) zu parsen:

Code: Alles auswählen

setSlot('a', List clone append(1)); a count 
Um ":=" und "=" zu ersetzen, laufe ich rekursiv über die Nachricht und manipuliere sie (das war kürzer und hoffentlich leichter verständlich als eine nicht-destruktive funktionale Lösung, die ich sonst vorgezogen hätte). Die Funktion "expand()" könnte übrigens auch eine Methode von "Message" sein:

Code: Alles auswählen

def expand(message):
    """
    Replace all occurences of := and = with setSlot and updateSlot messages.
    Return the modified message object. The message is modified in place.
    """
    if message.arguments:
        for m in message.arguments:
            expand(m)
    if message.next:
        expand(message.next)
        if message.next.symbol in (':=', '='):
            symbol = message.symbol
            if message.arguments is None:
                raise Exception("cannot assign to literal %r" % symbol)
            if len(message.arguments):
                raise Exception("cannot assign to message send expression")
            if len(message.next.arguments) != 1:
                raise Exception("assignment need 1 argument")
            message.symbol = 'setSlot' if message.next.symbol == ':=' else 'updateSlot'
            message.arguments = [Message(symbol), message.next.arguments[0]]
            message.next = message.next.next
    return message
Nun kann man auch

Code: Alles auswählen

a := List clone append(1); a count
schreiben und größere Io-Programme sind kein Problem mehr. Nur können wir sie natürlich noch nicht ausführen. Dazu müssen wir erst entscheiden, wie Objekte mit Slots in Python am besten repräsentiert werden.

Stefan
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

3. Objektrepräsentation

Wie sollen wir Io-Objekte in Python repräsentieren? Bei den Nachrichten haben wir bereits Python ints und strs benutzt, um Literale zu repräsentieren. In den Nachrichtenobjekten haben wir außerdem Python lists benutzt. Dies macht die Interaktion von Io mit Python einfach und scheint daher eine gute Idee zu sein.

Für Io-Objekte mit Slots würde sich ein dict anbieten, allerdings gibt es eine Vererbungsbeziehnung (die protos-Listen) zwischen den Objekten, die ein Python dict nicht kennt. Außerdem verhalten sich Methoden-Objekte (für die es merkwürdigerweise keinen eigenen Typ in der Io-Objektbibliothek zu geben scheint) anders als "normale" Slots-Objekte. Sie sind aktivierbar (aufrufbar).

Soll ich eine Unterklasse von dict benutzen? Eine eigene Klasse? Hat diese dann ein dict für die Slots oder benutzt man direkt die Attribute? Oder einfach in einem dict zwei spezielle Slots für die Liste der Prototypen und die Eigenschaft, aktivierbar zu sein ablegen?

Ich habe mich für die folgende Lösung entschieden:

Code: Alles auswählen

class IoObject(object):
    def __init__(self, protos, slots=None):
        self.__protos__ = protos
        if slots:
            self.__dict__.update(slots)

class IoMethod(IoObject):
    def __call__(self, io, context):
        ...
    
class IoBlock(IoMethod):
    def __calll__(self, io, context):
        ...
Ich nutze das in Python-Klassen eingebaute __dict__ für die Slots. Die Liste der Prototypen ist einfach ein (weiterer) Slot mit einem Namen, der hoffentlich zu keinem Konflikt führt. Dies macht die Interaktion mit Python sehr einfach, denn ich kann z.B. "obj.call.sender" benutzen, um auf Slots zuzugreifen. Io kennt Slot-Namen, die in Python illegal wären. Solange ich aber `setattr` und `getattr` für diese Namen benutze, scheint das kein Problem zu sein.

Ich habe eine besondere Klasse für Methoden, deren Exemplare callable sind. Ich übergebe hier den Interpreter, der Zugriff auf alle globalen Objekten und alle Funktionen bietet (siehe unten) und den Kontext, in dem die (Nachricht der) Methode ausgeführt werden soll. Der Kontext hat (wie in der Io-Dokumentation spezifiziert) einen Slot für "self", den Empfänger der Methode, die Parameter und das spezielle "call"-Objekt, über das der Sender der Methode, die ursprüngliche Nachricht und mehr in Erfahrung gebracht werden kann. Böcke sind noch einmal etwas spezieller als Methoden, sodass ich auch diese erkennen können muss.

Für die eingebauten System-Methoden (die in Python und nicht in Io geschrieben sind) möchte ich einfach Python-Funktionen benutzen - das motiviert zusätzlich die Entscheidung für callable.

Als nächstes möchte ich die Grundoperationen auf den Objekten vorstellen. Diese implementiere ich als Methoden der Klasse "Io", deren Exemplare auch alle globalen Objekten halten und damit ermöglichen, mehr als einen Io-Interpreter zu benutzen (ich mache mir mit dieser Entscheidung das Leben vielleicht unnötig schwer, aber egal).

Mit `get_protos()` kann ich die Liste der Prototypen eines Objekts ermitteln. Da Zahlen, Strings und Listen Io-Objekte sind und alles von außen einheitlich aussehen muss, kann ich nicht einfach "obj.__protos__" aufrufen. Stattdessen entscheide ich, dass es reicht, wenn sich alle Zahlen, alle Strings und alle Listen jeweils eine Liste von Prototypen teilen. Diese verwalte ich direkt in `Io`:

Code: Alles auswählen

class Io(object):
    def __init__(io):
        """
        Initialize a new Io interpreter, setting up default objects and methods.
        """
        io.object = IoObject([])
        
        io.int_protos = []
        io.str_protos = []
        io.list_protos = []
        ...
    
    def get_protos(io, target):
        """
        Return the list of prototypes of the `target` object.
        """
        if isinstance(target, int):
            return io.int_protos
        if isinstance(target, str):
            return io.str_protos
        if isinstance(target, list):
            return io.list_protos
        return target.__protos__
Setzen kann man die Liste natürlich auch - dazu dient `set_protos()`:

Code: Alles auswählen

class Io...
    def set_protos(io, target):
        """
        Set the list of prototypes for the `target` object.
        """
        if isinstance(target, int):
            io.int_protos = protos
        elif isinstance(target, str):
            io.str_protos = protos
        elif isinstance(target, list):
            io.list_protos = protos
        else:
            target.__protos__ = protos
Ein neues Objekt wird aus einem Prototyp mittels `clone()` erstellt:

Code: Alles auswählen

class Io...
    def clone(io, target, slots=None):
        """
        Create a new object that inherits from the `target` object.
        """
        if isinstance(target, (int, str)):
            return target
        if isinstance(target, list):
            return list(target)
        return target.__class__([target], slots)
Wird ein int oder str übergeben, kann davon kein Klon erzeugt werden. Eine Liste zu klonen heißt sie zu kopieren. Da sich Listen genau wie Zahlen und Strings alle einen Prototypen teilen, ist hier nicht mehr zu tun. Ansonsten erzeuge ich ein neues Exemplar der selben Klasse wie der Prototyp und trage das alte Objekt als Prototyp ein.

Einen neuen Slot zu erzeugen ist die einfachste Funktion zur Manipulation von Slots. Dies geschieht direkt. Den Wert zu ermitteln oder zu ändern greift auf eine interne Funktion `find_slot()` zurück, die in der kompletten Hierarchie der Prototypen nach dem Slot sucht und separat erklärt werden soll.

Code: Alles auswählen

class Io...
    def set_slot(io, target, name, value):
       """
       Create a new slot named `name` with value `value` in the `target` object.
       It is an error if `target` is not a slot object (having a `slots` attribute).
       """
       try:
           setattr(target, name, value)
           return value
       except AttributeError:
           raise Exception("cannot set %r in %r" % (name, target))
       
   def get_slot(io, target, name):
       """
       Return the value of the slot named `name` which is either located in
       the `target` object or one of its prototype objects. It is an error if
       the slot does not exist.
       """
       slot = io.find_slot(target, name)
       if slot:
           return slot.value
       raise Exception("unknown %r in %r" % (name, target))
       
   def update_slot(io, target, name, value):
       """
       Update the value of the slot named `name` which is either located in
       the `target` object or one of its prototype objects. It is an error if
       the slot does not exist.
       """
       slot = io.find_slot(target, name)
       if slot:
           slot.value = value
           return value
       raise Exception("cannot update %r in %r" % (name, target))
Für `find_slot` erzeuge ich kurzfristig die Illusion echter Slots, die wissen, aus welchem Objekt sie stammen. Diese Information wird später im Interpreter benötigt.

Code: Alles auswählen

class Io...
    class Slot(object):
        def __init__(self, target, name):
            self.target = target
            self.name = name
    
        def _get(self):
            return getattr(self.target, self.name)
    
        def _set(self, value):
            setattr(self.target, self.name, value)
    
        value = property(_get, _set)
    
    def find_slot(io, target, name):
        """
        Search for the slot named `name` in the `target` and all of its 
        prototype objects depth first. If found, return a Slot object which
        has a `value` attribute to set or get the slot value. If not found
        return `None`. Each object is visited only once, so loops in the
        prototype object graph are no problem.
        """
        visited = set()
        def find_in_object(target):
            if target in visited:
                return None
            visited.add(target)
            try:
                if name in target.__dict__:
                    return Io.Slot(target, name)
            except AttributeError:
                pass
            for proto in io.get_protos(target):
                slot = find_in_object(proto)
                if slot:
                    return slot
            return None
        return find_in_object(target)
Hier ist ein kurzes Testprogramm:

Code: Alles auswählen

io = Io()
o1 = io.clone(io.object)
o2 = io.clone(o1)
io.set_slot(o1, "answer", 42)
print io.get_slot(o2, "answer") #>> 42
io.update_slot(o2, "answer", 6 * 8)
io.set_slot(o2, "answer", 21)
print io.get_slot(o2, "answer") #>> 21
print io.get_slot(io.get_protos(o2)[0], "answer") #>> 48
o3 = io.clone(o1)
io.set_protos(o3, [o3, o1])
print io.get_slot(o3, "answer") #>> 48
Für die Interoperabilität mit Python ist es praktisch, dass die Slot-Operationen auch auf "normalen" Python-Objekten funktionieren:

Code: Alles auswählen

io = Io()
m = Message("foo", [Message(1)])
print io.get_slot(m, "symbol")
print io.get_slot(m, "arguments")
io.set_slot(m, "symbol", "bar")
print m
Wir haben jetzt alle Bausteine zusammen, um Nachrichten auszuwerten und damit Io-Programme ablaufen zu lassen. Wie der Interpreter funktioniert, zeige ich im nächsten Teil.

Stefan

PS: Frohes Neues Jahr!
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

4. Auswerten von Nachrichten

Ein Nachrichtenausdruck wie

Code: Alles auswählen

setSlot('a', List clone append(1)); a count
wird in einem Kontext ausgeführt. So ein Kontext kann eine Methode sein. Außerhalb jeder anderen Methode ist dies die Lobby, ein spezielles Objekt, dass der Io-Interpreter extra für diesen Zweck kennt.

Code: Alles auswählen

class Io...
    def __init__(io):
        """
        Initialize a new Io interpreter, setting up default objects and methods.
        """
        io.object = IoObject([])
        io.object.Object = io.object
        
        io.int_protos = [io.clone(io.object)]
        io.str_protos = [io.clone(io.object)]
        io.list_protos = [io.clone(io.object)]
        
        io.object.Lobby = io.clone(io.object)
        io.object.Lobby.self = io.object.Lobby
        ...
Io kennt keine (lokalen) Variablen, nur Slots und ein Kontext ist ein Objekt mit einem Slot für alle Methodenparameter und "lokalen Variablen". In dem Slot "self" steckt außerdem der Empfänger und ein "call"-Objekt, welches den Kontext des Senders (sender), die Nachricht in diesem Kontext (message), das Methodenobjekt (activated), das Objekt, in dem der Slot ist, in dem das Methodenobjekt steckte (slotContext) und schließlich den Empfänger (target) enthält.

Ich muss die Signatur für Methoden und Blöcke gegenüber Teil 3 nochmal korrigieren:

Code: Alles auswählen

class IoMethod(IoObject):
    def __call__(self, io, call):
        context = self.context(io, call)
        for name, message in zip(self.arguments, call.message.arguments):
            io.set_slot(context, name, io.evaluate(message, call.sender))
        return io.evaluate(self.message, context)
        
    def context(self, io, call):
        return io.clone(call.target, { 'self': call.target })

class IoBlock(IoMethod):
    def context(self, io, call):
        return io.clone(self.lexicalParent, {'self': self.lexicalParent.self})
Die Methode zum Auswerten einer Nachricht sieht jetzt so aus:

Code: Alles auswählen

class Io...
    def evaluate(io, message, context):
        """
        Evaluate the `message` which was sent from the `context` context and 
        return the result of the evaluation. If the message is a literal, the
        `context` is ignored and the literal from the message is returned or 
        taken as the new target for a chained message. Otherwise, the target is
        searched for a slot with the message's name and if the slot value is 
        callable (that is, a user defined method, block or builtin method), it
        is activated. Normal values are returned or taken as the new target for
        a chained message. If a chained message is ";", the previous target 
        object is thrown away and a new chain starts with the original target.
        If a chained message is "" (the empty message), the argument is simply
        evaluated and returned or taken as the target for a chained message.
        """
        target = context
        while message:
            if message.isliteral():
                target = message.symbol
            else:
                slot = io.find_slot(target, message.symbol)
                if not slot:
                    raise Exception("unknown %r in %r" % (message.symbol, target))
                value = slot.value
                if callable(value):
                    if hasattr(value, '__primitive__'):
                        args = [evaluate(io, a, context) for a in message.arguments]
                        target = value(io, target, *args)
                    else:
                        call = io.clone(io.object.Call, {
                            'sender': context,
                            'message': message,
                            'activated': value,
                            'slotContext': slot.target,
                            'target': target,
                        })
                        target = value(io, call)
                else:
                    target = value
            message = message.next
        return target
Damit das beschriebene Verhalten für ";" und "" funktioniert, brauche ich noch zwei eingebaute Methoden und muss diese im "Object" definieren:

Code: Alles auswählen

class Io...
    def __init__...
        ...
        setattr(io.object, "", Io.builtin_Object_empty)
        setattr(io.object, ";", Io.builtin_Object_semicolon)
        ...
    
    def builtin_Object_empty(io, call):
        return io.evaluate(call.message.arguments[0], call.sender)
    
    def builtin_Object_semicolon(io, call):
        return call.sender
Ich kann bereits jetzt das folgende Beispiel ausführen:

Code: Alles auswählen

io = Io()
print io.evaluate(parse("123"), io.object.Lobby) #>>123
io.object.Lobby.answer = 45
print io.evaluate(parse("Lobby answer"), io.object.Lobby) #>>45
Zugegeben unspektakulär, doch der Interpreter funktioniert prinzipiell und braucht jetzt "nur noch" mehr eingebaute Methoden.

Zum Abschluss möchte ich das an Anfang gezeigte Io-Programm ausführen.

Ich brauche dafür einige eingebaute Objekte und Methoden, etwa so:

Code: Alles auswählen

def evalargs(io, call):
    return [io.evaluate(msg, call.sender) for msg in call.message.arguments]

def builtin_Object_setSlot(io, call):
    args = evalargs(io, call)
    return io.set_slot(call.target, args[0], args[1])

def builtin_List_clone(io, call):
    return []
    
def builtin_List_append(io, call):
    args = evalargs(io, call)
    call.target.append(args[0])
    return call.target

def builtin_List_count(io, call):
    return len(call.target)

io = Io()
io.object.Object.setSlot = builtin_Object_setSlot
io.object.List = io.clone(io.object, {
    'clone': builtin_List_clone,
    'append': builtin_List_append,
    'count': builtin_List_count,
})
io.list_protos.append(io.object.List)
m = parse('setSlot("a", List clone append(1)); a count')
print io.evaluate(m, io.object.Lobby) #>> 1
Eingebaute Methoden zu definieren und aufzurufen ist noch recht umständlich. Dies zu vereinfachen soll das nächste Mal Thema sein.

Stefan
noise
User
Beiträge: 62
Registriert: Donnerstag 7. Februar 2008, 00:15

Hi, sehr interessante Sache. Ich wollte mal fragen ob du noch nen Teil machst, da du im letzten geschrieben das noch einer folgt, da die Methoden zu definieren und aufzurufen noch zu umständlich seinen.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Noise, ich hatte erstmal aufgehört, um zu schauen, ob das überhaupt jemanden interessiert. Daher freut mich deine Nachfrage und ich werde schauen, das Thema nochmals wieder aufzugreifen.

Stefan
Benutzeravatar
Craven
User
Beiträge: 223
Registriert: Dienstag 24. Januar 2006, 13:37

sma hat geschrieben:Noise, ich hatte erstmal aufgehört, um zu schauen, ob das überhaupt jemanden interessiert. Daher freut mich deine Nachfrage und ich werde schauen, das Thema nochmals wieder aufzugreifen.

Stefan
Wie siehts aus? :)
[code]q = 'q = %s; print q %% repr(q)'; print q % repr(q) [/code]
fred.reichbier
User
Beiträge: 155
Registriert: Freitag 29. Dezember 2006, 18:27

Ich finde die bisherigen Teile auch sehr interessant. :)
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Danke für das positive Feedback nach einem Jahr. Leider kommt die Forumssoftware nicht mit einem Tutorial, das mehrere Codeabschnitte enthält klar und braucht ewig, die Seite anzuzeigen. Daher muss ich mir erst eine andere Form überlegen.

Stefan
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Du könntest es ins Wiki auslagern, das hat auch Syntaxhighlighting.
Benutzeravatar
HerrHagen
User
Beiträge: 430
Registriert: Freitag 6. Juni 2008, 19:07

Du könntest es ins Wiki auslagern, das hat auch Syntaxhighlighting.
Gute Idee. So ein umfangreiches Beispiele würden das wiki echt bereichern.
Benutzeravatar
Blade Runner
User
Beiträge: 21
Registriert: Montag 23. Februar 2009, 11:41

Gibts da schon was, einen Wiki-Eintrag oder eine Fortsetzung?
[quote="Roy Batty"]All those moments will be lost in time, like tears in rain ... time to die.[/quote]
Antworten