”Lispy BASIC” Hack — furchtbar & unvollstädig

Code-Stücke können hier veröffentlicht werden.
Antworten
Benutzeravatar
__blackjack__
User
Beiträge: 13997
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Inspiriert von der Entwicklung in CPython vs. Numba vs. Rust und meiner BASIC-Lösung von Projekt Euler #18 wollte ich mal sehen wie viel Code ich brauche um da einen Interpreter für zu schreiben.

Damit der Parser möglichst einfach wird, habe ich bei meinem BASIC-Dialekt die Zeilennummern weg gelassen und Klammern wie bei Lisp/Scheme eingeführt. Im Gegensatz zu Lisp/Scheme ist die Interpretation davon dann aber nicht so allgemein, sondern im Grunde bestehen die Anweisungen alle aus „special forms“, die in einem grossen ``match``-Konstrukt verarbeitet werden.

Als Literale Werte gibt es nur ganze Zahlen.

Die binären Rechenoperationen müssen alle geklammert werden; der Operator steht zwischen den Operanden.

Code: Alles auswählen

(PRINT (3 + 5))       ; Gibt 8 aus.
(PRINT ((3 + 5) - 1)) ; Gibt 7 aus.
Variablennamen stehen entweder für eine Zahl oder ein Array von Zahlen. Der gleiche Name kann für beides verwendet werden, die Skalare und Arrays haben jeweils einen eigenen Namensraum. Arrayzugrif besteht aus einer (Lisp-)Liste mit dem Arraynamen gefolgt von den Indizes.

Code: Alles auswählen

(PRINT 42)        ; Ausgabe eines literals.
(PRINT a)         ; Ausgabe des Wertes der Variablen `a`.
(PRINT (a 23 42)) ; Ausgabe eines Wertes aus dem 2D Array `a`.
Der Körper von FOR und IF darf nur aus einer Anweisung bestehen, darum gibt es die DO-Anweisung, die mehrere Anweisungen nacheinander ausführt.

Böse zusammengehackt, keine sinnvolle Fehlerbehandlung, und die Sprache nur so weit implementiert, wie es das Beispiel zur Ausführung benötigt (zum Beispiel fehlen die meisten Rechenoperationen):

Code: Alles auswählen

#!/usr/bin/env python3
import re
from pprint import pprint

from attrs import define, field


SOURCE = """\
; Read in the data.
(READ n)
(DIM (t n n) (o n n))
(FOR i = 1 TO n
  (FOR j = 1 TO i
    (DO
      (READ k)
      ((t i j) = k)
      ((o i j) = k))))

; Calculate the result.
(FOR i = n DOWNTO 2
  (FOR j = 1 TO (i - 1)
    (DO
      (k = j)
      (IF ((t i j) < (t i (j + 1))) THEN (k = (k + 1)))
      ((t (i - 1) j) = ((t (i - 1) j) + (t i k))))))

(PRINT (t 1 1))

(DATA 15)  ; Row count to follow.

; The triangle values.
(DATA 75)
(DATA 95 64)
(DATA 17 47 82)
(DATA 18 35 87 10)
(DATA 20 4 82 47 65)
(DATA 19 1 23 75 3 34)
(DATA 88 2 77 73 7 63 67)
(DATA 99 65 4 28 6 16 70 92)
(DATA 41 41 26 56 83 40 80 70 33)
(DATA 41 48 72 33 47 32 37 16 94 29)
(DATA 53 71 44 65 25 43 91 52 97 51 14)
(DATA 70 11 33 28 77 73 17 78 39 68 17 57)
(DATA 91 71 52 38 17 14 91 43 58 50 27 29 48)
(DATA 63 66 4 68 89 53 67 30 73 16 69 87 40 31)
(DATA 4 62 98 27 23 9 70 98 73 93 38 53 60 4 23)
"""

SCANNER = re.Scanner(
    [
        (r"\s+|;.*$", None),
        (r"\(", "("),
        (r"\)", ")"),
        (r"-?\d+", lambda _, text: int(text)),
        (r"\w+|[-+=<]", lambda _, text: text),
    ],
    re.MULTILINE,
)


def parse(text):
    tokens, text = SCANNER.scan(text)
    if text:
        raise ValueError(f"expected end of source text, found:\n{text}")

    stack = []
    statement = ["DO"]  # Wrap the whole program in a DO block.
    for token in tokens:
        if token == "(":
            stack.append(statement)
            statement = []

        elif token == ")":
            if not stack:
                raise ValueError("unexpected closing parenthesis")
            stack[-1].append(statement)
            statement = stack.pop()

        else:
            statement.append(token)

    if stack:
        raise ValueError(f"{len(stack)} unclosed parenthesis")

    assert statement and statement[0] == "DO"
    #
    # If the DO block contains exactly one statement we can do without the DO.
    #
    return statement[0] if len(statement) == 1 else statement


def filter_data(program):
    program_without_data = []
    data = []
    for statement in program:
        match statement:
            case "DATA", *values:
                data.extend(values)
            case _:
                program_without_data.append(statement)

    return program_without_data, data


@define
class Context:
    data_values = field(factory=lambda: iter([]))
    scalars = field(factory=dict)
    arrays = field(factory=dict)


def execute(context, program):
    match program:
        case "DO", *statements:
            for statement in statements:
                execute(context, statement)

        case "DIM", *arrays:
            for name, *dimensions in arrays:
                dimensions = (
                    execute(context, dimension) + 1
                    for dimension in reversed(dimensions)
                )
                array = [0] * next(dimensions)
                for dimension in dimensions:
                    array = [array.copy() for _ in range(dimension)]
                context.arrays[name] = array

        case (
            "FOR",
            name,
            "=",
            start,
            "TO" | "DOWNTO" as direction,
            end,
            statement,
        ):
            context.scalars[name] = execute(context, start)
            end_value = execute(context, end)
            step_value = 1 if direction == "TO" else -1
            while True:
                execute(context, statement)
                value = context.scalars[name] + step_value
                if (
                    step_value > 0
                    and value > end_value
                    or step_value < 0
                    and value < end_value
                ):
                    break
                context.scalars[name] = value

        case "IF", condition, "THEN", statement:
            if execute(context, condition):
                execute(context, statement)

        case "PRINT", *statements:
            print(
                " ".join(
                    str(execute(context, statement))
                    for statement in statements
                )
            )

        case "READ", name:
            context.scalars[name] = next(context.data_values)

        case [name, *indices], "=", expression:
            array = context.arrays[name]
            for index in indices[:-1]:
                array = array[execute(context, index)]
            array[execute(context, indices[-1])] = execute(context, expression)

        case name, "=", expression:
            context.scalars[name] = execute(context, expression)

        case a, "+", b:
            return execute(context, a) + execute(context, b)

        case a, "-", b:
            return execute(context, a) - execute(context, b)

        case a, "<", b:
            return execute(context, a) < execute(context, b)

        case name, *indices:
            result = context.arrays[name]
            for index in indices:
                result = result[execute(context, index)]
            return result

        case str(name):
            return context.scalars[name]

        case int(value):
            return value

        case _:
            raise ValueError(f"can not execute {program!r}")


def main():
    program = parse(SOURCE)
    pprint(program, indent=2, compact=True)
    program, data = filter_data(program)
    context = Context(iter(data))
    execute(context, program)


if __name__ == "__main__":
    main()
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Antworten