LizardScript - язык программирования:
- Скриптовый - LizardScript создан для взаимодействия с программами, написанными на C++. Он имеет наибольшее право называться скриптовым.
- Объектный - с помощью генератора метаданных METAGEN и скрипта make_metadata.py (спасибо Лёхе) из классов C++ извлекаются метаданные членов, и они становятся видимы в контексте LizardScript. Объявлять собственные классы в данный момент невозможно.
- Динамический - во время выполнения кода LizardScript можно компилировать новые функции с исходным кодом, заданным строковыми переменными.
Программист на LizardScript называется скриптолиз.
FAQ: Зачем ещё один скриптовый язык?
- А скриптовый язык, пользующийся классами С++ как родными?
Релиз первой официальной версии: | 11.10.2019 |
Автор: | killer-nyasha |
Расширение файлов: | .ls |
Последняя версия: | v0.1.0 |
Релиз последней версии: | 11.10.2019 |
Система типов: | Слабая статическая |
Экспериментировать с LizardScript можно в программе LizardScriptInteractive. При запуске с параметром командной строки она выполнит скрипт из файла, заданного параметром. Иначе откроется интерактивный режим, в котором можно вводить скрипты и сразу же видеть результат.
В LizardScriptInteractive введены метаданные следующих классов С++:
struct B
{
int x = 1;
float f = 1;
B* b = nullptr;
B() { }
};
struct A
{
int i = 10;
float f = 20.0f;
int k = 30;
A* a = nullptr;
B* b = nullptr;
void test()
{
f *= 2;
}
void test2(int a, int b)
{
f = a + b;
}
A()
{
i = 100;
}
};
this
- объект класса A.
Внимание: каждая строка (физическая, не строка кода), введённая в интерактивном режиме - это новая функция. Поэтому локальные переменные, объявленные в одной строке, в следующей уже не будут существовать. Все действия с локальной переменной должны быть в той же строке. Состояние объектов классов и их полей сохранятеся между строками.
Первый скрипт:
lsl.print("Hello world!");
- Токен - это любое смысловое «слово» языка: оператор, литерал или идентификатор.
- Строки кода разделяются точкой с запятой.
- Фигурные скобки позволяют объявить блок кода
{
}
. Блок кода эквивалентен одной строке. - Разрывы строк и табуляция в исходном коде интерпретируются как пробелы.
Разрывы строк и знаки табуляции интерпретируются как пробелы.
Внимание: в данной версии все не-ascii символы кроме комментариев и строковых литералов выводят предупреждение и интерпретируются как пробелы.
Доступны комментарии в стиле С++:
- От символов
//
до конца строки - От символов
/*
до символов*/
Внимание: в отличие от С++ многострочные комментарии поддерживают вложенность.
Переменные объявляются почти так же, как в С++:
<тип> <имя>
Где <тип> и <имя> - токены.
int myInt;
Можно сразу же присвоить ей значение:
int myInt = 1;
Интересно, что объявить переменную можно прямо внутри выражения, тут же использовав её значение:
lsl.print(int myInt = 1);
Область видимости локальной переменной заканчивается при выходе из ее блока кода. При совпадении имён более новые определение в своей области действия перекрывают старые.
int a = 15;
{
int a = 25;
lsl.print(a); //25
}
lsl.print(a); //15
В LizardScript есть три способа задать строку.
$<токен>
Самый короткий, но самый ограниченный способ. Возник ненамеренно. Подходит только для строк, состоящих из одного токена."<любые символы>"
. Всем привычный способ. Не будет работать, если строка содержит кавычки.[<любые символы>]
. Самый продвинутый, но непривычный способ. Позволяет строке включать и кавычки, и корректно закрытые квадратные скобки, поэтому особенно удобно для хранения исходного кода в переменной. Примеры:
stringptr source = [ lsl.print([hello world]); ];
stringptr source = [ lsl.print("hello world"); ];
stringptr source = [ lsl.print($hello); ];
LizardScript может вызывать функции-члены классов C++. Поддерживается перегрузка функций. Простейший пример - функция lsl.print. На самом деле это несколько перегрузок для разных типов.
Внимание: порядок проверки, подходит ли каждая из перегрузок функций к типам аргументов, зависит от порядка передачи метаданных этих перегрузок в METAGEN. Для простоты можно считать, что это зависит от воли рептилоидов, и избегать в этой версии перегрузок, вызывающих неоднозначности в выборе.
lsl
в примерах выше - это ключевое слово, которое возвращает указатель на объект класса LizardScriptLibrary.
В этот класс помещены системные функции, которые могут понадобиться в коде на LizardScript.
void print(int);
void print(float);
void print(stringptr);
void eval(void* ths, stringptr type, stringptr source);
Три перегрузки print выводят свой входной параметр на консоль. Всё сложнее с функцией eval - она во время выполнения вызывает компилятор LizardScript, динамически компилируя и выполняя код.
lsl.eval(this, "A", [
lsl.print("Hello world!");
lsl.print("from eval =)");
]);
В данном примере компилятор для объекта this типа A выполняет код в квадратных скобках. Весь промежуточный вывод компилятора при выполнении eval выводится на консоль так же, как при компиляции основного скрипта.
Объявив переменную, например, int x;
, мы можем в одном из сообщений компилятора заметить, что тип выражения x
- int*
, а не int
.
При этом, например, 10
будет иметь тип int
.
Почему?
В LizardScript нет понятий lvalue и rvalue, как в C++11.
Адрес переменной, по которому мы можем что-то записать, или передать переменную по ссылке (как lvalue), имеет тип <тип>*
- он является указателем на значение переменной.
Просто <тип>
- тип уже извлечённого значения переменной, и не имеет адреса (как rvalue).
Таким образом, x
- ещё только указатель на переменную, так как не было необходимости извлекать значение.
Почти всегда при использовании <тип>*
он будет неявно преобразовываться в <тип>
.
В данный момент LizardScript имеет следующие типы:
int
. 32-битное знаковое целое. Задается числовым литералом.float
. Число с плавающей запятой одинарной точности. Задаётся числовым литералом.std::nullptr_t
. Возвращается ключевым словомnull
. Невозможно объявить переменную.void
. Невозможно объявить переменную.stringptr
. Умный указатель наstd::string
. Задается строковым литералом.- Другие типы из C++, переданные в METAGEN. Видимыми будут только те их члены, которые переданы.
- Указатели.
<тип>*
, где<тип>
- абсолютно любой тип (даже указатель). Возможны, например,int**
илиstringptr****
.
Указатели объявляются так же, как в C#. В отличие от С++ звездочка при объявлении - часть типа, а не названия переменной.
Внимание: в данной версии LizardScript тип переменной - это один токен. Звёздочка не может входить в токен, поэтому типы указателей оборачивают в "backticks":
`текст`
Все типы указателей имеют размер 4/8 байт, зависит от платформы, не зависит от типа указателя.
Правильно:
`int*` x;
Неправильно:
int *x;
Неправильно в данной версии, будет добавлено позже:
int* x;
В данной версии нормального механизма преобразований типов нет. Доступны следующие неявные преобразования, где <тип>
- любой тип:
<тип>* -> <тип>
int -> float
std::nullptr_t -> <тип>
<тип> -> void
Там, где требуется void
, любой тип будет передан без реальных преобразований, как "сырые" данные.
Тип std::nullptr_t
, наоборот, преобразуется в любой тип - так ключевое слово null
заполняет нулями переменную любого типа.
<тип_не_указатель>* = <тип_не_указатель>
<тип_не_указатель>** = <тип_не_указатель>*
int i;
i = 5;
`int*` p = i;
p = i = 5; //возвращает свой левый аргумент, право-ассоциативно
int + int
int - int
int * int
int / int
float + float
float - float
float * float
float / float
Если один из параметров int
, а другой - float
, int
будет преобразован во float
.
int < int
int > int
Возвращает int
, равный единице, если выражение истинно, и нулю, если ложно.
int++
int--
++int
--int
Внимание: в данный момент любой инкремент или декремент работает как префиксный в C++.
int i = 10;
lsl.print(i++); //11
i = 10;
lsl.print(++i); //11
Как операторы, но на самом деле не совсем =)
Определяют приоритеты действий. Без скобок приоритеты операторов такие (фрагмент исходного кода компилятора):
("="), 20
(">"), 30
("<"), 30
("+"), 50
("*"), 60
("-"), 50
("/"), 60
("++"), 90
("--"), 90
Также в круглые скобки оборачиваются аргументы функций.
Определяют блоки кода, подробнее см. [Немного о синтаксисе][]
<токен1>.<токен2>
Ищет идентификатор <токен2>
, принадлежащий <токен1>
.
Приоритеты поиска идентификаторов Когда LizardScript встречает токен, он проверяет, является ли он оператором. Если да, то приоритеты такие:
- Унарный оператор.
- Бинарный оператор.
- Ошибка компиляции. Если нет, то
- Локальная переменная. При совпадении имён более новые определение в своей области действия перекрывают старые. Если было явно указано
this.<токен>
, пропускаем этот пункт. - Функция класса
this
. - Поле класса
this
. lsl
- Тип (возможно, это объявление переменной).
- Ошибка компиляции.
Возвращает объект, для которого запущен скрипт.
В сочетании с оператором .
даёт возможность обратиться к членам класса, если есть одноимённая локальная переменная.
Возвращает объект, который при присвоении обнуляет любую переменную.
Указывает на глобальный объект LizardScriptLibrary.
Создаёт в куче новый объект. Действует аналогично оператору new
из C++.
Синтаксис:
new <тип>
new <тип>(<параметры конструктора>)
`A*` a = new A;
Обычно функция, импортированная из C++ через METAGEN, имеет то же имя.
Исключение - конструктор, он имеет имя ctor
.
Конструктор, если он есть, вызывается после создания объекта.
Внимание: конструктор без параметров вызывается только если после имени типа добавить круглые скобки.
Внимание: в данной версии есть проблемы с импортом конструкторов с параметрами.
`A*` a = new A; //память не инициализирована
`A*` b = new A(); //вызывает A::ctor();
Интересно, что конструктор является обычной функцией, возвращающей void
, и его можно вызывать, например, чтобы сбросить состояние объекта.
`A*` a = new A;
a.ctor();
`A*` a = new A();
a.i = 25;
a.ctor();
Внимание: new
без конструктора может плохо закончиться, если в объекте есть указатели. LizardScriptInteractive стремится вывести информацию о содержимом указателей, и если null
он может обработать корректно, то случайное значение - нет.
Поддерживается ветвление if
и цикл while
. Синтаксис следующий:
if ( <условие> ) <действие>
if ( <условие> ) <действие> else <действие иначе>
while ( <условие> ) <действие>
Как условие передаётся int
, считающийся истиной, когда не равен нулю.
С else if
есть проблемы. Оставлю этот пример здесь, но в этой версии он неправильно работает.
if (i > 11) lsl.print(1); else if (i > 12) lsl.print(2); else lsl.print(3);
Примеры
int power = 0;
if (power > 9000)
lsl.print("over 9000!!");
else lsl.print(":(");
int power = 0;
while (power++ < 9000)
{
if (power > 8990)
lsl.print(power);
}
lsl.print("over 9000!!");
Вложенные циклы:
int a = 4;
while (a)
{
int b = 3;
while (b)
{
lsl.print(100*a+b);
b--;
}
a--;
}
Тело цикла или условия может быть и пустым.
int power = 0;
while (power++ < 9000);
lsl.print("over 9000!!");
int y = 1;
int n = 100;
int x = 1;
int j = 2;
while (j < n)
{
y = x + y;
x = y - x;
j++;
}
lsl.print("fib(");
lsl.print(n);
lsl.print(") = ");
lsl.print(y);