Давненько не писал о GScript! Между тем, VM языка уже пригодна для использования в играх, а спецификация пополнилась множеством интересных фич, о которых я сейчас и расскажу.
Изоляция памяти потоков
О потоках в GScript3 я уже писал в отдельном посте. Разрабатывая модель памяти языка, я придумал совершенно новую парадигму, которую в существующих реализациях не видел. Каждый поток имеет свою изолированную арену для динамических аллокаций (с размером блока в 1024 байта). Ссылочные типы — объекты, массивы, динамически созданные строки — в контексте потока создаются в этой арене и по умолчанию помечаются как принадлежащие этому потоку (у каждой переменной GsDynamic есть поле владельца — owner). VM запрещает ссылке покидать изолированный контекст, проверяя владельца при операциях внешней передачи — например, при записи в глобальную переменную. Исключение делается только для главного потока, который имеет право писать куда угодно без ограничений.
Такой подход гарантирует, что данные, созданные потоком, не переживут его самого, и в памяти машины не будет висячих ссылок, что очень полезно для платформы, не имеющей сборщика мусора. Но это несколько ограничивает полезность потоков, поэтому я предусмотрел два вида исключений из этого правила. Если создать данные со спецификатором shared, они будут размещены в арене главного потока и, таким образом, будут валидны вплоть до завершения работы скрипта (в терминах GScript — будут разделяемыми):
const thread = spawn func
{
const arr1 = shared [10, 5, 6];
const arr2 = [10, 5, 6];
global.arr = arr1; // ok
global.arr = arr2; // error!
};
shared, строго говоря, применяется не к данным как таковым, а к операции конструирования. Например, его можно применить к конкатенации строк, чтобы результат был разделяемым:
const s = shared("a" ~ "b");
Помимо этого, есть ключевое слово escape. Оно, как и shared, применяется к результату выражения. Оно берет неразделяемые данные и делает их разделяемыми — то есть, якобы принадлежащими основному потоку. Это позволяет передать ссылку во внешний контекст, даже если она указывает на изолированную арену.
func test(value)
{
print value;
}
const thread = spawn func
{
const arr = [0, 1, 2];
test(escape arr);
};
Это небезопасно и введено для тех случаев, когда программист знает, что делает. В основном, такая передача нужна для вызова в потоке чистых функций, которые не оставляют нигде глобальных висячих указателей.
Каналы
Канал — это концепция из Go, примитив, реализующий одновременно синхронизацию и межпоточный обмен данными. При вызове метода send поток-производитель блокируется до тех пор, пока сообщение не будет получено другим потоком. При вызове метода receive поток-потребитель блокируется до тех пор, пока не появится доступное сообщение.
const ch = global.channel();
const thread1 = spawn func
{
print ch.receive();
ch.send("world");
};
const thread2 = spawn func
{
ch.send("hello");
print ch.receive();
};
Приведенная программа выведет строку «hello», затем строку «world». Порядок гарантируется, потому что сначала будет запущен первый поток, который ждет сообщения «hello» от второго, а затем отправляет ему «world».
Проверка типов и обработка ошибок
Тип динамического значения можно сравнить с константой типа с помощью оператора :
const x = 10;
if (x: Number)
{
// ...
}
Альтернативно, тип можно получить в качестве числового значения, используя ключевое слово type, и сравнить с константой с помощью обычных операторов сравнения. Это полезно для логики типов:
if (type(x) != Null)
{
// ...
}
if (type(x) == type(y))
{
// ...
}
Константы типов в языке следующие:
Null = 0
Number = 1
String = 2
Array = 3
Object = 4
NativeMethod = 5
NativeFunction = 6
Error = 7
Function = 8
Vector = 9
Вы, наверное, заметили, что отдельной константой типа является Error — «ошибка». Ошибка в GScript3 действительно является отдельным типом переменной. Это позволяет не использовать пресловутый Null для обработки ошибок (хотя отказ от Null вообще — фича крайне спорная и даже безумная, я этого не стал делать) и одновременно — реализовать очень простую, не перегружающую синтаксис обработку ошибок на уровне VM.
Простейший случай — функция просто возвращает ошибку:
func myFunc()
{
return error("BADBEAF");
}
Значением ошибки является строка, которую вызывающий код может вывести в консоль.
const thread = spawn myFunc;
let running = true;
while(running)
{
running = thread.running;
const result = await thread;
if (result: Error)
{
print result;
}
}
В некоторых тяжелых случаях поток должен экстренно завершить выполнение не в основной функции, а где-то в недрах контекста. Для этого я предусмотрел оператор raise. Это что-то напоминающее исключение, но сходство лишь синтаксическое. raise преждевременно завершает работу потока, сохранив значение ошибки в специальный слот. Вызывающий код отлавливает это состояние и реагирует на него. Отличие от исключений в том, что мой подход не имеет специального синтаксиса обработки ошибок — она делается в рамках обычной синхронизации через await:
func test()
{
raise error("Something");
}
func threadFunc()
{
test();
}
const thread = spawn threadFunc;
let running = true;
while(running)
{
running = thread.running;
const result = await thread;
if (result: Error)
{
print result;
}
}
Векторы
Свежее нововведение. Поскольку GScript3 создан с прицелом на игры, в языке не помешает векторный тип данных, и теперь он есть. Поддерживаются арифметические операции и встроенные функции (интринсики) для ускорения вычислений:
const v = vector(3, 2, 1) + vector(1, 2, 3);
print v;
print normalize(v);
Вектор является стековой структурой и хранится в стандартном GsDynamic.
VM реализует следующие интринсики для векторов: dot, cross, normalize, distance.
В этом посте я не затронул еще две важные темы — AST-макросы и библиотеки байт-кода. Они заслуживают отдельных статей.

















