100 языков спидран: Эпизод 78: Лучший интерпретатор Whenever с помощью Python и SLY

Некоторое время назад я рассказывал о Whenever — языке, в котором программа представляет собой список дел. В Whenever нет ни функций, ни переменных, а состояние — это просто список дел. Пока в нем что-то есть, он выбирает элемент наугад и выполняет его.

Для этого эпизода я хочу создать улучшенную версию языка Whenever. Он в основном обратно совместим с оригиналом и может выполнять все программы Whenever, которые я смог найти. Однако поведение для крайних случаев нигде не документировано, а абсолютная совместимость не является целью.

Я сделал следующие изменения, чтобы сделать его более удобным, сохранив при этом уровень сложности таким же высоким, как и раньше:

  • элементы todo могут быть идентифицированы по имени, а не только по номеру строки
  • пункты todo не обязательно должны быть расположены в определенном порядке
  • разрешены пустые списки
  • разрешены комментарии с //
  • точки с запятой необязательны
  • вы можете смешивать действия print и действия счетчика в одном todo
  • если вы назовете один из элементов todo как start, программа начнется с него как с единственного активного действия в списке todo
  • использует более умную модель выполнения, поэтому может выполнять более амбициозные программы, чем оригинальный интерпретатор
  • он обнаруживает некоторые бесконечные циклы, если ни одно действие не может произойти, и выбрасывает ошибку
  • использует целые числа бесконечной точности
  • Я не стал добавлять forget, так как он внесен в список устаревших.

Как это работает

Здесь много кода, и он только немного прокомментирован, так что вот общий обзор:

  • вы можете запустить его с помощью whenever.py whenever_file.txt
  • лексер и парсер реализованы с помощью SLY, я недавно писал об этом, посмотрите, если вам нужна помощь в понимании того, как работает SLY.

Я буду вдаваться в подробности, когда буду разбираться с каждым из этих файлов.

Примеры лучших программ

Я включил все примеры из предыдущего эпизода, а также все примеры из официальной документации, но поскольку я хочу улучшить язык, я также включил еще несколько.

math.txt, просто быстрый тестовый скрипт для проверки того, что операторы имеют правильный приоритет:

a print(300 + 50 * 4 + 80 / 4 - (80 - 30) * 2)
Войти в полноэкранный режим Выход из полноэкранного режима

fizzbuzz.txt, простая программа, упрощенная для использования новых возможностей, таких как комментарии, отсутствие запятых и start, но все еще использующая числа для элементов todo:

// start
start 10,11
// variables
2 2
3 3
4 4
// printing
5 defer((N(6) + N(7) + N(8) + N(9)) != 3) 6#-N(6),7#-N(7),8#-N(8),9#-N(9)
6 defer(N(3) == 0 || N(4) == 0) print(N(2))
7 defer(3 || N(4) == 0) print("Fizz")
8 defer(N(3) == 0 || 4) print("Buzz")
9 defer(3 || 4) print("FizzBuzz")
// loop logic
10 defer(5 || N(2) >= 100) 2,3,-3#((N(3)/3)*3),4,-4#((N(4)/5)*5),5,6,7,8,9,10
11 defer(5 || N(2) < 100) -2#N(2),-3#N(3),-4#N(4),-10#N(10)
Вход в полноэкранный режим Выход из полноэкранного режима

fizzbuzz2.txt, то же самое, но теперь все пункты названы по имени, и используется !:

// start
start loop,loopend
// variables
a a
t t
f f
// printing
prn defer((N(pn) + N(pf) + N(pb) + N(pfb)) != 3) -pn#N(pn),-pf#N(pf),-pb#N(pb),-pfb#N(pfb)
pn defer(!t || !f) print(N(a))
pf defer(t || !f) print("Fizz")
pb defer(!t || f) print("Buzz")
pfb defer(t || f) print("FizzBuzz")
// loop logic
loop defer(prn || N(a) >= 100) a,t,-t#((N(t)/3)*3),f,-f#((N(f)/5)*5),prn,pn,pf,pb,pfb,loop
loopend defer(prn || N(a) < 100) -a#N(a),-t#N(t),-f#N(f),-loop#N(loop)
Войти в полноэкранный режим Выход из полноэкранного режима

WheneverLexer.py

Это самый простой файл. Возможным улучшением может быть поддержка кодов, таких как n в строках (но тогда print должен быть изменен, чтобы остановить автоматическую печать n), и, возможно, некоторые дополнительные функции, такие как read или forget. Я также не потрудился отслеживать номера строк, поэтому сообщения об ошибках будут не очень хорошими.

from sly import Lexer

class WheneverLexer(Lexer):
  tokens = { PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN, COMMA, HASH, REL, NUM, ID, EOS, STRING, AND, OR, NOT, PRINT, DEFER, AGAIN, N, TRUE, FALSE, MOD }

  ignore = " tr"
  EOS = r"(n|;|//[^n]*n)+"
  PLUS = r"+"
  MINUS = r"-"
  TIMES = r"*"
  DIVIDE = r"/"
  MOD = r"%"
  LPAREN = r"("
  RPAREN = r")"
  COMMA = ","
  HASH = "#"
  REL = r"<=|<|>=|>|==|!="
  OR = r"||"
  AND = "&&"
  NOT = "!"
  STRING = r'"[^"]*"'
  NUM = r"[0-9]+"
  ID = r"[a-zA-Z_][a-zA-Z0-9_]*"

  ID['again'] = AGAIN
  ID['defer'] = DEFER
  ID['false'] = FALSE
  ID['N'] = N
  ID['print'] = PRINT
  ID['true'] = TRUE

  def NUM(self, t):
    t.value = int(t.value)
    return t

  def STRING(self, t):
    t.value = t.value[1:-1]
    return t
Вход в полноэкранный режим Выход из полноэкранного режима

SLY позволяет очень просто определять лексеры.

WheneverParser.py.

Парсер длинный, но не очень сложный. Для AST я просто использую словари Python, и я использую явные правила типа exprm вместо таблиц прецедентов, так как я больше привык к такому стилю. SLY по умолчанию имеет очень странную обработку ошибок (печатает ошибку на stderr, ничего не делает), поэтому нам нужно переопределить ее, чтобы бросать правильные исключения. Я использовал те же сообщения, которые будут выводиться на печать.

Для простоты я не делаю различий между идентификатором (в основном символами) и строками, так что вы можете сделать N("loop") или print(Fizz), хотя на самом деле это не должно поддерживаться. На данный момент это в основном безвредно.

Большая часть синтаксиса достаточно очевидна. Одна несколько необычная вещь, которую я сделал, это явное запрещение множественного сравнения на одном уровне без круглых скобок, например (1 == 2 == 3 или 1 < 2 == 3 < 4), так как это просто очень запутывает.

from sly import Parser
from WheneverLexer import WheneverLexer

class WheneverParser(Parser):
  tokens = WheneverLexer.tokens

  @_("")
  def program(self, p):
    return {}

  @_("EOS program")
  def program(self, p):
    return p.program

  @_("todo program")
  def program(self, p):
    program = p.program
    id, todo = p.todo
    if id in program:
      raise Exception(f"Duplicate todo id: {id}")
    program[id] = todo
    return program

  @_("id modifiers actions EOS")
  def todo(self, p):
    again = []
    defer = []
    for mod in p.modifiers:
      if "again" in mod:
        again.append(mod["again"])
      if "defer" in mod:
        defer.append(mod["defer"])
    return (p.id, {"again": again, "defer": defer, "actions": p.actions})

  @_("")
  def modifiers(self, p):
    return []

  @_("modifier modifiers")
  def modifiers(self, p):
    return [p.modifier] + p.modifiers

  @_("DEFER LPAREN expr RPAREN")
  def modifier(self, p):
    return {"defer": p.expr}

  @_("AGAIN LPAREN expr RPAREN")
  def modifier(self, p):
    return {"again": p.expr}

  @_("action")
  def actions(self, p):
    return [p.action]

  @_("action COMMA actions")
  def actions(self, p):
    return [p.action] + p.actions

  @_("id")
  def action(self, p):
    return {"action": "change", "id": p.id, "count": {"value": 1}}

  @_("MINUS id")
  def action(self, p):
    return {"action": "change", "id": p.id, "count": {"value": -1}}

  @_("id HASH expr")
  def action(self, p):
    return {"action": "change", "id": p.id, "count": p.expr}

  @_("MINUS id HASH expr")
  def action(self, p):
    return {"action": "change", "id": p.id, "count": {"op": "uminus", "a": p.expr}}

  @_("PRINT LPAREN expr RPAREN")
  def action(self, p):
    return {"action": "print", "arg": p.expr}

  # expr, going through all the priority levels
  @_("exprlo")
  def expr(self, p):
    return p.exprlo

  ### expr logical or
  @_("exprla")
  def exprlo(self, p):
    return p.exprla

  @_("exprlo OR exprla")
  def exprlo(self, p):
    return {"op": "||", "a": p.exprlo, "b": p.exprla}

  ### expr logical and
  @_("exprr")
  def exprla(self, p):
    return p.exprr

  @_("exprla AND exprr")
  def exprla(self, p):
    return {"op": "&&", "a": p.exprla, "b": p.exprr}

  ### expr relational
  ### do not support chaining them without parentheses
  @_("expra")
  def exprr(self, p):
    return p.expra

  @_("expra REL expra")
  def exprr(self, p):
    return {"op": p.REL, "a": p.expra0, "b": p.expra1}

  ### expr additive
  @_("expra PLUS exprm")
  def expra(self, p):
    return {"op": "+", "a": p.expra, "b": p.exprm}

  @_("expra MINUS exprm")
  def expra(self, p):
    return {"op": "-", "a": p.expra, "b": p.exprm}

  @_("exprm")
  def expra(self, p):
    return p.exprm

  ### expr multiplicative
  @_("exprm TIMES expru")
  def exprm(self, p):
    return {"op": "*", "a": p.exprm, "b": p.expru}

  @_("exprm DIVIDE expru")
  def exprm(self, p):
    return {"op": "/", "a": p.exprm, "b": p.expru}

  @_("exprm MOD expru")
  def exprm(self, p):
    return {"op": "%", "a": p.exprm, "b": p.expru}

  #### expr unary
  @_("expru")
  def exprm(self, p):
    return p.expru

  @_("value")
  def expru(self, p):
    return p.value

  @_("NOT expru")
  def expru(self, p):
    return {"op": "!", "a": p.expru}

  @_("MINUS expru")
  def expru(self, p):
    return {"op": "uminus", "a": p.expru}

  ### primitive value
  @_("LPAREN expr RPAREN")
  def value(self, p):
    return p.expr

  # No reason to distinguish between foo and "foo" yet
  @_("ID")
  def value(self, p):
    return {"value": p.ID}

  @_("NUM")
  def value(self, p):
    return {"value": p.NUM}

  @_("N LPAREN id RPAREN")
  def value(self, p):
    return {"N": p.id}

  @_("STRING")
  def value(self, p):
    return {"value": p.STRING}

  @_("TRUE")
  def value(self, p):
    return {"value": True}

  @_("FALSE")
  def value(self, p):
    return {"value": False}

  @_("ID")
  def id(self, p):
    return p.ID

  @_("NUM")
  def id(self, p):
    return p.NUM

  def error(self, token):
    if token:
      lineno = getattr(token, "lineno", 0)
      if lineno:
        raise Exception(f"sly: Syntax error at line {lineno}, token={token.type}")
      else:
        raise Exception(f"sly: Syntax error, token={token.type}")
    else:
        raise Exception("sly: Parse error in input. EOF")
Вход в полноэкранный режим Выход из полноэкранного режима

WheneverEval.py

Оценка выражений включает в себя так много кода, что я вынес ее в отдельный файл. Whenever имеет необычные коэрцитивы типов — например, 3 || !4 означает N(3) > 0 || !(N(4) > 0). Whenever также преобразует все в строку, если добавить к строке, и преобразует строки (только начальные цифры, или 0, если нет начальных цифр) и булевы в инты, если они используются в контексте int.

Операторы сравнения >, <, >=, <=, == и != также преобразуются в целые числа. Whenever не делает ничего полезного со строками, кроме их печати, поэтому в любом случае нет смысла делать "foo" == "bar".

Такие вещи, как коэрцитивность типов, можно улучшить, но это будет стоить обратной совместимости.

class WheneverEval:
  def __init__(self, program):
    self.program = program

  def int_eval_node(self, node):
    a = self.int_eval(node["a"])
    b = self.int_eval(node["b"])
    return (a, b)

  def int_eval(self, node):
    return self.int(self.eval(node))

  def bool_eval(self, node):
    return self.bool(self.eval(node))

  def str_eval(self, node):
    return self.str(self.eval(node))

  # A lot of aggressive casting to int here
  def eval(self, node):
    if "value" in node:
      return node["value"]
    if "N" in node:
      return self.program.counters[node["N"]]
    if "op" not in node:
      raise Exception(f"Neither op nor value? {node}")
    op = node["op"]
    if op == "-":
      (a, b) = self.int_eval_node(node)
      return a - b
    if op == "*":
      (a, b) = self.int_eval_node(node)
      return a * b
    if op == "/":
      (a, b) = self.int_eval_node(node)
      return a // b
    if op == "%":
      (a, b) = self.int_eval_node(node)
      return a % b
    # Not even clear about this one:
    if op == "+":
      a = self.eval(node["a"])
      b = self.eval(node["b"])
      if isinstance(a, str) or isinstance(b, str):
        return self.str(a) + self.str(b)
      else:
        return self.int(a) + self.int(b)
    if op == "uminus":
      a = self.int_eval(node["a"])
      return -a
    if op == "||":
      a = self.bool_eval(node["a"])
      b = self.bool_eval(node["b"])
      return a or b
    if op == "&&":
      a = self.bool_eval(node["a"])
      b = self.bool_eval(node["b"])
      return a and b
    if op == "!":
      a = self.bool_eval(node["a"])
      return not a
    if op == "<":
      (a, b) = self.int_eval_node(node)
      return a < b
    if op == "<=":
      (a, b) = self.int_eval_node(node)
      return a <= b
    if op == ">":
      (a, b) = self.int_eval_node(node)
      return a > b
    if op == ">=":
      (a, b) = self.int_eval_node(node)
      return a >= b
    if op == "==":
      (a, b) = self.int_eval_node(node)
      return a == b
    if op == "!=":
      (a, b) = self.int_eval_node(node)
      return a != b
    raise Exception(f"TODO: Operation {op} not supported yet")

  def int(self, value):
    if isinstance(value, bool):
      if value == True:
        return 1
      if value == False:
        return 0
    if isinstance(value, int):
      return value
    if isinstance(value, str):
      if re.match(r"^-?d+", value):
        return int(value)
      else:
        return 0
    raise Exception(f"Invalid type {value}")

  def bool(self, value):
    if isinstance(value, bool):
      return value
    return self.program.counters[value] > 0

  def str(self, value):
    if isinstance(value, bool):
      if value == True:
        return "true"
      if value == False:
        return "false"
    if isinstance(value, str):
      return value
    if isinstance(value, int):
      return str(value)
    raise Exception(f"Invalid type {value}")
Вход в полноэкранный режим Выход из полноэкранного режима

whenever.py

И, наконец, основная программа.

Основной цикл выполнения выглядит следующим образом:

  • если есть символ start, начинаем со счетчиков {start: 1, evertyhing_else: 0}. В противном случае дайте всему начальный счетчик, равный 1.
  • Пометьте любое правило типа n n как тривиальное — они ничего не делают при выполнении, нам все еще нужно отслеживать их значение, но нам никогда не нужно их выполнять, так как состояние программы одинаково до и после их выполнения (мы можем игнорировать defer здесь и т.д. для большей оптимизации).
  • пока существует нетривиальное правило с ненулевым счетчиком, выбирайте что-нибудь наугад при его выполнении.

Тривиальный алгоритм выбора следующего todo приведет к чрезвычайно медленному выполнению. Например, fib из официальной документации выполняется настолько медленно, что никогда не завершается. С этим улучшенным интерпретатором он завершается практически мгновенно.

  • каждый раз мы копируем список всех нетривиальных правил как actionable
  • мы выбираем случайное правило из числа actionable
  • проверяем, является ли правило отложенным, если да, то удаляем его из набора actionable и пробуем снова
  • если программа не завершена, но все правила тривиальны или отложены, мы выдаем исключение, а не делаем бесконечный цикл.

Это означает, что если у нас такая ситуация:

  • N(1) — 17167680177566 раз, отложено while N(3) > 0
  • N(2) — 27777890035289 раз, отложено, пока N(3) > 0
  • N(3) — 1 раз, выполнимо

Нам не нужно повторять 17167680177566+27777890035289+1 раз, пока мы не выполним пункт 3.

Алгоритм ленивый, поэтому мы проверяем отложенное правило, только если мы его прокрутили, так что обычно это не должно нас замедлять. Интерпретатор все еще не очень оптимизирован, и для него определенно можно писать очень медленные программы, но, по крайней мере, он позволяет избежать экспоненциального замедления для таких простых программ.

#!/usr/bin/env python3

import sys
from WheneverLexer import WheneverLexer
from WheneverParser import WheneverParser
from WheneverEval import WheneverEval
from random import randint

class WheneverProgram:
  def __init__(self, path):
    self.path = path
    with open(path) as f:
      self.text = f.read()
    lexer = WheneverLexer()
    parser = WheneverParser()
    self.program = parser.parse(lexer.tokenize(self.text))
    self.eval = WheneverEval(self)

  # A lot more optimizations are possible
  # total is the sum of all nontrivial counters
  def init_counters(self):
    self.total = 0
    self.trivial = set()
    self.nontrivial = set()
    self.counters = {}
    has_start = ("start" in self.program)

    for id in self.program.keys():
      if id == "start" or not has_start:
        starting_value = 1
      else:
        starting_value = 0

      self.counters[id] = starting_value

      is_trivial = (self.program[id] == {'again': [], 'defer': [], 'actions': [{'action': 'change', 'id': id, 'count': {'value': 1}}]})
      if is_trivial:
        self.trivial.add(id)
      else:
        self.nontrivial.add(id)
        self.total += starting_value

  def is_deferred(self, id):
    for expr in self.program[id]["defer"]:
      if self.eval.bool_eval(expr):
        return True
    return False

  def random_todo(self, actionable, actionable_total):
    i = randint(0, actionable_total - 1)
    for id in actionable:
      i -= self.counters[id]
      if i < 0:
        return id
    raise Exception("Math failure")

  def execute_random_todo(self):
    actionable = self.nontrivial.copy()
    actionable_total = self.total

    while True:
      if actionable_total == 0:
        raise Exception("All actions are deferred")
      id = self.random_todo(actionable, actionable_total)

      if self.is_deferred(id):
        actionable.remove(id)
        actionable_total -= self.counters[id]
      else:
        self.execute_todo(id)
        return

  # defer is checked before we call this
  def execute_todo(self, id):
    todo = self.program[id]
    again = todo["again"]
    remove_todo = True
    for expr in todo["defer"]:
      if self.eval.bool_eval(expr):
        remove_todo = False
    for action in todo["actions"]:
      self.execute_action(action)
    if remove_todo:
      self.update_counter(id, -1)

  def execute_action(self, action):
    action_type = action["action"]
    if action_type == "print":
      s = self.eval.str_eval(action["arg"])
      print(s)
    elif action_type == "change":
      id = action["id"]
      count = self.eval.int_eval(action["count"])
      self.update_counter(id, count)
    else:
      raise Exception(f"Unknown action: {action_type}")

  def update_counter(self, id, change):
    old_value = self.counters[id]
    new_value = old_value + change
    if new_value < 0:
      new_value = 0
    self.counters[id] = new_value
    if id not in self.trivial:
      self.total += (new_value - old_value)

  def run(self):
    self.init_counters()
    while self.total > 0:
      self.execute_random_todo()
    if sum(self.counters.values()) != 0:
      raise Exception("Program entered do-nothing infinite loop")

program = WheneverProgram(sys.argv[1])
program.run()
Вход в полноэкранный режим Выход из полноэкранного режима

Стоит ли вам попробовать Better Whenever?

Я думаю, что улучшенная версия намного более дружелюбна, поскольку такие вещи, как комментарии, имена переменных, правило start просто делают весь процесс кодирования более приятным, и вы можете сосредоточиться на выполнении безумных вещей на языке с эзотерической моделью выполнения без всех этих хлопот по преобразованию всего в числа.

Также определенно помогает то, что этот интерпретатор позволяет избежать многих экспоненциальных замедлений.

Как и все остальное в этой серии, я взломал эту программу за один вечер, поэтому она не идеальна. Если вы заинтересованы в ее дальнейшем улучшении, дайте мне знать, и я смогу создать для нее соответствующий репозиторий, документацию, спецификации и т.д.

Этот интерпретатор очень точно следует оригинальной идее, но с идеей «программа как список дел» определенно можно сделать что-то более творческое, так же как AsciiDots действительно развил идею Befunge.

Код

Все примеры кода для этой серии будут находиться в этом репозитории.

Код для эпизода Better Whenever Interpreter with Python and SLY доступен здесь.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *