Вы находитесь на странице: 1из 22

Лекция 10. Примеры на ML II.

Синтаксический анализ

Введение в Функциональное программирование

Джон Харрисон

Университет Кембриджа

10 сентября 2008 г.

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Темы

Задача синтаксического анализа;


Метод рекурсивного спуска;
Реализация синтаксических анализаторов на языке ML;
Комбинаторы синтаксического анализа;
Эффективность и ограничения.

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Грамматика языка термов


Рассмотрим язык термов — выражений, в которых участвуют лишь две
инфиксные операции: + и *, а также числовые константы и переменные с
алфавитно-цифровыми именами. Мы хотим реализовать синтаксический
анализатор, чтобы не задавать выражения в виде композиции конструкторов
типов.

term −→ name(termlist)
| name
| (term)
| numeral
| −term
| term+term
| term∗term
termlist −→ term,termlist
| term

Таким образом, грамматика языка термов определяется множеством


продукций.
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Неоднозначность
Задача синтаксического анализа (разбора), в целом, является
обратной рассмотренной ранее задаче составления грамматики.
Цель синтаксического анализа — найти последовательность
применения продукций, порождающую данную строку.
К сожалению, наша грамматика является неоднозначной, поскольку
некоторые строки могут порождаться различными путями, например

term −→ term+term
−→ term+term∗term

term −→ term∗term
−→ term+term∗term

Различный порядок применения продукций соответствует


различным «деревьям разбора». На самом деле, мы вправе
трактовать x+y ∗z как x+(y ∗z) либо как (x+y )∗z.
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Назначение приоритетов
Приоритеты операций могут быть выражены путём введения
дополнительных категорий, например

atom −→ name(termlist)
| name
| numeral
| (term)
| −atom
mulexp −→ atom∗mulexp
| atom
term −→ mulexp+term
| mulexp
termlist −→ term,termlist
| term

Такая грамматика однозначна. Операция умножения имеет больший


приоритет, чем сложение; обе операции правоассоциативны.

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Метод рекурсивного спуска

Синтаксический анализатор, реализующий метод рекурсивного


спуска, представляет собой множество взаимно рекурсивных
функций, прямо сопоставленных каждой синтаксической категории
(term, mulexp и т. д.).
Структура рекурсивных зависимостей при этом соответствует
структуре грамматики.
Это соответствие обеспечивает достаточно лёгкий и естественный
способ реализации подобных анализаторов, особенно на языке ML,
для которого рекурсия представляет собой основной механизм
управления порядком действий.
Например, процедура анализа термов term при появлении во
входном потоке символа - будет рекурсивно вызывать себя для
анализа аргумента операции, а обнаружив имя, за которым следует
открывающая скобка, осуществит вызов termlist. В свою очередь,
эта процедура по крайней мере один раз вызовет term, и так далее.

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Реализация синтаксического анализа на языке ML

Предположим, что синтаксический анализатор принимает список


входных символов или лексем (токенов) произвольного типа.
Результатом его работы является значение некоторого
произвольного типа, а также список входных объектов, которые не
были ещё обработаны. Следовательно, тип анализатора

(α)list → β × (α)list

Например, для заданной входной строки символов (x + y) * z


функция atom обработает подстроку (x + y) и вернёт
нераспознанные символы * z. Также она может вернуть дерево
разбора обработанной подстроки в виде значения рекурсивного типа,
определённого ранее. Таким образом, мы в итоге получим
atom " ( x ␣+␣ y ) ␣∗␣ z " =
Fn ( "+" , [ Var " x " ; Var " y " ] ) , " ∗␣ z "

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Комбинаторы синтаксического анализа


Используя язык ML, мы можем определить набор комбинаторов, при
помощи которых будем совместно использовать различные
синтаксические анализаторы и создавать новые на базе уже имеющихся.
Определив некоторые из них как инфиксные операции, мы в состоянии
придать программе синтаксического анализа вид, весьма схожий со
структурой самой грамматики.
Для начала введём исключение, сигнализирующее об ошибках анализа:
exception Noparse ; ;

Далее определим операцию p1 ++ p2, которая вначале применяет p1,


затем — p2 к оставшимся лексемам; а также many, применяющую
заданный анализатор максимально возможное количество раз.
p » f работает аналогично p, но затем применяет f к результату
анализа.
p1 || p2 делает попытку применить p1, а затем, в случае ошибки — p2.
Все эти операции автоматически считаются инфиксными с
приоритетами по убыванию.

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Определение комбинаторов
l e t p r e f i x ++ p a r s e r 1 p a r s e r 2 i n p u t =
let result1 , rest1 = parser1 input in
let result2 , rest2 = parser2 rest1 in
( result1 , result2 ) , rest2 ; ;

l e t r e c many p a r s e r i n p u t =
try let r e s u l t , next = parser input in
l e t r e s u l t s , r e s t = many p a r s e r n e x t i n
( result :: results ) , rest
with N o p a r s e −> [ ] , i n p u t ; ;

l e t p r e f i x >> p a r s e r t r e a t m e n t i n p u t =
let result , r e s t = parser input in
treatment ( r e s u l t ) , r e s t ; ;

let p r e f i x | | parser1 parser2 input =


try parser1 input
with N o p a r s e −> p a r s e r 2 i n p u t ; ;

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Вспомогательные функции
Введём следующие универсальные функции, которыми
воспользуемся в дальнейшем:
l e t rec i t l i s t f =
fun [ ] b −> b
| ( h : : t ) b −> f h ( i t l i s t f t b);;

let uncurry f (x , y ) = f x y ; ;
let K x y = x ; ;
let C f x y = f y x ; ;

let o f g x = f (g x ) ; ;
#i n f i x " o " ; ;

let explode s =
l e t r e c ex ap n l =
i f n < 0 then l e l s e
exap ( n − 1 ) ( ( s u b _ s t r i n g s n 1 ) : : l ) i n
exap ( s t r i n g _ l e n g t h s − 1 ) [ ] ; ;
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Элементарные анализаторы
Для начала, определим несколько примитивных синтаксических
анализаторов:
l e t some p =
fun [ ] −> r a i s e N o p a r s e
| ( h : : t ) −> i f p h then ( h , t )
e l s e r a i s e Noparse ; ;

l e t a tok =
some ( fun i t e m −> i t e m = t o k ) ; ;

let finished input =


i f i n p u t = [ ] then 0 , i n p u t
e l s e r a i s e Noparse ; ;

Первые два принимают очередной символ входной


последовательности, если он удовлетворяет условию p или равен tok
соответственно. Третий примитив гарантирует, что вся входная
последовательность была обработана.
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Лексический анализ
Для начала нам потребуется провести лексический анализ, т. е.
разбить входную последовательность символов на лексемы (токены).
Это также возможно сделать, используя наши комбинаторы в
сочетании с несколькими функциями классификации символов.
Прежде всего, определим тип, представляющий лексемы:
type t o k e n = Name o f s t r i n g
| Num o f s t r i n g
| Other of s t r i n g ; ;

Нам требуется, чтобы лексический анализатор принимал строку и


возвращал соответствующий ей список лексем, игнорируя пробелы,
например:
#l e x " s i n ( x ␣+␣ y ) ␣∗␣ c o s ( 2 ␣ ∗␣ x ␣+␣ y ) " ; ;
− : token l i s t =
[ Name " s i n " ; O t h e r " ( " ; Name " x " ; O t h e r "+" ;
Name " y " ; O t h e r " ) " ; O t h e r " ∗" ; Name " c o s " ;
Ot h e r " ( " ; Num " 2 " ; O t h e r "∗" ; Name " x " ;
Ot h e r "+" ; Name " y " ; O t h e r " ) " ]
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Реализация лексического анализатора


let lex =
l e t s e v e r a l p = many ( some p ) i n
l e t l o w e r c a s e _ l e t t e r s = " a " <= s & s <= " z " i n
l e t u p p e r c a s e _ l e t t e r s = "A" <= s & s <= "Z" i n
let letter s =
l o w e r c a s e _ l e t t e r s or uppercase_letter s in
l e t a l p h a s = l e t t e r s o r s = "_" o r s = " ’ " i n
l e t d i g i t s = " 0 " <= s & s <= " 9" i n
l e t alphanum s = a l p h a s o r d i g i t s i n
l e t s p a c e s = s = " ␣ " o r s = " \n" o r s = "\ t " i n
l e t c o l l e c t ( h , t ) = h ^( i t l i s t ( p r e f i x ^) t " " ) i n
l e t rawname =
some a l p h a ++ s e v e r a l alphanum
>> ( Name o c o l l e c t ) i n
l e t rawnumeral =
some d i g i t ++ s e v e r a l d i g i t
>> (Num o c o l l e c t ) i n
l e t r a w o t h e r = some (K t r u e ) >> Oth er i n
l e t token =
( rawname | | r a w n u m e r a l | | r a w o t h e r ) ++
s e v e r a l s p a c e >> f s t i n
l e t t o k e n s = ( s e v e r a l s p a c e ++ many t o k e n ) >> snd i n
l e t a l l t o k e n s = ( t o k e n s ++ f i n i s h e d ) >> f s t i n
f s t o a ll t ok e ns o explode ; ;

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Анализатор термов
Реализацию анализа языка термов начнем с определения базовых
анализаторов отдельных лексем заданного класса:
l e t name =
fun ( Name s : : r e s t ) −> s , r e s t
| _ −> r a i s e N o p a r s e ; ;

l e t numeral =
fun (Num s : : r e s t ) −> s , r e s t
| _ −> r a i s e N o p a r s e ; ;

let other =
fun ( O t h e r s : : r e s t ) −> s , r e s t
| _ −> r a i s e N o p a r s e ; ;

С помощью этих функций мы можем определить анализатор термов


в виде, очень схожем с исходной грамматикой. Основное различие
состоит в том, что каждой продукции сопоставлено некоторое
действие, результат которого возвращается как результат анализа.
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Анализатор термов (вариант 1)


l e t r e c atom i n p u t
= ( name ++
a ( O t h e r " ( " ) ++ t e r m l i s t ++ a ( Othe r " ) " )
>> ( fun ( ( ( f ,_) , a ) ,_) −> Fn ( f , a ) )
| | name
>> ( fun s −> Var s )
| | numeral
>> ( fun s −> Co nst s )
| | a ( O t he r " ( " ) ++ term ++ a ( O the r " ) " )
>> ( snd o f s t )
| | a ( O t he r "−" ) ++ atom
>> snd ) i n p u t
and mulexp i n p u t
= ( atom ++ a ( Ot he r "∗" ) ++ mulexp
>> ( fun ( ( a ,_) ,m) −> Fn ( "∗" , [ a ;m] ) )
| | atom ) i n p u t
and term i n p u t
= ( mulexp ++ a ( Other "+" ) ++ term
>> ( fun ( ( a ,_) ,m) −> Fn ( "+" , [ a ;m] ) )
| | mulexp ) i n p u t
and t e r m l i s t i n p u t
= ( term ++ a ( Oth er " , " ) ++ t e r m l i s t
>> ( fun ( ( h ,_) , t ) −> h : : t )
| | term
>> ( fun h −> [ h ] ) ) i n p u t ; ;

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Примеры
Объединим определённые ранее примитивы в единую функцию:
let parser =
f s t o ( term ++ f i n i s h e d >> f s t ) o l e x ; ;

Наглядной иллюстрацией работы этой функции является её вызов до


и после установки специализированной функции вывода (см. выше):
#p a r s e r " s i n ( x ␣+␣ y ) ␣∗␣ c o s ( 2 ␣ ∗␣ x ␣+␣ y ) " ; ;
− : term =
Fn
( " ∗" ,
[ Fn ( " s i n " , [ Fn ( "+" , [ Var " x " ; Var " y " ] ) ] ) ;
Fn ( " c o s " , [ Fn ( "+" , [ Fn ( "∗" , [ C o n s t " 2 " ;
Var " x " ] ) ;
Var " y " ] ) ] ) ] )
#i n s t a l l _ p r i n t e r " p r i n t _ t e r m " ; ;
− : unit = ()
#p a r s e r " s i n ( x ␣+␣ y ) ␣∗␣ c o s ( 2 ␣ ∗␣ x ␣+␣ y ) " ; ;
− : term = ‘ s i n ( x + y ) ∗ c o s ( 2 ∗ x + y ) ‘
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Автоматический учёт приоритетов


Возможности языка ML позволяют на основе динамического списка
инфиксных операций легко сконструировать «исправленную» с их
учётом версию грамматики.
l e t r e c b i n o p op p a r s e r i n p u t =
l e t atom1 , r e s t 1 as r e s u l t = p a r s e r i n p u t i n
i f n o t r e s t 1 = [ ] & hd r e s t 1 = O t he r op then
l e t atom2 , r e s t 2 = b i n o p op p a r s e r ( t l r e s t 1 ) i n
Fn ( op , [ atom1 ; atom2 ] ) , r e s t 2
else r e s u l t ; ;

let findmin l = i t l i s t
( fun (_, p r 1 as p1 ) (_, p r 2 as p2 ) −>
i f p r 1 <= p r 2 then p1 e l s e p2 ) ( t l l ) ( hd l ) ; ;

l e t rec d e l e t e x ( h : : t ) =
i f h = x then t e l s e h : : ( d e l e t e x t ) ; ;

l e t rec precedence i l i s t p a r s e r i n p u t =
i f i l i s t = [ ] then p a r s e r i n p u t e l s e
l e t opp = f i n d m i n i l i s t i n
l e t i l i s t ’ = d e l e t e opp i l i s t i n
b i n o p ( f s t opp ) ( p r e c e d e n c e i l i s t ’ p a r s e r ) i n p u t ; ;

Использование этой функции в основном анализаторе делает его


более простым и более общим.
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Анализатор термов (вариант 2)


l e t r e c atom i n p u t
= ( name ++
a ( O t h e r " ( " ) ++ t e r m l i s t ++ a ( O t h e r " ) " )
>> ( fun ( ( ( f ,_) , a ) ,_) −> Fn ( f , a ) )
| | name
>> ( fun s −> Var s )
| | numeral
>> ( fun s −> C o n s t s )
| | a ( O t h e r " ( " ) ++ term ++ a ( O t h e r " ) " )
>> ( snd o f s t )
| | a ( O t h e r "−" ) ++ atom
>> snd ) i n p u t
and term i n p u t = p r e c e d e n c e ( ! i n f i x e s ) atom i n p u t
and t e r m l i s t i n p u t
= ( term ++ a ( O t h e r " , " ) ++ t e r m l i s t
>> ( fun ( ( h ,_) , t ) −> h : : t )
| | term
>> ( fun h −> [ h ] ) ) i n p u t ; ;

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Возврат и повторное сканирование

Некоторые продукции в рамках одной и той же синтаксической


категории могут иметь общий префикс. Например, таким свойством
обладают правила вывода term:

term −→ name(termlist)
| name
| ···

В нашей реализации более длинная продукция целенаправленно


обрабатывается первой, так как успешное чтение имени может
привести к проблемам с дальнейшим разбором списка аргументов.
Однако, при необходимости возврата (первая альтернатива
оказалась неприменимой), разбор имени будет проведен дважды.
В данной продукции это не приводит к серьезным потерям
эффективности, но может привести при обработке termlist.

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Устранение избыточности
Заменим
let . . .
and t e r m l i s t i n p u t
= ( term ++ a ( O t h e r " , " ) ++ t e r m l i s t
>> ( fun ( ( h ,_) , t ) −> h : : t )
| | term
>> ( fun h −> [ h ] ) ) i n p u t ; ;

на
let . . .
and t e r m l i s t i n p u t
= term ++
many ( a ( O t h e r " , " ) ++ term >> snd )
>> ( fun ( h , t ) −> h : : t ) i n p u t ; ;

получив очередную реализацию, которая будет и проще, и


эффективнее. Окончательный вариант анализатора выглядит так:
Джон Харрисон Введение в Функциональное программирование
Лекция 10. Примеры на ML II. Синтаксический анализ

Анализатор термов (вариант 3)

l e t r e c atom i n p u t
= ( name ++
a ( O t h e r " ( " ) ++ t e r m l i s t ++ a ( O t h e r " ) " )
>> ( fun ( ( ( f ,_) , a ) ,_) −> Fn ( f , a ) )
| | name
>> ( fun s −> Var s )
| | numeral
>> ( fun s −> C o n s t s )
| | a ( O t h e r " ( " ) ++ term ++ a ( O t h e r " ) " )
>> ( snd o f s t )
| | a ( O t h e r "−" ) ++ atom
>> snd ) i n p u t
and term i n p u t = p r e c e d e n c e ( ! i n f i x e s ) atom i n p u t
and t e r m l i s t i n p u t
= ( term ++ many ( a ( O t h e r " , " ) ++ term >> snd )
>> ( fun ( h , t ) −> h : : t ) ) i n p u t ; ;

Джон Харрисон Введение в Функциональное программирование


Лекция 10. Примеры на ML II. Синтаксический анализ

Общие замечания
Рассмотренный метод синтаксического анализа может с разумной
осторожностью эффективно использоваться в различных приложениях.
Он служит хорошей иллюстрацией обширных возможностей функций
высших порядков.
Код анализатора тщательно структурирован и соответствует
грамматике, что обеспечивает лёгкость его модификации.
Однако, он не так эффективен, как хорошие LR-анализаторы, в
частности те, которые могут быть автоматически сгенерированы
CAML-Yacc.
Ещё одним ограничением метода рекурсивного спуска являются
проблемы с обработкой левосторонней рекурсии в продукциях.
Например, если бы мы попытались сделать левоассоциативной
операцию сложения, определённую в рассмотренных ранее примерах,
это можно было бы выразить так:

term −→ term+mulexp
| mulexp

Прямолинейный перевод этого правила на язык ML при исполнении


зацикливается. Однако, зачастую подобные конструкции легко
заменяются соответствующим итерированием.
Джон Харрисон Введение в Функциональное программирование

Вам также может понравиться