Меня немного удручает ситуация когда из низкоуровневых языков на которых хочется и весело писать - почти ничего. Ну условно говоря у нас есть C++, на этом собственно и всё.
Какие есть альтернативы и чем мне они не нравятся? Ну для начала есть D. Это такие плюсы но чуть получше.
Есть Rust со своим борроу-чекером. Сама идея хорошая, но писать на подобных языках не шибко весело. Есть обычный C (маловато возможностей для метапрограммирования из коробки).
Есть невышедший всё еще JAI.
Но в любом случае они все в каком-то смысле друг на друга похожи, кроме может JAI. Все - представляют из себя такую еще алгольно-фортрановскую семантику. С типами там и так далее.
У нас есть переменная (коробочка хранящая значение). У нас есть структура. Которая определяет то как структура лежит в памяти. У нас есть методы обращения к этой структуре, а в иных случаях мы пишем обвязки в стиле полностью абстрагированных операций с айдюками.
После наваждения ООП, у нас случилось наваждение ФП. После наваждения ФП у нас сейчас потихоньку идёт разговор за перформанс.
И в плане перформанса предлагается использовать DOD. Иначе говоря организуйте свои данные не так как вы их бы разбили согласно предметной области. И даже не рассматривайте их как потоки данных, как в случае FP, преимущественно.
А типа садитесь и распологайте объекты согласно тому насколько хорошо они соответствуют предметной области. Но еще по тому принципу насколько хорошо их обрабатывает непосредственно железо.
Иначе говоря если вам приходится бегать между сотнями тысяч мест ради того чтобы сделать пару локальных изменений. ВЫ - что-то делаете не так.
Проще всего например разницу можно проиллюстрировать следующим образом:
Традиционный ООП к которому мы привыкли не очень заботится о том каким образом мы работаем со структурам.
Простой пример: допустим у нас есть тысяча квадратиков на экране. И для каждого из них нам надо посчитать позицию. Потом посчитать цвет зависящий от времени. Потом отсортировать по z координате и зарендерить.
Мы получим что-то вроде.
type object {
int32 x;
int32 y;
byte color;
byte* image_texture;
...
}
for (object of objects) {
...
object.setPosition(...)
object.setColor(...)
object.render(...)
...
}
При этом если у нас GC язык, то по чему мы бежим может распологаться чёрт знает где. И самое главное некоторые вещи ну просто нецелесообразно или не получится хранить там же где объект. Ну например текстуру объекта выходит дороговато хранить прямо в объекте, когда мы их передаём в другие места.
Мы могли бы организовать наш код немножко иначе. Заместо отдельных текстур у нас был бы например огромный буфер текстур.
objects {
size_t size;
byte* textures;
int32* pos;
byte* color;
}
for (uint32 i = 0; i < objects.size * 2; i += 2) {
setPosition(i, pos[i], pos[i + 1])
}
for (uint32 i = 0; i < objects.size; i += 1) {
ajustColor(i, color[i]);
}
for (uint32 i = 0; i < objects.size; i += 1) {
render(i);
}
Казалось бы в чём разница? Ну в том что например мы можем посмотреть что происходит в ajustColor. И увидеть что там происходит куча одинаковых похожих операций которые поделажет векторизации. Иначе говоря компилятор тут может увидеть потенциал для SIMD и многократно ускорить происходящее.
Но самое главно в далёком будущем мы сможем еще часть попробовать предзагрузить на видеокарту в качестве шейдера. И воспользоваться тем что она очень хорошо работает с кучей параллельных одинаковых действий.
В каком-нибудь коде для рендера мы могли бы пробегать по массиву фигур отсортированному по z координате. И отрисовывать каждый объект независимо через object.render(), совершенно не зная где он находится и что там происходит.
Иначе говоря у нас эдакий массив гетерогенных объектов, которые сами по сути являются ссылками на то что мы будем изменять. Мы прыгаем по коду и еще мы прыгаем очень активно по памяти.
Иногда конечно бывает и наоборот. И надо делать performance check. Чтобы типа выбрать чего из этого будет шустрее. При чём тут традиционное ООП? Ну при том что по сути наш код нарушает в каком-то смысле принципы ООП. Мы хотим иметь объекты каждый из которых ну весьма независим, изолирован имеет сложное скрытое поведение.
А тут мы наоборот не то что знаем о существовании других сородичей, мы этим напрямую балуемся. И это нормально.
Естественно когда речь заходит о подобных переменах в том как мы храним данные. У нас происходит много перемен в том какой у нас интерфейс для самих объектов. Как мы их создаём как с ними взаимодействуем.
Традиционный - объект это кусок памяти с сылками на другие объекты. А также с привинченными гвоздями методами - начинает работать хуже.
Гораздо лучше например какая-нибудь традиционная для лиспа (CLOS) модель разделенная: У нас есть данные. Они живут своей жизнью. И есть методы поверх этих данных. Функции по сути дела.
Но! Часто так бывает что во-первых мы изначально не думали что нам прям так жизненно необходим компонент. Во вторых нам весьма неприятно с подобными вещами работать когда нам таки приходится смотреть на объекты через призму независимых сущностей от их расположения в памяти.
В итоге сохраняется определенная дуальность которая в лучшем случае либо может быть разрешена compile-time оптимизациями. Либо метапрограммированием. Либо привычкой терпеть разные уродливые интерфейсы.
К большому сожалению самое адекватное и приятное метапрограммирование находится традиционно в Лиспах. А самое приятное взаимодействие напрямую с ресурсами типа памяти - находится в системных языках вроде Плюсов.
И получается достаточно неприятная вещь: приходится писать много бойлерплейта. С другой стороны сталкиваться с GC и всеми проблемами его обхода - тоже сталкиваться не хочется.
Есть так называемая 30 millions lines of code problem. По сути дела спикер говорит о том что у нас полная чехарда в плане хардваря. Поэтому нам необходимы драйверы. Поэтому нам необходимы операционные системы которые будут жить поверх этих драйверов.
Чтобы мы не писали свой собственный HAL. Не мучались с plug & pray так далее и тому подобное. Результатом конечно же становится та ситуация что у нас типа сколько… 3 операционных систем? Ну типа таких более менее серьезных. Может 4.
Каждая из которых так или иначе базируется на языке C и его конвенциях. Иначе говоря чтобы быть полезными а не самозамкнутыми, многим другим языкам, которые не являются C-подобными. Требуется поддерживать или хотя бы оборачивать вызовы к тому что было написано на C или имеет совместимый с ним интерфейс.
В конце концов всё это приводит к тому что если раньше вы просто делали memory map с девайса и писали читали значения. Теперь вам для создание каких-то интереных вещей нужно потратить не один час изучая каким образом рисовать например в окне. Потому что в одном месте у вас одна библиотека, в другом другая. А в третьем третья.
Если пойдёте и напишете свою - будет четвертая поверх этих трёх. Браво.
Поэтому такая простая задача которую захотел бы сделать любой человек - порисовать пикселями на экране. Ну не очень-то нынче и выполнима так вот сходу. Вам придётся пробираться через кучу разной дичи. Как исторической так и современной. Обходить всякие проблемы с безопасностью.
Вообще когда я об этом думаю, я вспоминаю невольно концепцию виртуальной памяти. Да нам приходится у операционной системы запрашивать кусок памяти, разблокировать странички.
Но в остальном сильная сторона виртуальной памяти состоит в том, что процессор не вынужден ни общаться с другими процессами ни знать о существовании на самом деле всерьез операционной системы.
Интерфейс крайне просто и понятный: мы живём в своей изолированной реальности. И делаем два вызова: разблокировать большой кусок памяти. И заблокировать обратно.
Представьте что заместо тысячи вызовов к библиотеке вы просто делаете 1 вызов. К 1 универсальной библиотеке. Открываете кусок памяти, пробрасываете указатель на начало памяти.
Пишете потом в этот буфер. Читаете приходящие значения с этого буфера.
Делаете 1 вызов который отключает устройство, девайс от нас. И делаете 1 вызов удаляя весь буфер к чёртовой матери.
По сути не очень-то много. Но на самом деле много. Для начала мы избавляемся от библиотек и конвенций вызова.
Чтение и запись в память - не конвенции вызова. Мы для начала почти полностью убили дикую зависимость от C библиотек. Сведя вопрос к реализации 2-х 4-х очень обобщённых функций. Для каждого конкретного девайса.
Дальше мы общаемся считайте через регион памяти. А не через вызовы функций. Это позволяет очень легко виртуализировать программы. Исполнять их на эмуляторах.
Позволяет за пару часов реализовывать удалённый доступ. Вам например потребуется всего лишь отправить по TCP координаты своей мышки приложению с другого конца Земли. А то и не заметит никакого подвоха. Потому что оно фундаментально не вкурсе и не знает что там происходит вокруг.
Для него нет никаких файлов. Операционных систем. Никаких драйверов и библиотек. Полная монада. Сделали 2-4 вызова и поехали работать.
Самое главное - никогда не умрём в том числе. Будем работать как работали на эмуляторе в отрыве от реальности.
Всё о чём мы реально знаем и с чем взаимодействуем - память.
Далее более: Программирование становится веьсма веселым занятием. Вы больше не думаете о том как открыть окно. Нет вы сразу пишете что-то на экран.
И вопрос вашего рантайма и библиотеки - где конкретно это выводится. С вашей точки зрения вы сделали 1 вызов и уже имеете экран на который вы всё прекрасно выводите.
Сделали пару вызовов - вы уже имеете звуковой буфер в который пишете музыку. Берете лок и потом отпускаете.
Берете мапите часы и оттуда читаете текущее значение времени.
Берете мапите странный девайс, но тем не менее - собственые настройки приложения. Сохраняете текущую сессию. Здесь у вас есть несколько килобайт памяти куда можно сериализовать всякое разное.
Больше не надо думать о том где мой config.json на диске. И на диске ли он вообще хранится. Быть может мои настройки - это меморимап по куску собственного файла исходного кода. Я просто об этом не в курсе. Но вполне возможно - меняем свой собственный исходный код. Который в дальнейшем при демонтировании девайса с региона памяти сохраняет изменения в файл.
Вообще когда я начал об этом думать. Меня немного осенило что в итоге почти всё что мы моделируем с помощью объектов у которых методы влияют на поведение памяти. Мы могли бы на самом деле делегировать и предоставлять как memory mapped interface.
Понятно что не всё из этого будет удобно, потому что по существу memory mapped interface так или иначе ограничен в размерах. Но как минимум что мы получим - очень простую абстракцию которая решает в том числе проблему cache friendly организации.
Условно говоря по-настоящему объект лежит в разных местах, в зависимости от задачи. И его части используют разные системы. Но мы перед собой видим кусок памяти который напрямую мапится в эти самые другие части памяти.
Иначе говоря объект представляет из себя такой девайс для нас. Который мы создаём с помощью реального или фейкового mmap.
Это особая боль. Мы либо везде пихаем GC как в лиспе. Либо везде пихаем MMM. Более того этих MMM много разных. Так например не во всех языках вы можете взаимодействовать со стеком напрямую.
И такое чувство что решения у этого нет. Но подумайте об этом следующим образом.
Что если считать что стекфрейм в операционной системе - это memory mapped interface. Ну иначе говоря стек это девайс.
Что если GC - тоже девайс. Вы пишите туда данные, получаете в ответ указатель на новосозданный объект. Который по сути является девайсом. Например вызывая библиотечный метод clone вы бы получали девайс-копию в другом месте. Условно у вас на стеке.
Итак давайте попробуем придумать lisp. Который я так люблю. Но такой, чтобы он одновременно умел работать с совершенно разными устройствами.
Первая концептуальная вещь которая нам потребуется - список регионов. Она же виртуальная память.
Иначе говоря мы можем попросить разблокировать память (define ptr (virtual-alloc 4kb))
.
И даже туда что-то записать (serialize-write-bytes ptr object-1 object-2)
.
Я предлагаю более выразительный синтаксис на будущее:
{ptr "hello" 12 34}
Запишет напрямую сериализованные сконкатенированный байтики. И вернет указатель на то место где закончило писать.
Но допустим мы хотим как в примере раньше взять записать цвет на color-ptr
, а текстуры мы хотим записать в texture-ptr
.
Эти вещи могу лежать вообще на разных страницах памяти.
Можно ли как-то придумать механизм их объединения?
Да, встречайте - список. ptr: ‘(addr offset bytes-left next-ptr)
Это наша основа для того что мы назовём виртуальным лейаутом. Иначе говоря заместо указателей.
У нас гораздо более умные указатели. В некоторых языках подобное еще могут в библиотеках называть view
.
ListView
например. Или string_view
.
В случае когда можно заместо построения этого списка - мы должны получать что-то оптимизированное. Но если не получается - то ну так.
Короче заместо нормального cons
и списков мы будем делать (vmcons chunk-a (vmcons chunk-b nil))
.
Теперь мы можем смотреть на память через эти вьюшки. Мы можем туда в том числе писать и это будет отображаться в их оригинальных местах.
Ну например пусть (sizeof chunk-a) === 5
, (sizeof chunk-b) === 5
(serialize-write-bytes vmem "hello" "world")
.
Тогда при попытке
(read-bytes chunk-a)
Мы должны получить
> "hello"
В ассемблере есть такая вещь как регистры. Регистры по сути дела это то, куда вы записываете вещи из памяти (потому что из памяти надо грузить). Чтобы потом делать с ними какие-то операции. И потом по памяти куда-то записываете.
Регистров ограниченное число и пользоваться этим весьма неудобно. Языки программирования решили пойти по какому пути: давайте регистров вообще не будет. А будут типа переменные, которые чем-то похожи на регистры, но только отличаются тем что являются абстракцией над произвольным регионом памяти.
Они бывают на стеке а бывают где-то еще. Но в любом случае переменная это вещь что занимает пространство в памяти.
Я принципиально не хочу переменные и подобные модели. Объясню почему: они жестко привязывают имя к месту где оно находится и к его значению.
В то время как реально важно не место где - а что?! Более того на одну и ту же часть памяти вполне адекватно смотреть под разными углами.
Мы можем думать что эти 3 байта это цвет r g b
.
Но мы так же можем подумать что это r g b r g b r g b
- 3x3 матрица. Которую можно умножить на другую матрицу.
А что если мы захотим смотреть только на красную компоненту? Она же тут есть.
r _ _ r _ _ r _ _
Или например на 3 строчки красного канала как на 3x3 матрицу?
r _ _ r _ _ r _ _
r _ _ r _ _ r _ _
r _ _ r _ _ r _ _
Мы могли бы организовать vmem на этот регион (понятно что в данном примере без компиляторных оптимизаций мы не получим никакого ускорения) и напрямую писать прямо туда изменения.
Так что там с регистрами и причём тут они? Ну дело в том что регистров чтобы они были юзабельны хотелось бы побольше. И так или иначе эмулировать бесконечно много регистров можно только с помощью жонглирования регистрами и записью в память. Либо! Мы реально можем выделить под регистр какой-то кусок памяти.
И так уж получилось что концепцию которую я сейчас буду презентовать в синтаксисе - я назвал регистрами. Потому что они как бы читают значение которое лежит в данный момент по памяти. Но де-факто являются как бы значением. Но и одновременно ссылкой на кусок памяти.
В общем регистр в языке который я предлагаю это тип. Тип не инстанциируется мы не создаём типы, мы их интерпретируем с памяти. И регистры в этом смысле локальны - они существуют только в контексте.
Давайте пример: определим регистр
(type (reg point2d ((int32 x) (int32 y))))
Как теперь создать и прочитать в регистр?
(reg-bytes (vmem size) (point2d register)
(+ register.x register.y))
Вы могли заметить использование точки. Ну на самом деле дело в определении регистра. Попробуем подставить его рекурсивно.
(reg-bytes (vmem 64) ((point2d register))
(reg-bytes (vmem 64) ((int32 register.x) (int32 register.y))
(+ register.x register.y)))
Ну я думаю вы поняли что происходит. Лэйаут который мы определили позволяет нам по-разному бить кусок памяти. Либо как тип целиком либо как под-регистры.
Мы теперь можем сложить эти два значения прочитанные из памяти и вернуть. То куда мы возвращаем - зависит от того регистра куда мы возвращаем.
Возможно никуда не возвращаем.
Так как регистр после чтения де-факто указывает на текущую память, можно сказать он её mmap-ит. Мы можем сделать композиционный регистр из двух других регистров.
take a look
(reg-bytes (vmem 64) ((int32 x) (int32 y))
(reg (x y) ((int32 y) (int32 x))
(- x y)))
Это какой-то бессмысленный пример. Но я что хотел этим показать - регистры имеют scope. Это локальная интерпретация памяти по некоторому адресу. Через некоторый тип.
И кроме того reg это что-то вроде
(reg-bytes ((vmcons (chunk (get-current-addr x) (get-reg-size x) nil)
(vmcons (chunk (get-current-addr y) (get-reg-size y) nil)
nil))
(+ (get-reg-size x) (get-reg-size y)))
((int32 y) (int32 x))
(- x y))
Что при финальной генерации в машинный код должно агрессивно оптимизороваться компилятором.
Всё вместе на самом деле может быть скомбинировано таким образом
(reg (|addr type|) ...)
(reg ( r ... ) ...)
Где |addr type.field|
это на самом деле
(reg-bytes (addr (sizeof type)) ((type reg))
reg.field)
Ну иначе говоря это селектор либо дереференс оператор короче в С++ / С (*ptr).field
Что в итоге должно быть оптимизированно конечно. Но суть в том что мы можем простым образом достать регистр по адресу.
Обратный ему оператор - (get-register-vmem reg)
, он же <reg>
- делает обратную задачу.
Регистр который хранит ссылку на vmem
- тож типизирован, так и будет: vmem
.
Например тип регистра addr
(если это не прямое значение адреса 0x122…) - vmem
.
vmem
- особый тип, он несмотря на то что список - он частично числовой, на нём работает арифметика указателей.
Регистры могут иметь методы int32 x
. (x/inc)
Это по сути синтаксический сахар для вызова замыкания (λ [x x] () (int32/inc x))
.
Сам int32/inc
скорее всего выглядит вот так
(register-write x (+ 1 x))
; или сокращенная запись
[x (+ 1 x)]
Замыкание executable vmem
куда приконкатенировали рядом таблицу closure (vmem)
Откуда она собственно читает значения.
И вызов замыкания эт чёт типа на самом деле
(reg (return-addr closure-size) (((byte closure-size) closure)
((byte funct-size) body))
(reg-bytes (<closure> closure-size) (((typeof x) x'))
[x' x])
{body (ε
(reg-bytes (closure-offset size) (((typeof x) x) ...)
(int32/inc x)))})
Условно говоря регистр и всё необходимое для дальнейшей работы - переезжает туда.
executable vmem
это то что тут зовут code pointer
их dereference происходит по-старинке (executable-vmem)
Если вы хотите
иначе говоря в теории вы можете сгенерировать код на ассемблере засунуть туда функцию и просто сделать (asm)
Можно даже туда напрямую попробовать записать байтами условно.
{page 0x...}
И потом сделать (page)
.
Ну короче о чём это я: есть методы у типов это и есть замыкания / функции-лямбды у типов. Некоторые методы особые. Например /read /write
Как вы могли догадаться они перенаправляют то что происходит с регистром когда дёргают (register-read reg)
(register-write reg value/reg)
.
Методы позволяют создавать поведение у регистров / памяти.
И позволяют инициализировать девайсы (device/init)
(device/destroy)
.
let является обёрткой над reg и дёргает специальные собственные магические методы у регистра (reg/let-enter)
(reg/let-init syntax)
(reg/let-unwind syntax)
, (reg/let-exit)
Позволяя делать что-то такое (если у нас есть специальный девайс-регистр gc
)
(let gc ((int32 x 0))
(λ [x x] () (x/inc)))
Мы также напрямую может управлять аллокацией на стеке
(let sp ((float32 x 12)
(float32 y 13)
(float32 h1 (/ (+ x y) 2))
(float32 h2 (/ (- x y) 2)))
(let gc ((point2d point (h1 h2)))
point))
Либо например отлично работает с арена-алокатором. Накидали значений, всё почистили на выходе.
let может не содержать аллокатора так как это макрос то просто используется то что находится в макро-параметре let-allocator на этапе компиляции и используется соответствующий регистр
(parametrize (let-allocator gc)
(my-program))
Таким образом можно писать аллокаторно независимый код.
Вы пишите прототип? Используйте gc
везде. Пишете финальную версию игры? Попробуйте arena
, arena-once
, sp
и что-то еще.
Можно что-то странное делать типа html генерации наверное. Но это легонца извращение над идеей регистров.
(let html-gen ((html
(doctype 'html')
(head (title 'sample'))
(body)
))
(stdout/write html.text)
)
Это не очень адекватно но суть понятная: с помощью let и макрохуков требуется спавнить регистры.
Основной ближе к racket
. Но как и с аллокаторами - сам макрос является девайсом который macro/expand
.
Поэтому можно поменять его на кастомный, а можно вообще смешивать.
(with-macro-device common-lisp-style
your code)
Современные приложения требуют много бойлерплейта. В функции main мне приходится писать кучу бреда.
Типа если парсим такие параметры. Вызови этот хендлер. Если параметры такие, вызови другой.
Мне нужно каждый раз вспоминать как в этом языке проще всего хранить настройки моего приложения.
Пора положить этому конец - да здравствует shadowspace. Это штука где по-дефолту замаплены разные вещи для рефлексии в программе.
Естественно можно это убить и решить проблему альтернативно. Но здесь будет открыт по умолчанию стандартный интерфейс для настроек.
config device
в конфиг вы можете написать свичи для вашей программы которые будут заполняться для надо проставить соответствующий байт на регистре
в конфиг также можно в рантайме добавить новые пары ключей и тем самым изменить собственную конфигурацию
конфиг будет показывать байт может ли он сохранить или нет и байт автосейва
никакого роутера по стандарту не надо, но если хочется можно записать свой хендлер в /router
эта функция может вернуть 0 или 1 стандартная точка входа выглядит примерно так, вам её не нужно конечно писать но её можно заменить тоже
[config.disable-standard-router (config/router)] (if config.disable-standard-router (exit runtime.error) ;; if any (config/standard-router))
ну в общем не сильно это важно но суть в том что у вас должен быть простой интерактивный способ после компиляции в том числе ручками открыть и проредактировать конфигурацию
не писать свой инсталятор и много чего еще что от нас требуют системные языки программирования
заместо вычурных вещей должна поставляться простая документация которая автоматом красиво рендериться
в дебагере это будет выглядеть словно вы просто редактируете какие-то окошки прямо для компилятора будут стандартные прессеты девайсов доступных по умолчанию и свои кастомные можно задать когда дебажишь компилятор
Мы разобрались более менее что происходит когда мы делаем что-то такое
(reg-bytes (addr 8) ((int32 x) (int32 y))
...)
но что происходит когда мы пишем что-то такое?
(reg-bytes (addr 7) ((int32 x) (int32 y))
...)
здесь какая-то ошибка, мы указали слайс по vmem addr в размере 7, но хотим лейаут на 8 байт
даже в случаях о которых позже - не понятно что это значит в общем size всегда должен соответствовать лейауту
а что это такое?
(reg-bytes (addr 16) ((int32 x) (int32 y))
...)
тоже ошибка? не совсем мы будем думать что это векторизованный регистр представьте просто что лейаут повторяется
вообще регистры нативно векторизованы и имеют типа размер
поэтому регистр x
не совсем int32
это int32[2]
в данном случае как бы
векторные операции по дефолту работают как обычные
но что происходит когда у нас (int32[47] r)
и (int32[32] g)
давайте вспомним что такое регистр - регистр это тип, способ смотреть на vmem vmem изначально сырые байты, регистр дефайнит лейаут vmem дефайнит регионы
vmem - union тип от nil девайса, mem девайса (addr size)
и чего-то вроде (car cdr size)
где car - ссылка на vmem, cdr - ссылка на vmem
иначе говоря это такое то ли дерево то ли список + размер того куска регион которого мы рассматриваем
nil это специальный иммутабельный vmem бесконечной длины по умолчанию в него можно что угодно писать - будешь ничего не делать из него можно читать нули
если вам нужен круговой буфер - вы можете его сконструировать примерно так
[(vmem/last).cdr vmem]
[vmem.size 2048]
тогда при попытке читать вы получите циклический буфер такого размера
(define sp (char 8) pass "password") ;; define on stack register pass "password"
(define shadowstack vmem repl (vmcons <pass> <pass>)) ;; create cyclic buffer 'passwordpasswordpassword' currently of size 16
;; we create this on shadowstack to not pollute our normal one
(define sp (char 11) text "hello world") ;; defined 11 chars on stack
(XOR text |repl (typeof pass)|) - ксорит байты двух векторизованных регистров
;; альтернативно
(reg repl ((char src))
(reg <text> ((char dst))
[src dst]))
по сути ксорит байтики
"hello world" xor
"passwordpas"
Некоторые вещи на самом деле являются специальными формами или макросами в данном случае.
Например над (serialize-write-bytes vmem ...)
- является макросом.
Он берет конкатенирует vmems/registers и делает тупую операцию
[vmem vmem]
Так как это байтовые регистры они просто повторяются и получается побайтовое копирование.
Поэтому например такая операция
(reg-bytes (vmem-rgb 600) ((byte r) (byte g) (byte b))
(reg-bytes (vmem-chan 600) ( ((byte 200) cr) ((byte 200) cg) ((byte 200 cb)) )
[cr r]
[cg g]
[cb b]))
перезапишет из буфера vmem-rgb r g b r g b r g b r g b в буфер vmem-chan r r r r r r r …. g g g g g g … b b b b b b b …
а в случае если один из них будет слишком маленький для другого - запись в nil ничего не даст компилятор может оптимизировать и не писать в nil девайс но если есть сайд-эфекты оптимизировать не получится
помимо vmem которому конкатенируют nil девайс
есть чуть более понятный просто (mem addr size)
который по сути представляет из себя примитив - регион памяти
например vmem который вы получаете с обычного (virtual-alloc size)
- это (vmcons (mem allocted-at-addr size) nil)
векторизация всегда по умолчанию для неё не надо макроса
все операции над регистрами работают соответствующее
;; на shadowstack (стек рефлексии) закинуть объект типа vmem
;; регистр raw-bytes-20 имеет тип vmem
(define shadowstack vmem raw-bytes-20 (arena/alloc (byte 20) 0))
;; аналогично
(define shadowstack vmem raw-bytes-ab (arena/alloc (byte 2) "ab"))
(define shadowstack vmem raw-bytes-cd (arena/alloc (byte 2) "cd"))
;; примитив vmcons работает над регистрами типа vmem и возвращает по сути новый vmem
(define shadowstack vmem abcd (vmcons raw-bytes-ab (vmcons raw-bytes-cd nil)))
;; сделали циклический буфер записав заместо nil сами себя в конец
[(abcd/last) abcd]
;; кстати говоря можно например так сделать
(reg (raw-bytes-ab raw-bytes-cd) ((vmem x))
[x nil])
что произошла за дичь? мы взяли внутри reg из двух регистров типа vmem сделали vmem который указывает на vmem-ы (указатель на указатель) и проинтерпретировали его как (vmem x) - регистр
когда мы пишем [x nil]
мы получаем векторизованную операцию которая проставляет векторизованно в регистр x типа vmem другой регистр типа vmem - nil
таким образом мы записали в [raw-bytes-ab nil]
и [raw-bytes-cd nil]
это кстати говоря поменяет в том числе abcd, потому что vmem - всего лишь пара: указатель на адрес текущего vmem
и его размера и следующего за ним
так как значение vmem по адресу поменялось на shadowstack - поменялся и наш список
теперь это по значению abcd === (vmcons nil (vmcons nil abcd))
но теперь допустим мы этого не делали abcd до сих пор циклический буфер abcdabcdabcdabcd
мы можем теперь сделать так
(reg abcd ((byte src))
(reg raw-bytes-20 ((byte dst))
[src dst]))
мы записали регион памяти raw-bytes-20
на арена алокаторе 5 раз
"abcd abcd abcd abcd abcd"
макрос это функция принимающая syntax vmem на вход и выплёвывающая executable vmem
(exec vmem) - своего рода тоже векторное
исполнение инструкций
понятно что если происходит goto инструкция - то уже мы регистр поменяли на vmem и уехали
но суть понятна
например можно исполнить только часть кода реинтерпретировав
или сделать циклический буфер операций
но суть понятна
by the way how do you call functions in lisp machine device
(λ () ...
) // function is itself just a section of exectutable statements
stored inside the f register
the low level call primitive is actually almost like in assembly
you have function loading code to register
[load-printf
(λ ()
[lisp-machine.return-address (+ 2 ip)]
[f printf-body])]
the call itself for example
is just
[lisp-machine.exec (vmcons load-printf f)]
you just prepare code and then set vmem to the register and run it as if it was the register of a device
f register points to executable memory load-printf points to executable-memory which modifies the f register
this way vmcons dynamically changes the code of itself when executed the f register will swap the body because of load-printf
and then it itself will be executed
я знаю короче как организовать
когда мы делаем (function …) мы под капотом на самом деле юзаем регистр function который например в lisp-machine
(мой нативный девайс интерпретатора / компилятора)
представляет из себя ну вот то что мы видели (let ib ((emem load-function (λ () [lisp-machine.return-addr …]
…)
(emem function (vmcons load-function lisp-machine.f)))
(function 1 2 3 …))
то есть по сути там создаётся load-function регистр чуть ранее, потом запускается ехекьютбл регистр
при этом можно например проставить бит (function.machine macro)
by default function.machine === lisp-machine
и тогда исполнение перейдёт в руки макро-девайса
(defmacro …) - оповещение макро-девайса
(defun …) - диспатч функции на лисп машину о том что надо зарегистрировать новую функцию
можно написать лямбду и проставить самостоятельно на каком девайсе она исполняется
для этого девайс должен соответствовать спецификации executor-device
лисп машина просто имеет регистр lisp-machine/macroexpand
куда засунут по сути macro/macroexpand можно например вырубить макрос тем что [lisp-machine/macroexpand (λ (vmem) vmem))]
ну и соответственно в будущем легко заменить компилятор он делает базовые тупорылые формы
x86_64-native-lisp
статический компилятор например в бинарь без рантайма
который принимает лямбды
не просто дебагер почти тривиально реализуется через короче macroexpand который передаёт управление внешней программе внутри а потом кладёт обратно как было до
короче вы сами можете сделать свой дебагер ну да а в силу того что лисп машина тупо девайс - кусок памяти мы как раз через другой девайс дебагер - хукаемся к исполняемому файлу
иначе говоря это похоже на то как если бы другая программа открыла консульство и прицепилась бы напрямую к интерпретатору
it will partially hook via some other operating system device in the end we need memory map to create a debugger machine
but yeah it is simple as setting
[lisp-machine/macroexpand debugger/macroexpand]
since vmem is kind of a tree or a list (depends on how you constructed) there is a call (vmem-flatout …) which basically flats regions which should be flat and recreates the vmem
also vmem-treeify which remakes crazy vmem into normal binary tree with not so big height
this is good for you know when you have very discontigious vmem
there is a specific byte for each register and it says basically
[register.vmem-optimize true]
this is basically for scenarioous when you have a register but then you directly manipulate with underlying vmem with vmconses and crap
optimize it manually for what you wanted (maybe even transfer the register physical location)
and to do that you need to set [register.vmem-optimize false] after that compiler will not touch the vmem itself and you can modify however you like
also vmem.mutex can be aquired
so that other threads cannot do some crap this is for direct changes with vmem object / device this is basically for compiler optimizer jit thread
to stop
so by default you don’t use your own vmems you just work with data transforms and application code
but if you need that crap you create a vmems with vmcons
and by default they have optimize flag set to false this lets you build whatever you want and compiler obliges to follow (unless it directly bans vmem type)
however maybe you have created a nice circular buffer
out of giant crap and you can just hit [vmem.optimize true]
the compiler will then dynamically optimize your memory view for example for different cache strategies
maybe even something like lru (you want certain parts to be on top) and some points on bottom instead of log n even data
the compiler can also analyze and try inline your vmem accesses, basically removing the vmem from even tracking in some functions and do some other agressive optimizations
so when you vmem.mutex you basically say to the optimizer thread that now delist it please and follow the way I set after the lock is released back imagine it sees that you always go for red channel
and it remaps the underlying vmem from rgb rgb rgb to r r r r r r r r … gb gb gb gb gb … and recreates your vmem via conses
and pushes this huge red chanel on optimizer buffer and also it notes that no thread is currently using this memory region and that in a sourcecode nobody directly writes it as glanced
so it just marks it maybe mprotect and if somebody will try to do access or write there as raw bytes it will restore the page as if it is logically was put by programmer the page itself is actually a huge vmem device in a language so basically the page can change itself like
and since vmem is transparent for application code unles you do some manual vmcons and vmcar vmcdr vmem.size operations nobody knows that physically it is actually in different place and original marked mprotect
вообще как можно заметить reg является реинтерпретацией vmem даже случаи вроде
(reg vmem-reg ((byte r) (byte g) (byte b))
...)
ну то есть когда мы делаем реинтрпретацию лейаута с байтов ((byte) size)
каждый из этих регистров новых типа там r g b - всё еще фундаментально работает над той же самой vmem которую мы дали
ну условно r.vmem === g.vmem === b.vmem === vmem
разница только в том что при обращении к r мы будем обращаться к
r[i]
= (vmem-addr-resolve i * sizeof(layout))
g[i]
= (vmem-addr-resolve i * sizeof(layout) + sizeof(r))
b[i]
= (vmem-addr-resolve i * sizeof(layout) + sizeof(r) + sizeof(g))
в случае вложенных по вложенным это понятно тоже что-то вроде
(reg vmem ((int32 r) (int32 b) (int32 g))
(reg r ((int16 low) (int16 hi))
...))
low[i]
= (vmem-addr-resolve i * sizeof(layout))
hi[i]
= (vmem-addr-resolve i * sizeof(layout) + sizeof(low))
поэтому вложенные регистры is almost always free поэтому что это просто оффсеты в типе а мы знаем размер типа
Я долго думал каким образом я могу хранить временные значения. В ассемблере когда мы храним временное значение мы перекидываем их напрямую в return address.
Иначе говоря мы имеем регистр return-address куда мы пишем значение в итоге (после вычислений). В нашем случае return-address будет vmem регистром лисп машины куда мы закинем значение после выполнения функции.
В общем что я хочу сказать. Можно сделать так.
(return-address/on-read (λ () ... {return-address computation-result}))
при этом сделать return-address - циклическим буфером теперь следите за руками
помните у нас был синтаксис интересный?
(let arena (((byte 300) r)
((byte 300) g)
((byte 300) b))
;; three channels
(reg (r g b) ((color256 c))
{outbuf c}))
тут есть конструкция по имени
(reg (r g b) ...
)
которая в языке по сути выражает семантику того что мы рассматриваем discontinious memory regions as one united with new type color256 который мы удачно запишем в буфер как типа r g b кусочки памяти
есть одна фундаментальная проблема что такое c.vmem?! откуда оно вообще у нас есть
а вот в том и дело что об этом стоит думать скорее вот так
(let arena (((byte 300) r)
((byte 300) g)
((byte 300) b))
;; three channels
(reg λ(r g b) ((color256 c))
{outbuf c}))
что такое λ(r g b)
давайте разбираться
(let arena (((byte 300) r)
((byte 300) g)
((byte 300) b))
;; three channels
(reg return-address.vmem ((color256 c)) ;; interpret return address register as color256 c register
;; 1. get address where return-address contiguous space is mapped
;; 2. create vmem of appropriate size right there
;; 3. rewrite vmem register of return address register
[return-address.vmem (vmem <return-address.vmem> (sizeof color256)) ]
;; loop return address infinitely
[return-address.vmem.cdr return-address.vmem]
;; now we basically have really a place where c register resides
;; the last bit is to write there vectorically
;; since function calls by default are vectorized
;; we just make small emem on fly and write it to lambda (anonymous function register)
[lisp-machine.lambda
(λ ((byte r) (byte g) (byte b))
{return-address.vmem r g b})]
;; whenever we use c register now, we actually use this
;; c === (lisp-machine.lambda r g b)
;; or in other terms
[c/read (λ () (lisp-machine.lambda r g b))]
;; how it is invoked?
;; we use hidden vector cell param, which is used by invoked (lisp-machine.lambda r g b) expression
;; so now when you try to read it, it will iterate over r g b vectorically
;; write 3 bytes to return-address and then we just read the memory from vmem mapped to c
;; which is return-address.vmem
;; so you basically generate things on fly and map directly to return-address space
;; from there you read
;; you may ask what if many such emem reinterpretations are given
;; well then you use return-address space as nested layout
;;
;; consider such, perfectly valid code
;;
;; (reg λ(r g b) ((color256 c1))
;; (reg λ(b g r) ((color256 c2))
;; [out (+ c2 c1)]))
;;
;; then return-address after the second reg would be remapped
;; to sizeof(color256) + sizeof(color256) as a kind of tuple ((color256 c1) (color256 c2))
;; each consequent lambda will write only its part
;;
;; return-address / shadowstack space is predefined on compilation time
;; very much like stack, but it is remapped basically on every call
;; more like tail recursion optimization, where you just reuse the same frame over and over
;; and put there only the space we really need
;; since it is iteration - we can realistically do it
;;
;; if you never ever need allocations you have not asked - do not use emem
;; as virtual memory device because it will do this
;; do everything explicietly
;;
;; but for many usecases
;; emem device is perfectly valid
;; imagine for example some cryptography xor shift thing
;; it will generate an infinite sequence
;;
;; sometimes when you write you can set return-address.vmem right there
;; and generate even ligher code for my simple lisp-machine
;;
;; sometimes you can directly translate it to something appropriate
;;
{outbuf c} ;; will trigger c/read lambda
;; basically replacing the inital thing with this
;; {outbuf (c/read)}
;; but it is harder as I said in case of c1 c2
;; there you would create a buff
;; and map result to outbuf.vmem only after
;; {outbuf (+ c1 c2)}
;; basically
;; (reg return-address.vmem ((color256 x) (color256 y))
;; {outbuf (+ x y)})
;; unbind // remove this crap
...))
this is basically what happens with those emems virtual memory devices expressions and types tend to be not so giant
so unless you do some heavy macro expansions and expand to terrible terrible size of return-address.vmem it will be just fine
also as you have noticed this lisp as common lisp and racket will provide multiple return values this mechanism was used to create value register over emem lambdas
реинтерпретация крайне дешевая в итоге получилась по идее
потому что мы всегда знаем в точности где сейчас находится vmem и куда мы пишем
регистры в итоге получились чисто синтаксической вещью
всё остальное либо работает через return-address.vmem
либо мы знаем где оно в данный момент находится, какой конкретно vmem мы используем для интерпретации
даже в замыканиях это верно
по умолчанию лямбды использую stack
но можно на самом деле упороться и использовать тот же фрейм
возможно это будет делаться специальной формой
(ε device ((func … args) values)
.. potentially do something with values)
короче по умолчанию (func … args)
как и let без указания - использует default allocаtor = stack
для переданных аргументов и для return-values
но потенциально можно будет использовать другой девайс например gc для каких-то ленивых вычислений на куче с бесконечной рекурсией
и можно заместо стека положить reuse-frame девайс
который будет подобно tail recursion elimination
напрямую переиспользовать фрейм
это зависит от программиста что он делает и почему
ну это как раз реальный настоящий zero-cost abstraction
ты по сути можешь провалиться напрямую в чуть ли не ассемблер, но при этом на самом деле одновременно повысить чуть ли не до хаскеля
lazy-gc например
ну типа я смогу писать будто я на лиспе обычном
с gc, а если stack-а не будет хватать и надо будет сделать бесконечную рекурсию
ты просто оборачиваешь какой-нибудь dfs
с (parametrize default-function-allocator gc
(dfs …) )
и он почесал там короче считать
просто в обычных языках тебе потребуется сменить компилятор
тут как бы тоже не все будут поддерживать gc как девайс (в том и прелесть, embeded разработка будет в восторге: выключить всё ненужное, написать простой понятный компилятор/интерпретатор, который позволяет почти вообще не использовать лишних аллокаций даже при вызове функций, даже на стэке эдакий NASA level of control)
но многие реализации (во всяком случае дефолтная - будет)
аналогично gc вряд ли нужен в операционной системе
но очень нужен для научной работы
самое важное не всегда нужны векторные операции
как бы часто мы по старинке любим
pan - поддерживает это, если лейаут точно мапится в размеры vmem - никаких векторных операций
но стоит чуть расширить и вы тот же самый код с тем же самым лейаутом уже используете как вектора
это же не прелесть?
до меня допёрло что такое
(λ ( (int32 x) (int32 y) (int32 z) )
...)
ничто вам эта ерунда не напоминает (аргументы функции) это лейаут чёрт возьми
на самом деле единственный аргумет который получает лямбда - vmem это позволяет нам делать функции с бесконечным числом аргументов и прочее дерьмо
короче это то же самое что
(λ vmem
(reg vmem ( (int32 x) (int32 y) (int32 z) )
...))
в конечном счёте функция принимает vmem откуда читает и имеет vmem куда в итоге пишет
что такое композиция функций? это когда return = input
f.return = g.input vmem
(f (g vmem))
проблема в том что композиция функций должна где-то хранить эти intermediate results либо не хранить и колапсировать их
на помощь приходит концепция девайса именно он аллоцирует vmem куда функция будет писать параметры
допустим у нас три вызова функций (a (b (c vmem)))
с на вход получает vmem - ей ничего не нужно b на вход должа получить return-values с
для этого девайс аллоцирует return-values vmem и привязывает их к c после чего b может спокойно привязать в свой регистр return-values регистр лямбды
получается такая цепочка a device b device c vmem
последнее значение можно и не аллоцировать на самом деле если мы привяжемся и будем куда-то писать напрямую иначе нам надо опять будет использовать device
в общем всё понятно, можно объявить что let как в scheme это просто тоже композиция анонимных функций поверх девайса-аллокатора просто синтаксический сахар для emem лямбд
vmem как можно было догадаться уже - на самом деле представляет сам по себе векторизованную функцию ну действительно посмотрите просто на синтаксис записи в регистр какой-нибудь
[vmem vectorized-byte]
ничего не напоминает? ну мы буквально на вход вектор-функции vmem подали другой vmem и оно пошло туда записало причём столько сколько нужно и можно
аналогично с регистрами: они позволяют писать векторизовано-типизированно
но мы с вами не поговорили на тему того как девайс реально помогает нам с аллокацией при вычислениях допустим у нас есть такая лямбда
(λ vmem
(f (g vmem)))
внешний vmem прикативший в функцию - эт понятно что такое это ссылка на какой-то лэйаут который мы интерпретировать будем
никаких промежуточных значений нам тут не нужно но куда вопрос пишет функция g?
функция g пишет в vmem своего дефолтного return-address который в случае того что я называю free function, RetAddr === nil
иначе говоря g vmem векторизованно пишет байты в пустоту
если вы пишите свободную функцию вам нужен внешний регистр
(λ vmem
[some-external-reg (g vmem)]
;; okay (g x) return-address binded to external-reg
;; directly, check return type of g only
(f some-external-reg)
;; okay we execute f as last expression and bind
;; our own return-address to f return-address
)
это еще можно записать короче благо vmem и регистры являсь функциями возвращают сами себя.
(λ vmem
(f [some-external-register (g vmem)]))
не всегда запись в nil бессмысленна на вход f вы будете просто получать нули
наверное так: девайс управляет где разместить фрейм но функция говорит я хочу фрейм, дай мне фрейм такого размера я там буду хранить данные функция прерывается возвращается получает свою ссылку на свой фрейм и продолжает вести расчёты
в этом смысле + использует что он там привык использовать скорее всего стек для выполнения своей работы либо вообще ничего не использует но vmem которая отправляется плюсу, которую он копирует - аллоцируется на фрейме текущей функции
иначе говоря положняк такой: функция принимает на вход vmem (ссылку на данные) и метод аллокации собственного фрейма исполнения
vmem у функции получается всегда ссылка и мы можем при желании перекинуть данные полученные в собственный фрейм чтобы жить независимо
кроме того промежуточные вычисления будут положены на фрейм
device замыкания аллоцирует специальный кусок по имени frame
(f (g x) (z y))
на самом деле представляет из себя что-то вот такое
(f [frame-register-g (g x)] [frame-register-z (z y)])
функция создаёт в компайл тайме все эти регистры основываясь на return-type вызываемых функций и запрашивает у девайса соотстветствующий vmem соответствующего размера
могут возникнуть у читателя вопросы у нас векторизованные функции мы чо запрашиваем килобайты временного хранилища на стеке/куче/гарбач колекторе?
ааа не совсем вы можете выдать подобный кошмар например если вам нужно хранить все промежуточные результаты
но по умолчанию фрейм - циклический буфер размера ваших лейаутов x количество simd регистров например на вашем компе потому что в остальных случаях вся векторизация - простой циклик где вы циклически переиспользуете фрейм так как векторные операции независимы - вам не стоит беспокоиться
но если по какой-то неведомой причине вам всё же нужно создать 200 объектов на сборщике мусора в стандартной либе это проставляется в функции в специальном регистре vectorized-loop-size нужное значение
для большинства ситуаций удобно выдавать фрейм как циклический буфер и переиспользовать в цикле нежели делать
[function.frame-size (layout-size emem.compiled-frame-layout)] [function.vectorized-loop-size (vector-cycles emem.compiled-frame-layout)] [function.device gc] (function …)
нет никакой проблемы неструктурированности макроса это искусственная проблема
макрос структурирован до тех пор пока нужно - потом нет поэтому s-expr (с++ …) может полностью игнорировать всю обычную логику лиспа и самостоятельно переопределить как он парсит и работает с макросом выплюнув заместо переписанного синтаксического дерева - полноценный бинарник emem
обычный синтаксический трансформер просто перепишет выражение и подменит собой на тот же таргет-компиляции: компилятор лиспа для дальнейшей автоматической компиляции
главное чтобы финальный кусок кода можно было исполнить для этого нужно создать emem с нужными хедерами по сути
(emem type: machine-code device: x86-64-cpu …rawbytes )
emem имеет execution-target всегда это например означает что lisp-machine - дефолтный интерпретатор будет смотреть: окей this is machine code - switch execution to x86-64-cpu device okay this is jvm code, is jvm device available? switch execution there okay this is glsl code, is gpu device available? switch execution there
как это работает спросишь ты меня и почему это не раздует код lisp-machine? да очень просто executor-device получит на вход emem, input-vmem и запишет результат в output-vmem
всё очень просто
если девайсу прилетел неверный emem, допустим написано target: jvm а мы это отправили x86-cpu - он может кинуть сигнал ошибку типа чо за херня
как виртуальная память vmem сцепляет вместе кусками регионы памяти, позволяет строить циклические буферы и чего только еще не позволяет
так и emem позволяет сцеплять исполнения
(emem
(vmem bound-input: ???)
(vmem frame-bound: ???)
(vmem output-bound: ???)
(executor target: x86-64-cpu)
(allocator frame-provider: gc)
(int64 steps-made: 0)
(next-emem: emem))
)
(emcons emem-1 emem-2)
перенаправляет emem-1.output-bound === emem-2.input-bound
ставит по умолчанию
(executor target: lisp-machine)
(allocator frame-provider: lisp-machine.default)
зачем нужен frame-provider? чтобы выполнять цепочки преобразований и хранить результаты на фрейме
потому что emem-1.output-bound === emem-2.input-bound должен существовать
по сути заместо макроса традиционного предлагается во-первых использовать хуки на scoped функции иначе говоря когда лисп машина пытается скомпилировать выражение в теле которого лежит макро-функция
лисп машина останавливается и передаёт управление другому девайсу который может ей манипулировать в частности стандартный макрос может заменить синтаксические всякие конструкции
а макрос оптимизаций увидев что компилируется выражение с quick-sort
функцией
получит управление текущим компиляторным контекстом и например сможет сделать assembly inline
в зависимости от регистров и метаданных компилятора, лисп машины либо компилятор может дать управление вам, вы напишете идеальный асемблерный код и вернете управление обратно
всё потому что lisp-machine сигналит при компиляции сигналами типа breakpoint found global quick-sort register called (таким образом кстати достигается смесь гигиенического макроса и не очень)
как-то так по идее
для каких-то простых случаев компиляция соглашений о вызовых выглядит так
(type (reg cpu-calling-convention-A explain how memory is layed out…))
(type (reg cpu-calling-convention-B explain how memory is layed out…))
и вы пишете простую функцию A->B с входным типом (λ (cpu-calling-convention-A in) (reg output (cpu-calling-convention-B out) … ;; do transform out))
которая переставляет данные в сложных случаях - полиморфная функция
которая напрямую получает в итоге лейаут и автоматически транслируют
например это может быть функция укладывающая байты объекта определенным образом типа там например byte-align vs strict-packing
он берет лейаут типов, видит что они не очень хорошо уложены для этого например может существовать специальный тип прчерк _ который означает 1 байт padding
условно у тебя лейаут
((int32 x) (char x) _ _ _ )
это 4 + 1 + 3 = 8 байт лейаут, выровненный по int32 в сложных случаях padding в лейауте менее красиво отображается ((int32 x) (char x) ((byte) 3)) что это значит? byte - байт (byte) - анонимный лейаут без имени (по умолчанию для целей padding) ((byte) 3) - 3 раза повторённый лейаут
по сути синоним ((byte) (byte) (byte))
если написать (((byte) 3) register) то появится регистр с таким типом ((byte) (byte) (byte)) в такой регистр можно записывать сразу несколько значений например но сам по себе он конечно достаточно бесполезен
[register ‘a’ ‘b’ ‘c’]
но [register ‘a’ ‘b’ ‘c’ ‘d’] - уже нельзя trying to assign ((byte) (byte) (byte) (byte)) to (((byte) (byte) (byte)) register)
по сути финальный штрих - позволить девайсам получить сообщения о том что интересующие их регистры и vmem / emem были созданы
всё остальное особо трекать на самом деле не надо, потому что мы заместо индивидуальных байтов там и gc будем сразу управлять памятью на уровне скорее фреймов функций - это гораздо важнее и гораздо в итоге быстрее
так как emem содержит в своём заголовке не только ссылку на исполняемый код но и на девайс по сути евент-хендлер irq например - это регистр где лежит на какой девайс надо переключиться в случае если случилось что-то
по сути система языка Pan видит мир разными глазами - в зависимости от того кто сейчас исполняется реально то что по умолчанию доступно emem - её фрейм это по сути и есть виртуальное адресное пространство emem с которым она может рабоать
emem также видит frame-provider, в случае если ей захочется добавить фрейм а так же остальные девайсы которые к ней подключены через vmem на фрейме
таким образом когда нас прерывает - мы просто видим мир с точки зрения девайса на который переключились и его текущий фрейм и историю релевантных вызовов (vmem)
поэтому например концептуально что такое division by zero? это
basically
;; this is conceptual code
;; the real one would be compiled to simd or something appropriate in case of vectorized access
;; it will recast raw vmem to chunks and use simd inside
;; this is the power of compiler optimizer hooks
(define (/ (int32 x) (int32 y))
(if (= y 0)
(math.division-by-zero) ; the execution is handed to division-by-zero emem
; which is by default a user that further lends control to debugger
; debugger then stops the threads and much much more
((λ x86-64-cpu nil ()
(x86-64-cpu/mov x86-64-cpu.eax x) ;; this is a free function it directly manipulates registers
(x86-64-cpu/mov x86-64-cpu.ebx y) ;; we can also set emem directly inside processor interrupt vector table
(x86-64-cpu/div x y))))) ;; or inside linux handlers, whatever os we have
;; the cpu interrupt vector table