git @ Cat's Eye Technologies Eqthy / master src / eqthy / parser.py
master

Tree @master (Download .tar.gz)

parser.py @masterraw · history · blame

# Copyright (c) 2022-2024 Chris Pressey, Cat's Eye Technologies
# This file is distributed under a 2-clause BSD license.  See LICENSES directory:
# SPDX-License-Identifier: LicenseRef-BSD-2-Clause-X-Eqthy

from typing import Union

from eqthy.objects import (
    Document, Axiom, Theorem, Step,
    Hint, Reflexivity, Substitution, Congruence, Reference
)
from eqthy.scanner import Scanner, EqthySyntaxError
from eqthy.terms import Term, Ctor, Variable, Eqn


# Document := {Axiom} {Theorem}.
# Axiom    := "axiom" [Name] Eqn.
# Theorem  := "theorem" [Name] Eqn "proof" {Step} "qed".
# Name     := "(" Ident ")".
# Step     := Eqn ["[" "by" Hint "]"].
# Hint     := "reflexivity"
#           | "substitution" "of" Term "into" Var
#           | "congruence" "of" Var "and" Term
#           | Ident ["on" ("LHS" | "RHS")] ["with" Subst {"," Subst}].
# Eqn      := Term "=" Term.
# Term     := Var | Ctor ["(" [Term {"," Term} ")"].


class Parser(object):
    def __init__(self, text: str, filename: str):
        self.scanner = Scanner(text, filename)
        self.axiom_count: int = 0
        self.theorem_count: int = 0

    def document(self) -> Document:
        axioms = []
        theorems = []
        while self.scanner.on('axiom'):
            axioms.append(self.axiom())
        while self.scanner.on('theorem'):
            theorems.append(self.theorem())
        if (not axioms) and (not theorems):
            raise EqthySyntaxError(
                self.scanner.filename,
                self.scanner.line_number,
                "Eqthy document is empty"
            )
        self.scanner.check_eof()
        return Document(axioms=axioms, theorems=theorems)

    def axiom(self) -> Axiom:
        self.scanner.expect('axiom')
        name = self.name()
        eqn = self.eqn()
        if not name:
            self.axiom_count += 1
            name = 'unnamed_axiom_{}'.format(self.axiom_count)
        return Axiom(name=name, eqn=eqn)

    def theorem(self) -> Theorem:
        self.scanner.expect('theorem')
        name = self.name()
        eqn = self.eqn()
        self.scanner.expect('proof')
        steps = []
        while not self.scanner.on('qed'):
            steps.append(self.step())
        self.scanner.expect('qed')
        if not name:
            self.theorem_count += 1
            name = 'unnamed_theorem_{}'.format(self.theorem_count)
        return Theorem(name=name, eqn=eqn, steps=steps)

    def name(self) -> Union[str, None]:
        if self.scanner.consume('('):
            ident = self.scanner.token
            self.scanner.scan()
            self.scanner.expect(')')
            return ident
        else:
            return None

    def step(self) -> Step:
        eqn = self.eqn()
        hint: Union[Hint, None] = None
        if self.scanner.consume('['):
            self.scanner.expect('by')
            hint = self.hint()
            self.scanner.expect(']')
        return Step(eqn=eqn, hint=hint)

    def hint(self) -> Hint:
        if self.scanner.consume('reflexivity'):
            return Reflexivity()
        elif self.scanner.consume('substitution'):
            self.scanner.expect('of')
            term = self.term()
            self.scanner.expect('into')
            variable = self.var()
            return Substitution(term=term, variable=variable)
        elif self.scanner.consume('congruence'):
            self.scanner.expect('of')
            variable = self.var()
            self.scanner.expect('and')
            term = self.term()
            return Congruence(term=term, variable=variable)
        else:
            name = self.scanner.token
            side = None
            substs = []
            self.scanner.scan()
            if self.scanner.consume('on'):
                if self.scanner.on('LHS') or self.scanner.on('RHS'):
                    side = self.scanner.token
                    self.scanner.scan()
                else:
                    self.scanner.syntax_error("Expected 'LHS' or 'RHS'")
            if self.scanner.consume('with'):
                substs.append(self.eqn())
                while self.scanner.consume(','):
                    substs.append(self.eqn())
            assert isinstance(name, str)
            return Reference(name=name, side=side, substs=substs)

    def eqn(self) -> Eqn:
        lhs = self.term()
        self.scanner.expect('=')
        rhs = self.term()
        return Eqn(lhs=lhs, rhs=rhs)

    def term(self) -> Term:
        name = self.scanner.token
        assert isinstance(name, str)
        self.scanner.scan()
        if name.isupper():
            return Variable(name=name)
        subterms = []
        if self.scanner.consume('('):
            while not self.scanner.on(')'):
                subterms.append(self.term())
                if not self.scanner.on(')'):
                    self.scanner.expect(',')
            self.scanner.expect(')')
        return Ctor(ctor=name, subterms=subterms)

    def var(self) -> Variable:
        var = self.term()
        if not isinstance(var, Variable):
            self.scanner.syntax_error("Expected variable")
        assert isinstance(var, Variable)
        return var