Для учебных целей разработан модельный компилятор delta, реализованный с помощью утилит Lex и Yacc (вернее, их свободных версий flex и bison). В исходном языке специально опущены некоторые существенные возможности (логические переменные, циклы for, классы и др.) — они оставлены в качестве задач для самостоятельного решения различной степени сложности.
Входной язык представляет собой минимальное подмножество языка C, дополненниое некоторыми конструкциями языка Python. Существенное отличие от языка C состоит в отсутствии фигурных скобок для указания границ блоков. Также для определения блоков формально не используются отступы в стиле языка Python (хотя, конечно же, отступ в 4 пробела для всех строчек вложенного блока — это правило хорошего тона в программах на любых языках!). Вместо этого каждая конструкция имеет строку-заголовок и строку-конец (аналог закрывающей фигурной скобки в C). Например, цикл while, который в C можно записать как
while (условие) { тело цикла }в учебном языке записывается как
while (условие) тело цикла endwhile(в роли закрывающей фигурной скобки используется оператор endwhile, а открывающая фигурная скобка вообще не нужна).
Кроме того, в число управляющих конструкций учебного языка
добавлен оператор выбора, отсутствующий в C:
if (условие1) ...
elseif (условие2) ...
elseif (условие3) ...
else ...
endif
Например, следующий фрагмент вычисляет функцию signum
— знак числа x:
if (x > 0.) sgn = 1; elseif (x < 0.) sgn = -1; else sgn = 0; endif
Программа может завершаться концом файла или необязательным ключевым словом end. В последнем случае весь текст, следующий за словом end, игнорируется.
Типы данных ограничиваются базовыми типами и динамическими массивами. Базовых типов всего 3: целое число int, вещественное число double и текстовая строка string. Delta — язык с динамической типизацией (как Python), переменные в нем не имеют типов. Операторы описания типов переменных в языке отсутствуют, как и в Python'е. Типы выражений и значений переменных определяются только в процессе работы программы.
Для конструирования типов можно использовать динамические массивы, которые эквивалентны спискам в языке Python:
a = [1, 2, 3]; # Массив длины 3 b = [[1, 2], [3, 4]]; # Матрица 2x2
В выражениях можно использовать 4 обычные арифметические операции, операцию возведения в степень ^ или ** и операцию взятия остатка от деления % (как и в C). Присваивание, как и в C/C++, также является операцией. Реализованы также математические стандартные функции sqrt, sin, cos, exp, atan, atan2, rand и т.д. Числовые константы записываются так же, как и в языке C (правда, целочисленные восьмеричные и шестнадцатеричные константы не реализованы). В текущей реализации целые значения представляют собой знаковые 64-разрядные числа. Строковые константы записываются в двойных апострофах: "abc", "x=" и т.п. Типа символ в стиле типа char или wchar языка C в языке нет. Вместо этого из строк можно извлекать подстроки с помощью функций left, right, mid, substr:
s = "e2-e4; c7-c5"; t = left(s, 5); # t == "e2-e4" h = substr(s, 7, 2); # h == "c7" r = substr(s, 1, 1); # r == "2"
Язык состоит из операторов и функций. Оператор может состоять из выражения, после которого ставится точка с запятой. Чаще всего это оператор присваивания:
переменная = выражение;где переменная в левой части может быть простой переменной или элементом массива. Оператор заканчивается символом ";" и(точка с запятой). Отметим, что присваивание является операцией и может использоваться внутри любого выражения. Операция присваивания выполняется справа налево. Комментарий обозначается как в C++ "//" или как в Python'е "#". Примеры:
x = 5; y = 2*x + 1; x = y = z = 1000; f(x, y, z); d = gcd(17, 43); a = [1, 2, 3] + [4, 5]; # a == [1, 2, 3, 4, 5] append(a, 6); # a == [1, 2, 3, 4, 5, 6] append(a, [7, 8, 9]); # a == [1, 2, 3, 4, 5, 6, [7, 8, 9]]
В языке для базовых типов имеются операции приведения типа.
x = double("6.125"); # вычислется число по его текстовому # представлению s = string(x + 1); # s == "7.125"После выполнения этого фрагмента строковая переменная s приобретет значение "7.125".
Структурных операторов всего 2 типа: оператор цикла while
и условный оператор if.
Условный оператор может иметь 3 формы:
— оператор "если" if... endif
— оператор "если-иначе"
if... else... endif
— оператор выбора
"если - иначе если - иначе если -... иначе":
if... elseif... elseif...
else... endif
Структурные операторы имеют строку-заголовок и строку-окончание. В окончании к ключевому слову, с которого начинается строка-заголовок, добавляется префикс "end": if-endif, while-endwhile. Фигурные скобки или ключевые слова begin-end для указания границ структурных операторов становятся ненужными, а программа более ясной; кроме того, это позволяет избежать неоднозначностей в грамматике языка и конфликтов при разборе, которые присутствуют, например, в языке C при разборе конструкции
if (условие1) if (условие2) оператор1; else оператор2;(непонятно, к какому if относится else — в C считается, что к внутреннему; при LR-разборе конфликт сдвиг-свертка в данном случае разрешается в пользу сдвига).
Примеры использования структурных операторов.
1. Суммирование массива:
s = 0.; i = 0; while (i < 100) s = s + a[i]; i = i + 1; endwhile2. Определение окончания множественного числа в русском языке (0 мячей, 1 мяч, 2 мяча, 3 мяча, 4 мяча, 5 мячей, 6 мячей...):
numBalls = ""; . . . numBalls = numBalls+n; // число n преобразуется // к текстовому виду и добавляется в конец строки numBalls = numBalls + " мяч"; if (n == 0 || n >= 5) numBalls = numBalls + "ей"; elseif (n >= 2) numBalls = numBalls + "а"; endif
Для выхода из цикла while можно, как и в C,
использовать оператор break;
для перехода к следующей итерации цикла — оператор
continue;
В условиях можно использовать операции сравнения и логические операции, записываемые так же, как и в языке C. Например,
if (y > 0. && x / y > 10.) x = 10. * y; endifВ отличие от C, целочисленные выражения нельзя использовать в качестве логических. Например, фрагмент программы
x = 100; . . . if (x) y = 1; endifявляется ошибочным (впрочем, и в C это дурной стиль!). Вместо этого надо писать
if (x != 0) y = 1; endif(такому стилю хорошо бы следовать и при программировании на C).
Реализация логического типа bool и логических переменных оставлена в качестве задачи для самостоятельного решения.
В программах можно использовать встроенные операторы ввода и вывода, вводящие значения переменных с клавиатуры (стандартного входа) или выводящие значения в стандартный поток вывода: input, output, outputln. Но лучше вместо встроенных операторов пользоваться стандартными функциями scan, print и println, которые также реализованы в языке. Функция scan, так же как и оператор input, всегда вводит целиком строку до символа перевода строки '\n' (не включая этого символа). Пример:
s = scan(); println("s = ", s); x = int(s); y = double(s); println("x = ", x, " y = ", y);Пусть при выполнении этого фрагмента программы человек введет с клавиатуры строку
6.5Тогда будет напечатано:
s = 6.5 x = 6 y = 6.5
Динамические массивы похожи на списки в Python'е. Элементы массива могут иметь различные типы.
a = [1, 2, "abc", [3, 4, 5]];Матрица реализуются как массив массивов. В следующем примере мы определяем матрицу a размера 2x3:
a = []; append(a, [1, 2, 3]); append(a, [4, 5, 6]); # a -- матрица [[1, 2, 3], [4, 5, 6]]Стандартная функция append(a, x) добавляет элемент x в конец массива a, при этом длина массива увеличивается на 1. Функция extend(a, b) дописывает содержимое массива b в конец массива a. Операция + соединяет два массива, ее результатом является новый массив:
a = [1, 2]; append(a, 3); # a == [1, 2, 3] append(a, [4, 5]); # a == [1, 2, 3, [4, 5]] extend(a, [6, 7, 8]); # a == [1, 2, 3, [4, 5], 6, 7, 8] b = a + [9, 10]; # b == [1, 2, 3, [4, 5], 6, 7, 8, 9, 10]Стандартные функции left, right, mid извлекают из массива подмассивы в начале, середине и конце соответственно. Вторым аргументом функции mid является индекс начала подмассива, третьим аргументом — длина подмассива. Если третий аргумент отсутствует или отрицательный, то результатом явлется подмассив, начинающийся с указанной позиции и включающий все элементы до конца массива:
a = [0, 1, 2, 3, 4, 5]; b = left(a, 3); # b == [0, 1, 2] c = right(a, 2); # c == [4, 5] d = mid(a, 1, 3); # d == [1, 2, 3] e = mid(a, 2); # e == [2, 3, 4, 5]Функции left, right, mid аналогично работают и со строками:
s = "012345"; b = left(a, 3); # b == "012" c = right(a, 2); # c == "45" d = mid(a, 1, 3); # d == "123" e = mid(a, 2); # e == "2345"
Заголовок функции состоит из ключевого слова func (или function), за которым следует имя функции и список параметров. За заголовком следует тело функции. Заканчивается описание функции ключевым словом endfunc (или endfunction). Функция может возвращать значение value с помощью оператора "return value;". Пример функции gcd(a, b), вычисляющей наибольший общий делитель целых чисел a, b:
func gcd(a, b) while (b != 0) r = a%b; a = b; b = r; endwhile return a; endfunc
Функции могут вкладываться друг в друга, тогда параметры и локальные переменные объемлющей функции доступны во вложенных функциях и являются для них нелокальными объектами. Пример: пусть мы имеем функцию gauss(a), приводящую матрицу a к ступенчатому виду. Матрица a является входным параметром функции gauss, ее результатом будет ступенчатая матрица b. Тогда внутри функции gauss можно описать функцию swapRows(i, j), которая меняет местами две строки матрицы a с индексами i, j. При этом матрица a, а также число ее столбцов n не передаются функции swapRows как параметры, а являются для нее нелокальными объектами:
func gauss(a) func swapRows(i, j) k = 0; while (k < n) tmp = a[i][k]; a[i][k] = a[j][k]; a[j][k] = tmp; k = k + 1; endwhile endfunc m = len(a); # number of rows of matrix n = len(a[0]); # number of columns . . . swapRows(i, j); . . . endfunc
Чаще всего параметры передаются функциям по значению, т.е. при вызове функции происходит копирование фактических параметров. Иногда, однако, требуется передавать фактический параметр по ссылке, чтобы вызванная функция могла менять значения переданных ей объектов. Также это полезно, когда функции передается массив, передача по ссылке позволяет избежать лишних копирований. Для этого в языке Delta следует использовать колючевое слово ref в описании формального параметра функции. Для примера рассмотрим функцию пузырьковой сортировки массива. Сортируемый массив передается по сслыке:
func bubbleSort(ref a) func swap(i, j) tmp = a[i]; a[i] = a[j]; a[j] = tmp; endfunc wasInversions = 1; pass = 1; while (wasInversions != 0) wasInversions = 0; i = 0; while (i < len(a) - pass) if (a[i] > a[i+1]) swap(i, i+1); wasInversions = 1; endif i = i + 1; endwhile pass = pass + 1; endwhile endfunc
Одна и та же функция может возвращать значения разных типов в зависимости от типов фактических параметров или вовсе не возвращать значения. Набор значений можно возвращать как массив. Например, в Расширенном алгоритме Евклида функция extEucl(m, n) вычисляет наибольший общий делитель d целых чисел m, n и его выражение через исходные числа:
d = u*m + v*n.Таким образом, результатом алгоритма является тройка чисел [d, u, v], которая возвращается в виде массива длины 3:
# Extended Euclid Algorithm: # For integers m, n compute d = gcd(m, n) # and u, v such that d = u*m + v*n func extEucl(m, n) a = m; b = n; u1 = 1; v1 = 0; u2 = 0; v2 = 1; while (b != 0) # Invariant: a == u1*m + v1*n # b == u2*m + v2*n # gcd(a, b) == gcd(m, n) q = a/b; r = a%b; a = b; b = r; tmp = u1; u1 = u2; u2 = tmp - q*u2; tmp = v1; v1 = v2; v2 = tmp - q*v2; endwhile return [a, u1, v1]; endfunc
Функции можно запоминать в переменных и передавать как параметры другим функциям Например, следующая функция simpson(f, a, b, n) вычисляет интеграл по формуле Симпсона от произвольной функции f(x) на отрезке [a, b]; функция f является ее параметром.
function simpson(f, a, b, n) k = 2*n; h = (b - a)/k; s = f(a); x = a; while (0 == 0) x = x + h; s = s + 4.*f(x); x = x + h; if (x >= b - h/2.) break; endif s = s + 2.*f(x); endwhile s = s + f(b); return s*h/3.; endfuncПолный текст этой программы содержится в файле "simpson.d".
Пример "functional.d" иллюстрирует элементы функционального программирования. В нем реализованы популярные в этой области функции map(f, list) и reduce(g, list), работающие со списками. Функция map к каждому элементу списка применяет функцию f, заменяющую его на образ под действием f. Функция reduce "сворачивает" список (например, вычисляет произведение его элементов), используя функцию g как бинарную операцию над его элементами.
Список стандартных функций, реализованный в настоящее время в языке:
abs | целая абсолютная величина числа |
append | добавить элемент в конец массива/строки |
atan2 | atan2(y, x) = arctg(y/x) |
atan | arctg |
clear | очистить массив/строку |
cos | cos |
exp | экспонента |
extend | дописать массив в конец массива/строки |
fabs | вещественнная абсолютная величина числа |
fmod | остаток от деления вещественных чисел |
gcd | наибольший общий делитель |
left | выделить начало массива/строки |
len | длина массива/строки |
log10 | логарифм по основанию 10 |
log2 | логарифм по основанию 2 |
log | натуральный логарифм |
mid | выделить середину массива/строки |
powmod | целочисленная степень по модулю m |
pow | вещественная степень |
println | напечатать несколько выражений и перевести строку |
напечатать одно или несколько выражений | |
rand | случайное целое число (очень большое) |
randrange | randrange(a, b) —
случайное число r в диапазоне a≤ r<b |
range | range(a, b) —
сгенерировать массив целых чисел x в диапазоне a≤ x<b |
right | выделить конец массива/строки |
scan | ввести строку из входного потока |
sin | sin |
size | длина массива/строки |
sqrt | квадратный корень |
substr | выделить подстроку в середине строки |
tan | tg |
1. Печать квадратов первых 20 натуральных чисел — файл "square20.d":
n = 1; while (n <= 20) println(n, " ", n*n); n = n+1; endwhile endВывод программы:
1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 10 100 11 121 12 144 13 169 14 196 15 225 16 256 17 289 18 324 19 361 20 400
2. Печать n первых простых чисел — файл "nprimes.d":
# Input an integer number n and print first n prime numbers print("Number of primes: "); s = scan(); n = int(s); k = 0; p = 2; while (k < n) if (p == 2) print(" ", p); k = k+1; p = p+1; else prime = 1; d = 3; while (prime == 1 && d*d <= p) if (p % d == 0) prime = 0; break; endif d = d+2; endwhile if (prime == 1) print(" ", p); k = k+1; if (k % 10 == 0) println(); endif endif p = p+2; endif endwhile println();Вывод программы (для n=50):
Number of primes: 50 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229
3. Приближенное вычисление квадратного корня вещественного числа — файл "sqrt.d":
# # Compute a square root of a number # and compare the result with standard sqrt(x) function # func main() while (0 == 0) # Infinite loop print("Input a number (0 for the end): "); x = double(scan()); if (x == 0.) break; endif y = squareRoot(x); println("squareRoot(x) = ", y); println("standard sqrt(x) = ", sqrt(x)); endwhile endfunc # Compute a square root using Newton iterations func squareRoot(a) println(" in squareRoot: a = ", a); if (a < 0.) println(" Incorrect argument"); return 0.; endif EPS = 0.0000001; x = a; if (x < 1.) x = 1.; endif xPrev = x + 1e100; while (fabs(xPrev - x) > EPS) xPrev = x; x = (x + a/x) / 2.; println(" ", x); endwhile println(" in squareRoot: result = ", x); return x; endfunc # Starter main();Вывод программы для чисел 2 и 10 (для заданного числа печатаются последовательность приближений и окончательный результат):
Input a number (0 for the end): 2 in squareRoot: a = 2 1.5 1.41667 1.41422 1.41421 1.41421 in squareRoot: result = 1.41421 squareRoot(x) = 1.41421 standard sqrt(x) = 1.41421 Input a number (0 for the end): 10 in squareRoot: a = 10 5.5 3.65909 3.19601 3.16246 3.16228 3.16228 in squareRoot: result = 3.16228 squareRoot(x) = 3.16228 standard sqrt(x) = 3.16228
Пример "gcd.d" иллюстрирует возможность возвращения списка (динамического массива) в качестве результата функции. В данном случае функция extEucl(m, n) возвращает список из трех значений:
func main() while (0 == 0) # Infinite loop println("Input 2 integer numbers (two zeroes for the end):"); print("m = "); m = int(scan()); print("n = "); n = int(scan()); if (m == 0 && n == 0) break; endif d = gcd(m, n); println("gcd(m, n) = ", d); println("Extended Euclid algoritm:"); res = extEucl(m, n); println("d = ", res[0], ", u = ", res[1], ", v = ", res[2]); println("d == gcd(m, n) == u*m + v*n\n"); endwhile endfunc # Recursive implementation of gcd func gcd(a, b) if (b == 0) return a; else return gcd(b, a%b); endif endfunc # Extended Euclid Algorithm: # For integers m, n compute d = gcd(m, n) # and u, v such that d = u*m + v*n func extEucl(m, n) a = m; b = n; u1 = 1; v1 = 0; u2 = 0; v2 = 1; while (b != 0) # Invariant: a == u1*m + v1*n # b == u2*m + v2*n # gcd(a, b) == gcd(m, n) q = a/b; r = a%b; a = b; b = r; tmp = u1; u1 = u2; u2 = tmp - q*u2; tmp = v1; v1 = v2; v2 = tmp - q*v2; endwhile return [a, u1, v1]; endfunc main();Пример выполнения программы:
Input 2 integer numbers (two zeroes for the end): m = 19 n = 43 gcd(m, n) = 1 Extended Euclid algoritm: d = 1, u = -9, v = 4 d == gcd(m, n) == u*m + v*n
Пример "gauss.d" иллюстрирует работу с матрицами. В программе вводится квадратная матрица и методом Гаусса приводится к ступенчатому виду. Печатается ступенчатая форма матрицы и ее определитель.
Алгоритмы сортировки иллюстрируют использование
вложенных функций и нелокальных переменных:
"qsort.d" — сортировка QuickSort;
"heapsort.d" — сортировка HeapSort.
Примеры "simpson.d" и "functional.d" иллюстрируют работу с переменными, значениями которых являются функции (вернее, указатели на функции, если придерживаться терминологии языка C). В файле "simpson.d" реализована функция
function simpson(f, a, b, n)вычисляющая интеграл от функции f(x) по отрезку [a, b], использующая формулу Симсона с числом точек разбиения 2n. Функция f является параметром функции simpson.
Программа в файле "functional.d" иллюстрирует элементы функционального программирования. В ней реализованы стандартные в этой области функции map(f, list) и reduce(g, list), работающие со списками. Функция map к каждому элементу списка применяет функцию f, заменяющую его на образ под действием f. Функция reduce "сворачивает" список (например, вычисляет произведение его элементов), используя функцию g как бинарную операцию над его элементами (в языке Hakell эта функция называется foldl).
Исходные текста проекта
(архивы в tar-gzip формате и в zip-формате):
"Delta.tgz" или
"Delta.zip".
Для компиляции проекта следует выполнить команду
make
Для запуска готового компилятора и выполнения программы "filename.d" следует выполнить команду
./delta filename.dили просто
delta filename.dесли либо текущая директория находится в пути PATH, либо файл "delta" находится в одной из директорий, добавленных в путь. Текст программы читается из указанного файла. Можно также читать программу из входного потока:
delta -std_inputдля окончания ввода программы следует набрать строку
end(можно ввести также символ конца файла — Ctrl+D в Unix'е или Ctrl+Z в MS Windows). После этого программа выполняется, ее ввод и вывод происходит в консоли.
Также есть и другие возможности: распечатать промежуточный код программы, распечатывать таблицы символов и т.п. Полный текст подсказки показывается по команде "delta" без аргументов:
Delta Compiler + Interpreter. Usage: delta [-show_icode] [-show_nametable] [-std_input] [-debug] [source_file] where -show_icode Print intermediate code -show_nametable Print global nametable -std_input Read program from standard input -show_debug Print a lot of debugging information source_file File with a source program
Компилятор состоит из следующих модулей и файлов: