GScript3
В D все очень плохо со встраиваемыми скриптовыми языками. Перепробовал много вариантов — Lua, Python, AngelScript — везде боль, все делается через неимоверно сложные API, где элементарно привязать к скрипту свою функцию — это целый квест. Чтобы, например, использовать биндинг dangel, нужно патчить рантайм языка для поддержки соглашения вызовов функций D, без этого там ничего не будет работать. Под Lua-биндинги очень сложно найти нужную версию библиотеки, чтобы приложение не крашилось. Pyd, привязка Python — вообще какой-то фантастически запутанный фреймворк из compile-time костылей. Нативные языки, написанные на D, по большей части устаревшие, неподдерживаемые и тупо не компилируются.
Поэтому я уже много лет назад начал пилить свой язык GScript. Изначально это был больше учебный проект, у меня все не хватало времени довести до ума виртуальную машину — первые два варианта GScript являются просто прототипами. Третья итерация, кажется, приобретает уже законченный вид.
Это будет, как обычно, JavaScript-подобный язык с динамической типизацией, компилируемый в байт-код. Я сильно упростил ISA и саму виртуальную машину, избавившись от лишних аллокаций, и теперь код выполняется намного быстрее — также он гораздо эффективнее сериализуется для сохранения в бинарный файл.
Вот как выглядит простейший «Hello, World»:
print "Hello, World!";
Синтаксис во многом аналогичен базовому JS (функции, объекты, массивы), но я ввел несколько важных отличий. Например, вариативные аргументы у меня реализованы при помощи специального оператора $
, а список именованных аргументов необязателен.
func test
{
print $0;
print $1;
print $2;
}
test(5, 10, 20);
Список всех аргументов вызова можно получить оператором $$
. Он не создает новый массив, а использует срез буфера аргументов во фрейме, так что механизм очень эффективный:
func test
{
const args = $$;
print args.length;
args[0] = 100;
print $$; // первый аргумент теперь 100
}
test(5, 10, 20);
Объекты используют прототипное ООП, как в JS, и при вызове метода неявно передают ссылку на себя первым аргументом, как в Python. При наследовании от прототипа данные в новый объект не копируются, а просто вставляется «секретная» ссылка на прототип, которая используется для доступа к свойствам.
const Obj = {
prop: "bar",
test: func(self)
{
print self.prop;
}
};
const f = new Obj;
Obj.prop = 10; // свойство изменили в прототипе
f.test(); // выведет 10
f.prop = 5;
f.test(); // выведет 5 (переопределили в объекте-наследнике)
Пожалуй, самым необычным в языке является механизм модулей и импорта. Я намеренно не стал делать неймспейсы — все глобальные сущности программы живут в одном пространстве имен, как в C. Импорт осуществляется путем обращения к специальному объекту global
, в который нужно сохранять все, что должно быть кросс-модульно. Фактически модули просто конструируют глобальные прототипы объектов, которые затем можно использовать откуда угодно (похожая идея, только с классами, реализована в Haxe).
foo.gs:
global.Foo = {
prop: "Foo.prop",
test: func(self)
{
print self.prop;
}
};
main.gs:
import Foo from "foo.gs";
Foo.test();
Foo
в данном случае — это просто автоматически созданная компилятором константа, хранящая global.Foo
.
Такой подход означает, что в модулях лучше не объявлять глобальные переменные и функции, чтобы не создавать потенциальные конфликты имен.
Скомпилировать и выполнить скрипт очень просто, это буквально пять строк:
import gscript;
void main()
{
string script = "print \"Hello, World!\";";
GsInstruction[] bytecode = compile(script);
auto vm = new GsVirtualMachine();
vm.load(bytecode);
vm.run();
}
Легко привязывать глобальные функции:
GsDynamic printSum(GsDynamic[] args)
{
auto vm = cast(GsVirtualMachine)args[0].asObject;
auto a = args[1].asNumber;
auto b = args[2].asNumber;
writeln(a + b);
return GsDynamic(0);
}
vm["printSum"] = &printSum;
На стороне скрипта:
global.printSum(5, 3);
На текущей стадии уже можно привязывать целые классы — достаточно предоставить кастомную реализацию интерфейса GsObject
:
class TestObj: GsObject
{
int x = 0;
this()
{
}
GsDynamic foo(GsDynamic[] args)
{
writeln("TestObj.foo called");
return GsDynamic(0);
}
GsDynamic get(string key)
{
if (key == "x")
return GsDynamic(x);
else if (key == "foo")
return GsDynamic(&foo);
else
return GsDynamic(0.0);
}
void set(string key, GsDynamic value)
{
if (key == "x")
x = cast(int)value.asNumber;
}
bool contains(string key)
{
if (key == "x") return true;
else if (key == "foo") return true;
else return false;
}
GsObject dup()
{
TestObj copy = new TestObj();
copy.x = x;
return copy;
}
}
TestObj test = new TestObj();
vm["test"] = test;
На стороне скрипта:
global.test.x = 10;
print global.test.x;
global.test.foo();