Синопсис

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
				};
			};
		`);
	},
},

Контекст

Child списки

Удаление экземпляров

По той причине, что экземпляры Scope хранятся в памяти GPU в виде упорядоченных таблиц, при необходимости удалить один или несколько экземпляров из любого множества, приходится дефрагментировать порядок элементов данного множества, чтобы заполнить пустоты.

В свою очередь, это приводит к обновлению индексов у всех сдвинувшихся элементов, что вызывает необходимость обновить все входящие ссылки на все сдвинувшиеся элементы.

Иными словами, если сравнивать с SQL, то любая операция удаления из таблицы приводит к тому, что многие записи данной таблицы меняют свой id, а все записи во всех таблицах, ссылавшиеся на записи данной таблицы, должны обновить свои ‘parent_id’ / ‘child_ids’

Конструкторы и деструкторы

Point[i]      // обращение к экземпляру класса
Point.length  // количество экземпляров класса (длина массива)

// добавление нового экземпляра класса.
// на данный момент может быть вызвано только в виде "входа в контекст" добавленного экземпляра класса
// внутри фигурных скобок "." - это this
// возвращать экземпляры классов как   some point = Point.new();   пока не умеет, но будет уметь
Point.new {
	.             // номер добавленного экземпляра класса
	Point[.]      // данный экземпляр класса, извлечённый по номеру
	.pos = {0,0}; // определение свойства 
};

Наследование

Планируемый к реализации функционал

Проблемы, ожидающие решения