DemonScript: Основные операторы


Базовые типы

DemonScript - полиморфный язык. Это означает, что каждая переменная в момент исполнения скрипта имеет определённый тип. Однако его можно поменять, присвоив в эту переменную значение другого типа. Базовыми типами являются:
ТипОписаниеПример
float  вещественное число 2, 3.1415
logic  логическое значение True, (0.2,0.8)
string  строка "Привет!"
array  массив [1, "Привет", [3,4]]
graph  граф GRAPH
node  узел графа $box, GRAPH[5]
edge  ребро между двумя узлами  @in, @isa

Имена переменных могут содержать латинские буквы, цифры и символы подчёркивания. В DemonScript принято имена переменных начинать с заглавной буквы, имена демонов с маленькой. Впрочем, никаких запретов на использование другого стиля именования нет. Для объявления переменных служит оператор var, после которого через запятую перечисляются переменные. Там же их можно сразу инициализировать, присвоив в них некоторое значение (и тем самым задав тип переменной):

var X, Y = 5
Объявлять переменные обязательно и компилятор выдаст ошибку, встретив не объявленную переменную.

Узнать текущий тип переменной можно при помощи метода .type(). Например:

var X         out X.type()         //> None
X = True      out X.type()         //> logic
X = 2         out X.type()         //> float
X = "Привет"  out X.type()         //> string
X = @in       out X.type()         //> edge
Оператор out выводит на экран значение выражения, которое идёт после него. Команды в DemonScript можно разделять точкой с запятой, но ставить её, обычно, не обязательно. Впрочем, при появлении ошибки парсинга, стоит поставить разделитель ";" между командами. Например, out F (2+3) - это явная неоднозначность (или две команды или вызов функции F(2+3)).

Команды можно записывать как в различных строках, так и в одной. Так, первая сторока в примере выше эквивалентна следующему коду:

var X
out X.type()
или
var X;  out X.type()

В операциях сравнения X==Y, если тип переменных не совпадает, то будет возвращаться ложное значение False. Когда в переменную присваивается значение другого типа, её тип меняется (как это происходило в примере выше).


Работа с числами

Числа могут иметь дробную часть (число с плавающей точкой). Если дробной части нет, число считается целым. Операции с числами достаточно традиционны. Их можно складывать, вычитать, умножать и делить, используя скобки для группировки операци й:

out (2 + 3) * 4 - 5/2              //> 17.5
out  7 \ 2                         //> 3       : целочисленное деление
out  5.3 % 2                       //> 1.3     : остаток от деления на 2 целой части числа
Вычисления происходят слева направо. Приоритет операций приведен в справочнике.

Различные математические функции доступны как методы объекта Math:

out Math.pow(2,3) - 1              //> 7       : возведение в степень
out Math.random()                  //> 0.97166 : случайное число из интервала [0...1)
out Math.round(1.5)                //> 2       : округление

Для прибавления или вычитания единицы из переменной служат операции ++ и --. Изменение значения переменной происходит после вычисления выражения:

var I = 1
I++  out I                         //> 2       : тоже, что I = I + 1
out  I++                           //> 2
out  I                             //> 3
I--  out I                         //> 2       : тоже, что I = I - 1


Логические значения

Существуют три базовые константы: True (истина), False (ложь) и Undef (неопределено). Последняя означает, что информации о логическом значении нет, и оно в дальнейшем может оказаться как истинным, так и ложным. Для True и False справедливы стандартные таблицы истинности для операций логического отрицания: !X, логического ИЛИ: X | Y (дизъюнкции) и логического И: X & Y (конъюнкции). Например:

!True == False     (False & True) == False     (False | True) == True
Двойное равенство == означает сравнение логических значений. Если они совпадают, то результатом этой операции будет истина True, иначе - ложь False. Неопределённое значение Undef образует такую таблицу:
(False | Undef) == Undef   (True | Undef) == True    (Undef | Undef) == Undef  
(False & Undef) == False   (True & Undef) == Undef   (Undef & Undef) == Undef
Кроме этого, !Undef == Undef. Например, если высказывание не определено, то True & Undef также будет неопределённым, так как Undef может оказаться как истиной, так и ложью.

Обратим внимание на скобки. Сравнение == имеет более высокий приоритет, чем логические операции &,|. Поэтому выражение False | Undef == Undef без скобок скриптом будет интерпретировано как False | (Undef == Undef). Такое соглашение несколько отлично от принятого в логике, но типично для языков программирования.

В общем случае логическое значение может задаваться двумя неотрицательными вещественными числами. Они перечисляются через запятую в круглых скобках: (a0, a1). Число a1 интерпретируется как степень нашей уверенности в истинности высказывания, а a0 - как степень уверенности в его ложности. При этом a0 + a1 <= 1:

out (0, 1) == True,  (1, 0) == False,  (0, 0) == Undef

var A = (0.1, 0.8), B = (0.5, 0.5)
out A & B                          //> (0.5, 0.3)

В интервальной нечёткой логике базовые логические операции определены следующим образом:

   !(a0, a1) == (a1, a0)
   ( (a_0,a_1) | (b_0,b_1) ) == ( max(a0+b0-1, 0), max(a1, b) )
   ( (a_0,a_1) & (b_0,b_1) ) == ( max(a0, b0),     max(a1+b1-1, 0) )
Если X=(a0,a1), то к значению a0 можно получить доступ при помощи конструкции X[0]. Аналогично, X[1] равно a1.


Строки

Строковые константы заключаются в двойные кавычки. Строки можно складывать (конкатенация строк) и складывать с числом (оно преобразуется в строку). Метод .size() возвращает длину строки:

out "aaa" + "bbb"                  //> aaabbb
out "aaa" + 5                      //> aaa5
out "aaa".size()                   //> 3        : длина строки


Массивы

Массив - это список констант или переменных любых базовых типов. Элементы массива заключаются в квадратные скобки и перечисляются через запятую:

var A = [1, 2, True]
out A.type()                       //> array
out A                              //> [1, 2, True]
out A[2]                           //> True
A[0] = False                       //> меняем элемент массива
out A                              //> [False, 2, 3]
out A.size()                       //> 3                : число элементов в массиве
К элементам массива возможен индексный доступ, в котором первый элемент имеет нулевой индекс. Выше A[2] возвращает третий элемент.

Элементы массива можно добавлять в массив и убирать их из него:

var A = []                         // пустой массив
A.push(1)                          // добавили один элемент 
A.push(2, 3)                       // ещё два в конец
out A                              //> [1,2,3]

out A.pop()                        //> 3         : выталкиваем из конца
out A                              //> [1,2]    
Аналогично, при помощи функций .unshift() и .shift() можно добавлять и извлекать элементы в начале массива. Элементы можно случайно перемешивать функцией .mix(). Функция .clear() очищает массив, а .empty() возвращает True, если массив пустой. Полный перечень методов см. в справочнике.

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

var A = [[1,2], [3,4]]             // двумерный массив
out A[0]                           //> [1,2]
out A[0][1]                        //> 2

При присваивании переменных типа array происходит не копирование данных, а передача ссылки на данные. Переменные ссылающиеся на один и тот же массив, могут одновременно менять его элементы. Чтобы скопировать данные, необходимо вызвать метод .copy():

var A = [1,2,3]       
var B = A                          //> это тот же массив
var C = A.copy()                   //> это другой массив (копия)
B[0] = 0
C[0] = 4
out A,B,C                          //> [0,2,3] [0,2,3] [4,2,3]

Логическое значение можно преобразовать в массив:

var X = (0.1, 0.8).array()
out X
Аналогично, при помощи .logic() можно массив превратить в логическое значение.


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

Условный оператор if состоит из условия (логическое выражение) и набора команд, которые выполняются, если условие условие истинно (True). Команды окружаются фигурными скобками. Если команда одна, скобки можно опустить, но после условия необходимо поставить двоеточие:

var X = 5

if X < 7 :
   out "ok1"
   
if X > 3 & X < 7 {
   X++
   out "ok2"
}
else:
   out "not ok"

В общем случае структура оператора if имеет следующий формат:

if COND { PROG1 }    // выполняются команды PROG1, если условие COND == True 
false   { PROG2 }    // выполняются команды PROG2, если условие COND == False
undef   { PROG3 }    // выполняются команды PROG3, если условие COND == Undef 
else    { PROG4 }    // выполняются команды PROG4, если всё выше не выполнилось
Операторы false и undef можно менять местами, но else должно всегда быть последним. Как и с основным блоком, если команда одна, то фигурные скобки не нужны:
var X = (0.2,0.8)

if X    : out "X is true"
   false: out "X is false"
   undef: out "X is undef"
   else : out "X is something else" // сработает здесь

Условия можно также использовать в выражениях при помощи следующих конструкций:

E ? A:B         // если E==True, то A, иначе B
E ? A:B:C       // если E==True, то A, если E==False, то B, иначе С
E ? A:B:C:D     // если E==True, то A, если E==False, то B, если E==Undef, то С, иначе D
Например:
out (False? 1 : 2 : 3 ) + 10        //> 12


Циклы

Цикл while повторяет вычисление набора команд внутри фигурных скобок до тех пор, пока истинно условие, идущее после оператора while. Если команда только одна, то фигурные скобки можно опустить, но после условия поставить двоеточие:

var I = 0
while I < 5 : out I++ 
В циклах можно использовать операторы break и continue: Например, следующий цикл выведет только нечётные числа:
var I = 0
while I < 10 { 
   I++
   if I % 2 == 0 : continue
   out I
}

Есть ещё один цикл for. Его можно использовать в различных случаях. Например, при помощи оператора-функции range он производит вывод целых чисел 1,2,...,9:

for I in range(1,10) :               
   out I              
Переменную цикла I, стоящую между операторами for и in объявлять не надо. В общем случае range имеет три аргумента: range(I0, max, step), где I0 - начальное значение переменной цикла I; цикл работает, пока I < max; и step - прирост переменной цикла: I=I+step (по умолчанию step=1).

С помощью цикла for можно также перебирать элементы массива:

for X in [1, 2, 3]:
   out X
Как и в цикле while в нём допустимы операторы break, continue и фигурные скобки для группировки нескольких команд цикла.


Видимость переменных

В DemonScript принято стандартное для языков программирования соглашение о видимости переменных. Если при помощи var выше блока объявлена переменная, то она "видна" и в блоке (исключением являются блоки, образующие определения функций-демонов def). Переменные же, объявленные внутри блока, ниже его недоступны (являются локальными переменными). Более того, внутри блока можно объявить переменную с тем же именем, что и выше. Тогда это будет новая переменная. Например:

var X = 1
if True
{
   out X                           //> 1 ещё "глобальная"
   var X = 2
   out X                           //> 2 уже "локальная"
}
out X                              //> 1 "глобальная" не портится

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

Ввод - вывод

Оператор out выводит список констант и переменных, идущих после него. По умолчанию, после этого вывода происходит переход на новую строку. Если в конце списка поставить запятую, перехода на новую строку не будет. Кроме этого, запятая в списке означает пробел между выводимыми величинами. Если поставить подряд две запятые, то пробела не будет:

out 0,1,,2,
out 3,,
out 4
В результате будет выведено: 0 12 34.


Работа с файлами

Для работы с файлами служит объект File. Создадим файл с именем "out.txt" и запишем в него строку "Hi file":

var F = File.open("out.txt", "w")                 // открыть файл с именем "out.txt" на запись
File.out(F, "Hi file")                            // выводим в файл F строку
File.close(F)                                     // закрыть файл
Метод .open возвращает идентификатор файла (это просто имя файла), который далее используется в других методах File. Если идентификатор равен пустой строке, файл открыть не удалось. В методе .open второй параметр принимает значения: "w" - переписать, "a" - дописать в конец, "r" - читать.

Приведём пример загрузки файла в строку:

var F = File.open("out.txt", "r")                 // открыть файл с именем "out.txt" на чтение
if F.size() > 0 :                                 // если открыть удалось
   out File.in(F)                                 // читаем весь файл в одну строку

Можно также читать файл построчно:

var F = File.open("out.txt", "r")                 // открыть файл с именем "out.txt" на чтение
while !File.eof(F):                               // пока не конец файла
   out File.getline(F)                            // читаем из него одну строку
Ещё один метод var Arr = File.getlines(А) позволяет загрузить все строки файла в массив строк.