Синопсис
Scope - это базовая специфическая структура, по имени которой назван фреймворк.
Для описания Scope структур, испольузется JSON нотация.
Условно, Scope объединяет в себе свойства понятий:
- Класс
- Множество
- Неймспейс
Класс
Как “Класс”, Scope реализует принцип ООП, позволяющий создавать множества объектов, описывать их свойства и методы, а также включать статические свойства и методы
// имя класса
Point: {
// статическое JS/GPU свойство класса Point.simSpeed
'float simSpeed': 0.1,
// динамические GPU свойства экземпляров класса Point[i].pos
pos: 'float2',
vel: 'float2',
// статический/динамический GPU метод класса
'void method()': `
// ...
`,
// статический JS метод класса
method(){
// данный класс
this;
// JS обращение к статическому свойству и методу класса
this.simSpeed *= 1.01;
this.method();
// запуск 10 потоков GPU рантайма в контексте класса
this(10,`
// GPU side code "kernel"
// в каждом потоке параллельно создаём по одному экземпляру класса
Point.new {
// обращение к динамическому свойству экземпляра класса
.pos = {i,0};
// вызов динамического метода экземпляра класса
.method();
};
// вызов статического метода класса
method();
Point.method();
// чтение статической переменной класса (запись пока возможна только из JS)
simSpeed;
Point.simSpeed;
`);
},
}
Множество
Как “Множество”, Scope является контейнером всех экземпляров своего класса:
На уровне кода, Scope одновременно является массивом, содержащим все созданные на данный момент экземпляры класса:
Point[i] // обращение к экземпляру класса по индексу
Point.length // количество экземпляров класса (длина массива)
вот примеры кода, работающие с множеством экземпляров класса:
Point: {
go(){
// запускаем 1 поток GPU рантайма в контексте данного класса
this(1,`
// создаём три экземпляра класса
Point.new { };
Point.new { };
Point.new { };
// обращаемся к экземплярам класса по индексу
Point[0].pos = {0,0};
Point[1].pos = {0,0};
Point[2].pos = {0,0};
// определяем общее количество экземпляров класса
Point.length
// последовательный перебор всех экземпляров класса
Point {
.pos += .vel;
};
// входим в контекст определённого экземпляра класса по индексу
Point[2] {
// выполняем действие в контексте
.pos += .vel;
};
`);
// параллельный цикл по всем экземплярам класса
this.Point(`
.pos += .vel * simSpeed;
`);
// последовательный цикл по всем экземплярам класса
this(1,`
for ( int i = 0; i < Point.length; i++ ) {
Point[i] {
.pos += .vel * simSpeed;
};
};
`)
},
};
Неймспейс
Как “Неймспейс”, любой Scope может включать в себя описания других Scope
// неймспейс Engine
Engine: {
// неймспейс Engine.Ball
Ball: {
},
// неймспейс Engine.Spring
Spring: {
},
};
важным аспектом работы со Scope является понимание механизмов областей видимостей неймспейсов друг относительно друга:
1. изнутри каждого неймспейса видны все неймспейсы, объявленные на одном уровне с данным неймспейсом и выше по дереву
доступ к вложенным неймспейсам, не видимым напрямую, осуществляется в точечной нотации, через их предка, видимого относительно данного неймспейса
{
Engine: {
Ball: {
go(){
// из данной точки дерева область видимости выглядит так
// видны напрямую
this; // Ball
this.Spring;
this.Engine;
this.Creature;
// не видны напрямую
this.Body;
this.Mind;
// но видны так ( через видимого предка Creature )
this.Creature.Body;
this.Crature.Mind;
// несколько разных способов увидеть Ball из данной точки дерева
this;
this.Ball;
this.Engine.Ball;
this.Root.Engine.Ball;
},
},
Spring: {
},
},
Creature: {
Body: {
},
Mind: {
},
},
};
2. изнутри неймспейса видны все переменные, объявленные в данном неймспейсе и выше по дереву
Engine: {
'float simSpeed': 0.01,
Ball: {
'float collideForce': 0.01,
go(){
// видны напрямую
this.collideForce;
this.simSpeed;
this.Spring.resistForce;
// не видна напрямую
this.resistForce
// ещё видны так:
this.collideForce;
this.Body.collideForce;
this.Engine.Body.collideForce;
},
},
Spring: {
'float resistForce': 0.01,
},
}
3. особенности доступа к неймспейсу, переменным и статическим методам из JS и GPU
JS код транслирует текущий неймспейс как this.
Мнемонически, правильно понимать “this.” в Scope коде, как “здесь”:
точка, относительно которой мы обращаемся к данному неймспейсу и к другим неймспейсам
// данный неймспейс
this;
// другой неймспейс
this.Mind;
// неймспейс, вложенный в другой неймспейс
this.Mind.Neuron;
// вызов статического JS метода
this.method();
this.Mind.method();
this.Mind.Neuron.method();
// обращение к статическому свойству ( переменной )
this.someValue = 1;
this.Mind.someValue = 1;
this.Mind.Neuron.someValue = 1;
GPU код транслирует текущий неймспейс как Self, остальные неймспейсы, методы и свойства доступны напрямую без префикса “this.”
this(`
// данный неймспейс
Self;
// другой неймспейс
Mind;
// неймспейс, вложенный в другой неймспейс
Mind.Neuron;
// вызов статического GPU метода
// объявленного в данном или вышеописанном неймспейсе (в любом из предков)
mehtod();
// объявленного в другом неймспейсе
Mind.method();
// объявленного в неймспейсе, вложенном в другой неймспейс
Mind.Neuron.method();
// обращение к статическому свойству (переменной)
// (с GPU, на данный момент, переменные видны только для чтения)
// объявленной в данном или вышеописанном неймспейсе (в любом из предков)
someValue;
// объявленной в другом неймспейсе
Namespace2.someValue;
// объявленной в неймспейсе, вложенном в другой неймспейс
Namespace2.SubNamespace.someValue;
`);
4. область видимости GPU кода определяется неймспейсом, в контексте которого он запущен
{
Namespace1: {
'float property': 0,
go(){
// запуск в контексте Namespace1
this(`
// здесь видна
property;
// здесь не видна
property2;
// но видна так
Namespace2.property2;
`);
// запуск в контексте Namespace2
this.Namespace2(`
// здесь видна
property2;
// здесь не видна
property;
// но видна так
Namespace1.property
`);
},
},
Namespace2: {
'float property2': 0,
},
};
5. перекрытие областей видимости
Если во вложенном неймспейсе объявлен неймспейс, одноимённый вышележащему, то он перекрывает собой область видимости вышележащего неймспейса, но к вышележащему неймспейсу можно получить доступ через родителя.
Тоже самое справедливо и для переменных, а также для статических методов на GPU.
Node: {
'float property': 0,
Body: {
'float property': 0,
Node: {
},
go(){
// это переменная Body.property
this.property;
// это переменная Node.property
this.Node.property;
},
},
};
6. специальные псевдонимы неймспейсов
На данный момент, существует два специальных псевдонима неймспейсов:
this(`
// корневой анонимный неймспейс
Root;
// текуший неймспейс, в контексте которого запущен GPU код
Self;
`);
GPU код
описание Scope симуляций представляет из себя JS код, перемежающийся вставками GPU кода (так называемыми kernel’ами):
kernel код пишется как многострочный текст в обратных апострофах:
Scope: {
go(){
// GPU код:
// запуск параллельного GPU цикла по всем экземплярам данного класса
this(`
// итерируемый экземпляр Self[i]
.;
// свойство и метод итерируемого экземпляра
.pos;
.method();
`);
};
};
Корневое дерево
Scope симуляция описывается как одно большое анонимное JSON-дерево, у которого есть корень и иерархическая структура ветвей.
Доступ к корню этого дерева возможен по псевдониму Root.
// Root
Scope({
Creature: {
Body: {
Cell: {
},
Stick: {
},
},
Mind: {
Neuron: {
},
Synapse: {
},
},
},
});
Таким образом, для описания симуляции мы определяем корневое дерево симуляции, после чего вызываем корневой метод .go()
Scope({
Creature: {
Body: {
Cell: {
},
Stick: {
},
},
Mind: {
Neuron: {
},
Synapse: {
},
},
},
// корневой метод .go()
// аналог функции main() в Си
go(){
// создаём один Creature на старте
this(1,`
Creature.new {
};
`);
},
}).go();
Модули
Любой Scope может быть практически мгновенно превращён в модуль
Creature: {
Body: {
Cell: {
},
Stick: {
},
},
Mind: {
Neuron: {
},
Synapse: {
},
},
};
создаём файл body.scope
run.void
body.scope
с содержимым
module.exports = {
Cell: {
},
Stick: {
},
};
после этого заменяем секцию, описывающую ветку Body, на use(‘body’)
Creature: {
Body: use('body'),
Mind: {
Neuron: {
},
Synapse: {
},
},
};
таким образом, Scope код может быть в любой момент легко разбит на модули.
Модули, в свою очередь, также легко могут быть разбиты на подмодули:
Для этого нужно создать папку, одноименную модулю, поместить подмодули этого модуля в папку модуля, а сам модуль поместить в эту папку под именем index.scope, после этого, модуль можно подключать как use(‘body’)
body/index.scope
body/submodule.scope
run.void
Такая лёгкость важна для того, чтобы на начальном этапе комфортно описывать всю симуляцию в качестве одного дерева, ограничиваясь единственным стартовым файлом, и иметь возможность в любой момент вынести любую часть кода в отдельный модуль
Вообще, логика вложенных областей видимости и особенности поведения функции use(‘module’) преследуют именно возможность быстро и с наименьшими правками пластично менять иерархии неймспейсов, что очень удобно в условиях, когда модульная структура симуляции много раз перестраивается в процессе развития
Реляционная схема классов
Важной особенностью Scope является наличие двух параллельных систем взаимоотношений между классами:
- иерархическое дерево неймспейсов
- реляционная схема классов
Схема классов названа реляционной, потому что по дизайну копирует идею ORM:
Взаимоотношения между множествами экземпляров организуются в виде реляционных таблиц, связь определяется схемой данных с помощью ‘parent’ и [‘child’] ссылок.
Более того, технически, в памяти GPU множества экземпляров каждого класса образуют упорядоченные таблицы данных в памяти, у каждого экземпляра есть свой id, а ссылки являются int свойствами, интерпретируемыми как ссылки на объект с данным индексом в той таблице, на которую ссылаются свойство.
Сравним эти два примера:
// просто иерархическое дерево неймспейсов
Creature: {
Body: {
Cell: {
},
Stick: {
},
},
}
// реляционная схема классов
Creature: {
// свойство экземпляра Creature - ссылка на экземпляр Body
body: 'Body',
Body: {
// свойство экземпляра Body - множество ссылок на экземпляры Cell
cells: ['Cell'],
Cell: {
// свойство экземпляра Cell - множество ссылок на экземпляры Stick
sticks: ['Stick'],
},
// свойство экземпляра Body - множество ссылок на экземпляры Stick
sticks: ['Stick'],
Stick: {
// свойства-ссылки на экземпляры класса Cell
from: 'Cell',
to : 'Cell',
},
},
}
Здесь описан Creature, у которого есть один .body, у которого есть множество .cells и .sticks, причём Cell и Stick образуют граф, где Cell играет роль вершин, а Stick играет роль рёбер графа. При этом каждый Cell может помнить список всех .sticks, с которыми он связан, а Body может помнить полные списки всех .cells и .sticks, которые в него входят.
Реляционная схема классов может быть избыточной, и часто бывает таковой, так как системы ссылок носят прикладной алгоритмический характер. Однако, любую реляционную схему классов можно описать не избыточно, в этом случае обработать её алгоритмически всегда будет возможно, но не всегда - удобно и/или эффективно.
Разница между описанием Scope, вложенным в другой Scope и наличием свойства экземпляра класса, ссылающегося ‘одиночно’ или [‘множественно’] на экземпляры другого Scope, заключается в том, что иерархии вложенностей неймспейсов служат исключительно в целях образования пространств имён, и никаким образом не определяют реляционные взаимоотношения между связанными множествами экземпляров различных классов.
Более простой пример:
Creature: {
Body: {
},
go(){
this(1,`
// мы просто создаём экземпляр Creature,
// но это НЕ приведёт к созданию экземпляра Body,
// несмотря на то, что Body вложен в Creature
Creature.new {
};
`);
},
},
Creature: {
Body: {
},
go(){
this(1,`
Creature.new {
// мы просто создаём экземпляр Body,
// но он никак НЕ связан с только что созданным экземпляром Creature
Body.new {
};
};
`);
},
},
Creature: {
// определяем реляционную связь между Creature и Body
// каждый экземпляр Creature ссылается на экземпляр Body свойством .body
// при этом, на один и тот же Body может ссылаться как больше одного экземпляра Creature,
// так и не ссылаться ни одного
body: 'Body',
Body: {
},
go(){
this(1,`
// создаём экземпляр Creature
Creature.new {
// создаём экземпляр Body и кладём ссылку на этот экземпляр
// в свойство .body только что созданного экземпляра Creature
.body.new {
// здесь можем определить свойства только что созданного .body
};
};
`);
},
},
Контекст
- контекст экземпляра
- контекстный неймспейс
- вход в контекст
- .нотация
- some нотация
- контекст метода экземпляра - экземпляр
Child списки
Удаление экземпляров
По той причине, что экземпляры Scope хранятся в памяти GPU в виде упорядоченных таблиц, при необходимости удалить один или несколько экземпляров из любого множества, приходится дефрагментировать порядок элементов данного множества, чтобы заполнить пустоты.
В свою очередь, это приводит к обновлению индексов у всех сдвинувшихся элементов, что вызывает необходимость обновить все входящие ссылки на все сдвинувшиеся элементы.
Иными словами, если сравнивать с SQL, то любая операция удаления из таблицы приводит к тому, что многие записи данной таблицы меняют свой id, а все записи во всех таблицах, ссылавшиеся на записи данной таблицы, должны обновить свои ‘parent_id’ / ‘child_ids’
Конструкторы и деструкторы
Point[i] // обращение к экземпляру класса
Point.length // количество экземпляров класса (длина массива)
// добавление нового экземпляра класса.
// на данный момент может быть вызвано только в виде "входа в контекст" добавленного экземпляра класса
// внутри фигурных скобок "." - это this
// возвращать экземпляры классов как some point = Point.new(); пока не умеет, но будет уметь
Point.new {
. // номер добавленного экземпляра класса
Point[.] // данный экземпляр класса, извлечённый по номеру
.pos = {0,0}; // определение свойства
};
Наследование
Планируемый к реализации функционал
- @reverse ссылки
- сортировка и другие функции для child списков
- запись переменных из видеокарты
- полный доступ к данным из JS
- getter’ы и setter’ы
- развитие системы наследований
- временные списки данных
- сборщик мусора
- debug mode
- du -hs для Scope дерева
- META?
- документация по создаваемым симуляциям?
Проблемы, ожидающие решения
- невозможно создать дерево
- невозможно вызвать рекурсию
- .delete() иногда вызывает нарушения ссылочной целостности
- очень медленный .delete()
- много JS активности при запусках kernel’ов
- большой расход памяти служебными структурами данных
- двойной расход памяти служебными векторами remove