Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Информатика - Лекции - Семестр 2

.pdf
Скачиваний:
13
Добавлен:
10.05.2015
Размер:
1.18 Mб
Скачать

Языки программирования высокого уровня

Синтаксис, формальные грамматики.

Для представления алгоритмов, предназначенных для выполнения на ЭВМ, используются языки программирования (ЯП). Внешняя форма программы на ЯП устанавливается с помощью синтаксиса языка , который определяет формальный язык. Он определяется с помощью определенных грамматических правил, аналогичных алгоритму текстовых подстановок. Основой формального определения синтаксиса являются формальные грамматики.

Синтаксис языка — это набор правил, определяющих допустимые конструкции языка. Синтаксис определяет «форму языка» — задает набор цепочек символов, которые принадлежат языку.

Семантика языка — это раздел языка, определяющий значение предложений языка. Семантика определяет «содержание языка» — задает смысловое значение для всех допустимых цепочек языка.

Грамматика — это описание способа построения предложений некоторого языка. Иными словами, грамматика — это математическая система, определяющая язык.

Грамматику языка можно описать различными способами: например, грамматика русского языка описывается довольно сложным набором правил.

Правило — это упорядоченная пара цепочек символов (α, β). В правилах очень важен порядок цепочек, поэтому их чаще записывают в виде α→β.

Формально грамматика G определяется как четверка (N, T, P, S), где

N — конечное множество нетерминальных символов или нетерминалов;

T — конечное множество терминальных символов или терминалов, причем

NT= ;

P — конечное множество правил грамматики вида α→β,

где (α , β) (N UT )* N(N UT )* ×(N UT )* .

S — символ из N, называемый начальным символом.

Множество V = N T называют полным алфавитом грамматики G.

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

Пример 1.

Дана грамматика G =({A, S}, {0, 1}, P, S), где P = {S0A1, 0A00A1, Ae}.

Нетерминальными символами являются A, S, а терминальными 0, 1.

Грамматика определяет язык рекурсивным образом посредством задания особого рода выводимых цепочек.

Выводимые цепочки грамматики G = (N, T, P, S) определяются следующим образом:

S — выводимая цепочка;

Если αβγ — выводимая цепочка, и β→δ содержится в Р, то αδγ - тоже выводимая цепочка

Выводимая цепочка грамматики G, не содержащая нетерминальных символов,

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

Язык, порождаемый грамматикой G (L(G)) - это множество терминальных цепочек, порождаемых грамматикой G.

Пусть G = (N, T, P, S) — грамматика, V = N T — ее алфавит. α,β,γ V*, δ

V+. Цепочка ϕ = αβγ называется непосредственно выводимой из ψ = αδγ в

грамматике G, если в G существует правило δ→β P. Обозначается ψ G ϕ. В тех случаях, когда из контекста ясно, о какой грамматике идет речь, нижний индекс G будем опускать.

Иными словами, цепочка ϕ непосредственно выводима из ψ, если можно взять несколько символов в цепочке ψ, заменить их на другие символы согласно правилу грамматики и получить цепочку ϕ. Любая из цепочек δ и β (или обе они) может быть пустой. В предельном случае вся цепочка ψ может быть заменена на цепочку ϕ, тогда в грамматике G должно существовать правило ψ→ϕ P.

Через k будем обозначать k-ю степень отношения . Иначе говоря, α kβ , если существует последовательность α0,α1, α2,…, αk, состоящая из k+1 не обязательно различных цепочек, для которых α=α0, αi-1 αI, 1≤i≤k и αk=β. Эта последовательность называется выводом длины k цепочки β из цепочки α в

грамматике G.

 

Введем отношение ϕ +ψ означающее, что цепочка

ψ выводима из ϕ

нетривиальным образом: ϕ +ψ тогда и только тогда, когда

ϕ iψ для некоторого

i≥1.

 

Говорят, что цепочка ψ выводима из ϕ (обозначение ϕ *ψ)тогда и только тогда, когда ϕ iψ для некоторого i≥0.

Вывод называется законченным, если на основе цепочки ϕ, полученной в результате вывода, нельзя больше сделать ни одного шага вывода. Иначе говоря,

вывод называется законченным, если цепочка ϕ пустая или содержит только терминальные символы грамматики G = (N, T, P, S). Цепочка, полученная в результате законченного вывода, называется конечной цепочкой вывода.

Грамматики можно классифицировать по виду их правил. Грамматика G = (N, T, P, S) называется

праволинейной, если каждое правило из Р имеет вид A→αB или A→α, где

A,B N, α V*;

контекстно-свободной (бесконтекстной), если каждое правило из Р имеет вид A→α, где A N, α V*;

контекстно-зависимой (неукорачивающей), если каждое правило из Р имеет вид α→β, где α β .

Грамматика, не удовлетворяющая ни одному из указанных ограничений,

называется грамматикой общего вида или грамматикой без ограничений.

Пример 2.

Праволинейная грамматика G1=({S}, {0, 1}, {S0S|1S|e},S) порождает язык

L(G1)={0,1}*.

Пример 3.

Пусть G2=({E, T, F}, {a, +, *, (, )}, P, E), где Р состоит из правил

EE+T|T

TT*F|F

F(E)|a

Язык L(G2) представляет собой множество арифметических выражений, построенных из символов a, +, *, (, ). Грамматика G2 — контекстно-свободная.

Пример 4.

Пусть G3=({C, B, S}, {a, b, c}, P, S), где Р состоит из правил

SaSBC|abC

CBBC bBbb bCbc cCcc

Эта грамматика порождает язык {anbncn| n≥1}.Очевидно, это контекстнозависимая грамматика.

Для записи синтаксиса конструкций языков программирования часто используют форму Бэкуса-Наура. Она практически ничем не отличается от нотаций продукций

формальных грамматик, только вместо знака используется обозначение ::=. Пример 5. Десятичное целое без знака. <Десятичное_целое_без_знака>::=<Цифра>|<Десятичное_целое_без_знака><Цифра> <Цифра>::=0|1|2|3|4|5|6|7|8|9

Семантика.

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

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

1. Определить виды используемых в языке программирования простых операторов,

а также часто используемые методы композиции решений подзадач.

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

Ниже мы проиллюстрируем формальное определение семантики на

примере

простого модельного языка содержащего

основные конструкции и

правила

композиции, встречающиеся в современных языках программирования .

 

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

P

Q

Рис 3.1.

Здесь S — оператор, группа операторов или программа; Р — предусловие — логическое выражение, которое должно быть истинным перед выполнением S; Q — постусловие — логическое выражение, которое должно принимать истинное значение при выходе из вычислений S. Если ввести в язык понятие комментария как произвольного текста, заключенного в фигурные скобки, то свойство, изображенного на рис 3.1. можно в тексте программы записать как:

(1)

{P} S {Q}

Это — спецификация программы S со следующим смыслом: если соотношение Р истинно перед выполнением S, то Q будет истинно после выполнения S.

Пусть Р некоторое логическое выражение. Для простоты будем полагать, что кванторы всеобщности и существования не принадлежат Р. Для выражения x(P)

считаем, что вхождение переменной х в x связано квантором всеобщности и каждое вхождение х в Р –связанное значение. Та же терминология применяется к

выражению x(P) , за исключением того, что в этом случае вхождение переменной

х

в

x называют связанным квантором существования. Каждое вхождение переменной

х

в

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

переменной этого выражения.

Пример 1

все переменные в выражении ¬ x( y( y x)) связаны;

все переменные в выражении (i < j) = ( j < k) свободны.

Определим понятие подстановки.

Пусть P - логическое выражение. Нотация

(2)

Pyx

используется

для

выражения,

которое

получается

в

результате

систематической подстановки

выражения у вместо всех свободных вхождений

переменной х в Р. Аналогично

 

 

 

 

 

(3)

 

x1...xn

 

 

 

 

 

Py1... yn

 

 

 

 

обозначает одновременную подстановку вместо всех свободных вхождений любой переменной xi в P соответствующего выражения yi. Вхождения xi в некоторые yi не замещаются. Переменные x1...xn должны быть различными, в противном случае подстановка не определена.

Пример 2.

(z =10)zx+y = (x + y =10)

(x + x + y)ax,+yb,c = a +b +a +b +c (x + x + y)xx,+yy,z = x + y + x + y + z

Правила вывода - это схемы рассуждений, позволяющие доказывать свойства программы. Они имеют следующий вид:

(4)

H1 ,..., H n

H

 

Если H1,…,Hn — истинные утверждения, то H — также истинное утверждение. Рассмотрим правила вывода для простых операторов языка программирования.

Пустой оператор

Он не оказывает никакого воздействия на значения программных переменных. Для любого P имеем правило вывода

(5)

{P}{P}

Оператор присваивания

Он имеет вид x = e и устанавливает значение переменной x равным значению выражения e. Тогда для любого P

(6)

{ Pчe } x:=e { P }

Пример 3.

{x+y=10} z:=x+y {z=10} {x-y>0} x:=x-y {x>0}

Рассмотрим правила вывода для более сложных конструкций - составных и условных операторов.

Составной оператор

Он образуется путем последовательной композиции операторов S1; S2;…;Sn. Знак ; используется для соединения операторов в последовательность. В модельном языке составной оператор (или блок операторов) представлен программной конструкцией:

(7)

begin S1; S2;…;Sn end

Правило вывода для составного оператора имеет вид:

(8)

 

i =1,..., n{Pi1} Si {Pi }

{P0

} begin S1; S2

;...; Sn end {Pn }

 

Пример 4.

Дан составной оператор

Z: = z+y u := u-1

Известно постусловие

Q = (z+u*y = x*y, u≥0)

Тогда согласно правилу вывода для оператора присваивания легко установить

{z+(u-1)*y = x*y, u-1≥0} u := u-1 {z+u*y = x*y, u≥0} {z+u*y = z+y+(u-1)*y = x*y, u>0} z := z+y {z+(u-1)*y = x*y, u-1≥0}

Следовательно, для рассматриваемого составного оператора справедливо

{z+u*y=z+y+(u-1)*y=x*y, u>0} begin z := z+y; u := u-1 end {z+u*y=x*y, u≥0}

Нетрудно заметить, что логическое выражение z+u*y=x*y сохраняется при выполнении составного оператора. Логические выражения, сохраняемые прорграппной конструкцией называются инвариантами. Нетрудно заметить, что в нашем примере выполнение отдельных операторов присваивания в составном операторе не сохраняет инвариант. Выражение z+u*y=x*y инвариантно для всего составного оператора

Условный оператор

Если S1 и S2 - операторы, а B - булево выражение, то

(9)

if B then S1 else S2 endif

есть оператор, выполняющий следующие действия: вычисляется B; если B=true, то выполняется оператор S1, в противном случае — S2 . Пусть предусловие этого оператора есть P, а постусловие — Q.

Тогда если значение B есть true, то оператор S1 будет иметь постусловие Q, если справедливо соотношение

(10)

{P B} S1 {Q}

Аналогично для оператора S2 должно быть справедливо соотношение

(11)

{P ¬B} S2 {Q}

Объединяя (10,11) можно получить правило вывода для условного оператора

(12)

{P B} S1

{Q}, {P ¬B} S2

{R}

 

{P}

if B then S1 else S2 endif

{Q}

 

 

Рассмотрим более простую форму условного оператора

(13)

 

If B

Then

S Endif

 

По этому оператору вначале вычисляется B. Если B истинно, то выполняется оператор S, иначе должна быть выполнена тождественная операция. Правило вывода для оператора (13) имеет вид

(14)

Пример 5.

{P B} S {Q}, P Q

{P} If B Then S {Q}

Легко доказать, что оператор

If r<y Then r := r-y; q :=q+1 EndIf

сохраняет истинным предикат q*y+r=x, r≥0 при условии y>0. В самом деле, составной оператор удовлетворяет соотношению

(q*y+r=x,0≤r<y} r := r-y; q := q+1 {q*y+r=x,r≥0}

Тогда согласно (14),

{q*y+r=x, 0≤r} If r<y Then r := r-y; q = q+1 EndIf {q*y+r=x,r≥0},

а это означает инвариантность предусловия.

Селективный оператор выбора

Он имеет вид

 

(15)

Select Case x

 

Case k1:S1;

 

...

 

Case kn:Sn

 

EndSelect

Оператор Case выбирает для выполнения тот оператор-компоненту, метка которого ki совпадает с текущим значением селектора x. Если такой метки не обнаружено, то действие оператора (16) не определено. Очевидно, если требуется, чтобы Q было справедливым независимо от того, какой оператор Si выбирается для выполнения, мы имеем набор соотношений:

(17)

{Pi (x=ki)} Si {Q}, i=1..n

Тогда правило вывода для оператора выбора должно иметь вид:

(18)

 

 

 

 

 

 

{Pi

(x = ki )}

Si {Q}

для i =1,..., n

 

 

{P (x [k1 ,..., kn ])}

Select Case Case k1

: S1;...Case kn : Sn EndSelect {Q}

 

 

P = (P1 P2 ... Pn )

 

 

 

 

 

Оператор цикла с предусловием

 

 

 

 

Если B - булево выражение, а S - оператор, то

 

(19)

While B SWend

 

 

 

обозначает итерационное выполнение оператора S пока B истинно.

Если B

ложно с самого начала,

то S не

будет выполняться совсем. Процесс

итерации

заканчивается, когда В становится ложным. Определим правило вывода для этого оператора. Если Р - его предусловие, то предусловие для оператора S имеет вид

P B. Для итерационного повторения выполнения оператора S в цикле его постусловие должно совпадать с P. Следовательно, оператор S должен удовлетворять соотношению

{P B} S {P}

Оператор цикла с предусловием завершит свою работу, если справедливо

P¬B

Итак, получаем следующее правило вывода для оператора с предусловием

(17)

{P B} S {P}

{P} While B S Wend {P}

Далее будем называть Р инвариантом цикла.

Пример 6.

Рассмотрим оператор цикла

While r >= y

r := r-yz; q := 1+q

Wend

в предположении, что {x≥0 y>0}. Покажем, что данный цикл имеет инвариант

x=q*y+r 0≤r

Согласно правилам вывода для оператора присваивания и составного оператора легко получаем соотношение

{x=q*y+r 0≤r-y} r:=r-y; q:=q+1 {x=q*y+r 0≤r}

Предусловие составного оператора имеет форму P B. Поэтому для рассматриваемого цикла справедливо согласно (20) соотношение:

{(x=q*y+r) 0≤r}

While r>=y r:=r-y; q=q+1 Wend {(x=q*y+r) 0≤r<y}

Оператор цикла с постусловием

(21)

Do S Loop Until B

Где S – оператор (составной) и B - булево выражение. Здесь S выполняется перед вычислением B. Затем, если В ложно, процесс итерационного выполнения S продолжается, в противном случае он завершается.

Предположим, доказано, что {P} S {Q} и Q¬B P. Тогда возможно повторное

выполнение оператора S. Выход из цикла влечет за собой условие Q B. Тогда правило вывода для цикла с постусловием имеет вид

(22)

{P}S{Q}, Q (¬B P)

{P} Do S Loop Until B {Q B}

Пример 7.

Пусть дан цикл с постусловием

Do

z := z+y; u := u-1

Loop Until u = 0

Предположим, что x, y — переменные целого типа, которые удовлетворяют

условию x>0 y>0. Пусть требуемое постусловие цикла есть {z=x*y}. Очевидно, если объединить его с условием завершения цикла u=0, то его можно переписать в виде z+u*y=x*y. Используя правила вывода для операторов присваивания и составного оператора “z=z+y u=u-1” легко показать, что он имеет предусловие и постусловие {z+u*y=x*y}. Отсюда легко получить числитель правила вывода (22):

{z+u*y=x*y} z := z+y; u := u-1 {z+u*y=x*y}, {z+u*y=x*y} u>0

Отсюда и из правила вывода (22) следует, что

{z+u*y=x*y} z := z+y; u := u-1 {{z+u*y=x*y} u=0}

Очевидно, полученное постусловие эквивалентно {z=x*y}. Следует обратить внимание, что неявно мы здесь воспользовались еще одной группой правил вывода - правилами консеквенции.

Правила консеквенции имеют вид

 

 

(23)

{P}S{R}, R Q

,

P R, {R}S{Q}

 

{R}S{Q}

{P}S{Q}

 

 

 

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

Корректность программ.

Правильность программ обычно проверяется тестированием. Оно заключается в том, что выбираются конкретные значения входных данных программы и программа выполняется с этим входными данными. Результаты работы программы сравниваются с заранее известными правильными результатами.

Тестирование - процесс выполнения программы с намерением найти ошибку. Но оно никогда не докажет их отсутствия. Поэтому необходимо абстрагироваться от

индивидуальных

процессов

и постулировать некоторые общезначимые условия,

которые можно

вывести из

характера поведения. Этот аналитический метод

проверки носит название верификация программ или доказательство корректности программ. Верификация основана на систематическом применении правил вывода, полученных в предыдущем параграфе. Проиллюстрируем этот прием на примерах программирования на языке Visual Basic. Единственное отличие нотации данного пункта, от ранее введенной – использование комментариев и оператора присваивания этого языка.

Пример 1.

Алгоритм деления целых неотрицательных чисел x на у. Результат операции частное q и остаток r, которые удовлетворяют соотношению

x=q*y+r 0≤r<y

Очевидно, это постусловие программы, ее предусловие имеет вид x≥0 y>0

Алгоритм можно записать следующим образом

‘{x≥0 y>0} q = 0

r = x

‘{x=q*y+r 0≤r}

While r>=y

‘{x=q*y+r 0≤y<r} r = r-y

q = q-1 Wend

‘{x=q*y+r 0≤r<y}

Заметим, что первые два оператора q=0 и r=x автоматически гарантируют

истинность инварианта цикла {x=q*y+r 0≤r}. Этот типичный прием называется

инициализацией инвариантов цикла.

В примере 5 предыдущего параграфа мы доказали, что для цикла справедливо правило

‘{(x=q*y+r) 0≤r} While r>=y

r=r-y q=1+q

Wend

‘{(x=q*y+r) 0≤r<y}

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

оператора получаем постусловие программы {(x=q*y+r) 0≤r<y}, что и требовалось доказать.

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

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

2.Повторить этот процесс "декомпозиции и доказательство корректности декомпозиции" для подзадач.

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

Проиллюстрируем этот метод проектирования сверху вниз на простом примере.

Пример 2.

Умножение двух положительных целых чисел x и y. Результат – z. Начальное описание алгоритма таково:

‘{x>0 y>0}

‘сложить значение у само с собой x раз

‘ {z=x*y}

Предусловие есть требование положительности х и у. Постусловие - результат вычислений. Правильность преобразования предусловия в постусловие следует из определения операции сложения.

Следующий уровень декомпозиции требует введения переменных z для хранения промежуточной суммы и u, которая следит, сколько раз будет выполнено сложение. Таким образом, получаем следующую декомпозицию:

1.

‘{x>0 y>0}

Установить z равным нулю, u равным x

‘{z+u*y=x*y u>0}

2.

‘{z+u*y=x*y u>0}

‘Увеличить z на величину y и уменьшить u на 1; повторять этот процесс до тех ‘пор пока u не станет равным 0

{z=x*y}

Нетрудно заметить, что инвариантом является логическое выражение z+u*y=x*y

Тогда подзадача 1 содержит операторы присваивания, инициализирующие этот инвариант.

Действие «увеличить z на величину y и уменьшить u на 1» подзадачи 2

построено таким образом, чтобы сохранить инвариант. В самом деле, Z+y+(u-1)*y=z+u*y=x*y

Условие «повторять этот процесс до тех пор, пока u не станет равным 0»,

гарантирует, что после окончания итераций будет получен требуемый результат z=x*y.

Корректность подзадач показана, и мы получаем программу

‘{x>0 y>0} z = 0

u = x Do

z = z+y u = u-1

Loop until u=0

‘{z=x*y}

Можно непосредственно доказать корректность этой программы.

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