Ката – то вправа в східних бойових мистецтвах, набір рухів які треба повторювати поки не засвоїш досконально. Термін застосовується також і для неробочого, неспортивного, самостійного учбового програмування поза якимось проектом.
На моєму домашньому модемі відвалився DSL, то я вчора ввечері не мав чим зайнятись. (Хоча звісно є купа важливіших речей) Зате біля мене була роздруківка книжечки Ніклауса Вірта “Compiler Construction“, то я взявся перекладати його парсер EBNF що ілюструє метод рекурсивного спуску з Oberon на Python. Вийшло ніби незле:
# ebnf.py import re class Symbol(object): instances = {} def __init__(self, name, pattern): self.pattern = pattern self.regexp = re.compile(pattern) self.instances[name] = self def match(self, text): text = text.lstrip() m = self.regexp.match(text) if m: return m.group(), text[len(m.group()):] @classmethod def get_next(cls, text): if not text.strip(): return 'empty', '', '' for name, sym in cls.instances.items(): m = sym.match(text) if m: return name, m[0], m[1] raise UnknownSymbolError('Unknown symbol: %r' % text[:50] + ( '...' if len(text) > 50 else '' )) Symbol('ident', 'w+') Symbol('literal', '"[^"]*"') Symbol('eql', '=') Symbol('lparen', '(') Symbol('rparen', ')') Symbol('lbrak', '[') Symbol('rbrak', ']') Symbol('lbrace', '{') Symbol('rbrace', '}') Symbol('bar', '|') Symbol('period', '.') EBNF = ''' syntax = {production}. production = identifier "=" expression ".". expression = term {"|" term}. term = factor {factor}. factor = identifier | string | "(" expression ")" | "[" expression "]" | "{" expression "}". ''' def parse_ebnf(text): sym, sym_text, remaining = '', '', text def next(): nonlocal sym, sym_text, remaining sym, sym_text, remaining = Symbol.get_next(remaining) next() def syntax(): res = ['syntax'] while sym == 'ident': res.append(production()) return res def production(): name = sym_text next() if sym == 'eql': next() else: raise ExpectedEqualityError exp = expression() if sym == 'period': next() else: raise NoPeriodError return ['production', name, exp] def expression(): res = ['expression', term()] while sym == 'bar': next() res.append(term()) return res def term(): res = ['term'] while sym in ('ident', 'literal', 'lparen', 'lbrak', 'lbrace'): res.append(factor()) if len(res) == 1: raise NoFactorError return res def factor(): if sym == 'ident': res = [sym, sym_text] next() return res elif sym == 'literal': res = [sym, sym_text] next() return res elif sym == 'lparen': next() exp = expression() if sym == 'rparen': next() else: raise NoRParenError return ['(', exp] elif sym == 'lbrak': next() exp = expression() if sym == 'rbrak': next() else: raise NoRBrakError return ['[', exp] elif sym == 'lbrace': next() exp = expression() if sym == 'rbrace': next() else: raise NoRBraceError return ['{', exp] else: raise RuntimeError( "term should be called only when " "sym in ('ident', 'literal', 'lparen', 'lbrak', 'lbrace')" ) return syntax() class UnknownSymbolError(SyntaxError): pass class NoPeriodError(SyntaxError): pass class NoRParenError(SyntaxError): pass class NoRBrakError(SyntaxError): pass class NoRBraceError(SyntaxError): pass class NoFactorError(SyntaxError): pass class ExpectedEqualityError(SyntaxError): pass
Трохи TDD, трохи дописування тестів після:
# test.py from unittest import TestCase, main, skip from ebnf import Symbol from ebnf import parse_ebnf, EBNF from ebnf import print_tree from ebnf import ( NoPeriodError, NoRParenError, UnknownSymbolError, NoRBrakError, NoRBraceError, NoFactorError ) class TestSymbol(TestCase): def test_paren(self): self.assertEqual( Symbol.get_next('(asdf)'), ('lparen', '(', 'asdf)') ) def test_ident(self): self.assertEqual( Symbol.get_next('asdf'), ('ident', 'asdf', '') ) def test_bar(self): self.assertEqual( Symbol.get_next('|asdf'), ('bar', '|', 'asdf') ) class TestEBNF(TestCase): def test_self(self): tree = parse_ebnf(EBNF) self.assertEqual( parse_ebnf(EBNF), ['syntax', ['production', 'syntax', ['expression', ['term', ['{', ['expression', ['term', ['ident', 'production']]]]]]], ['production', 'production', ['expression', ['term', ['ident', 'identifier'], ['literal', '"="'], ['ident', 'expression'], ['literal', '"."']]]], ['production', 'expression', ['expression', ['term', ['ident', 'term'], ['{', ['expression', ['term', ['literal', '"|"'], ['ident', 'term']]]]]]], ['production', 'term', ['expression', ['term', ['ident', 'factor'], ['{', ['expression', ['term', ['ident', 'factor']]]]]]], ['production', 'factor', ['expression', ['term', ['ident', 'identifier']], ['term', ['ident', 'string']], ['term', ['literal', '"("'], ['ident', 'expression'], ['literal', '")"']], ['term', ['literal', '"["'], ['ident', 'expression'], ['literal', '"]"']], ['term', ['literal', '"{"'], ['ident', 'expression'], ['literal', '"}"']]]]] ) def test_without_period(self): with self.assertRaises(NoPeriodError): parse_ebnf('symbol = "asdf"') def test_without_rparen(self): with self.assertRaises(NoRParenError): parse_ebnf('symbol = ("asdf"') def test_without_rbrak(self): with self.assertRaises(NoRBrakError): parse_ebnf('symbol = ["asdf"') def test_without_rbrace(self): with self.assertRaises(NoRBraceError): parse_ebnf('symbol = {literal') def test_unknown_symbol(self): with self.assertRaises(UnknownSymbolError): parse_ebnf('!symbol = "asdf"') def test_no_factor(self): with self.assertRaises(NoFactorError): parse_ebnf('sym = ||') def test_for_binary(self): self.assertEqual( parse_ebnf(''' digit = "0"|"1". number = digit {digit}. '''), ['syntax', ['production', 'digit', ['expression', ['term', ['literal', '"0"']], ['term', ['literal', '"1"']]]], ['production', 'number', ['expression', ['term', ['ident', 'digit'], ['{', ['expression', ['term', ['ident', 'digit']]]]]]]] ) if __name__ == '__main__': main()
І я зрозумів дві речі: що найбільше на світі (якщо не враховувати сну) люблю писати код, і що таке множина . Якщо коротко, то це множина символів з яких може починатись рядок що виводиться з нетерміналу k. Наприклад:
digit = "0" | "1" number = digit {digit} first(number) = {"0", "1"}
І ми знаємо що парсер може прочитати не будь-яку граматику, а лише ту, в якої для кожного виразу на зразок
definition = exp1 | exp2
Виконується умова:
Таким чином отака граматика багатозначна:
sum = number | sum ("+" | "-") sum
Бо вираз 1 – 10 + 11 можна розпарсити як (1 – 10) + 11 або як 1 – (10 + 11). А все тому, що .
Її можна переписати так щоб в правилі для суми не було двох варіантів що починаються з однакових символів.
sum = number {("+" | "-") number}
Так вся сума розгортатиметься зразу і не буде проблем з тим яку операцію виконувати швидше.
Тепер би ще змусити його автоматично генерувати код парсера за EBNF і вийде власний YACC. :)
Filed under: Кодерство Tagged: Python