Documentation updated + I wrote sobiralka script

This commit is contained in:
Андреев Григорий 2025-10-16 03:31:10 +03:00
parent e09238a8e7
commit e1f2ff1824
4 changed files with 392 additions and 11 deletions

View File

@ -1,10 +1,13 @@
#import "../head.typ": *
#set page(footer: context {align(center, counter(page).display("1"))})
#set page(margin: (top: 1.5cm, left: 2cm, right: 2cm), footer: context {align(center, counter(page).display("1"))})
#set text(size: 1.2em)
#set par(first-line-indent: 16pt)
#align(center, heading(depth: 1)[Архитектура и стиль кода в prototype1 (Дарье)])
#align(center, heading(depth: 1)[Архитектура и стиль
кода в Дарье (prototype1)])
#heading(depth: 2)[Структура репозитория]
Для компиляции используется только сборочная система make.
Генерировать makefile с помощью shell-скрипта разрешено, просто в данный момент это не
@ -72,18 +75,116 @@ haskell и rust. Самые высшие уровни Дарьи это те, ч
Код с более высокого уровня может использовать код с более низкого уровня.
Сгенерированный код с какого-то уровня может использовать исходный код
с этого же уровня, но исходный код не может использовать генерированный
код своего уровня. Тем не менее код,
код своего уровня. Тем не менее код из `src/lX`,
генерирующий другой код, может быть осведомлён о существовании и содердании
более высоких слоёв, но только если он находится в специальной папке
`gen/l1vi`
`src/lX/anne`. Цель (функция `main`), что генерирует код `gen/lX` находится в
c-файле `src/lX/anne/codegen.c`. Вместе с ним в `src/lX/anne` помещены
h-файлы, которые содержат 'под-процедуры' от `src/lX/anne/codegen.c`,
каждый файл использует доступные в `src/lX` методы для нужд своего более высокого
уровня. В `src/lX/codegen` содержатся исключительно методы для генерации кода,
которые используются в `src/lX/anne`, но сам `src/lX/anne` не
осведомлён о высших уровнях. Больше нигде код, нужный непосредственно для генерации
кода, не помещается.
Из этого правила есть ОДНО исключение:
Файл `src/l1/core/VecU8_as_str.h` при компиляции цели `src/l1/anne/codegen.h`
использует, как ему и положено, только исходный код с первого уровня, но
при компиляции любых другиц целей он инклюдит `gen/l1/VecAndSpan_U8.h`.
Казалось бы, это нарушение, ведь именно `src/l1` отвечает за генерацию `gen/l1`,
но это самый первый уровень, и если `gen/l1/VecAndSpan_U8.h` ещё не существует - значит
наше цель это и есть `src/l1/anne/codegen.c`. В этом случае вместо
`gen/l1/VecAndSpan_U8.h` инклюдится `src/l1/core/chicken_VecU8.h`.
Файл `src/l1/core/chicken_VecU8.h` содержит копию некоторых самых полезных методов из
`gen/l1/VecAndSpan_U8.h`, что позволяет писать и `l1` и все другие уровни с
одним и тем же определение строки. Это грязный трюк, но он сэкономил мне
сотни нервных клеток.
_Не спрашивай как я написал `src/l1/core/chicken_VecU8.h` без
`gen/l1/VecAndSpan_U8.h`_...
Разберём что делает каждый уровень, что есть в данный момент в Дарье:
- `l_wl_protocols` -- из-за странного способа дистрибуции wayland-lib мы должны сами
генерировать код для неё.
- `l1` -- самый первый уровень. Тут определены все утилиты, которые не
потребовали кодгена. Так же исходный код `l` определяет типы и функции,
полезные при любой генерации кода. В утилитах `l1` так же есть
обёртки над базовыми обёртками над системными вызовами линукса.
Первый уроведь определяет самые главные шаблоны: `Vec<T>`, `Option<T>`,
`Span<T>`, `MutSpan<T>`, `Result<O>Or<E>`,
- `l1` -- самый первый уровень.
- - Тут определены все утилиты, которые не
потребовали кодгена.
- - Исходный код `l` определяет типы и функции,
полезные при любой генерации кода.
- - В утилитах `l1` есть
обёртки над базовыми обёртками над системными вызовами линукса.
- - Шаблоны: `Vec<T>`, `Option<T>`, `Span<T>`, `MutSpan<T>`, `Result<O>Or<E>`.
Ну а раз на нём эти шаблоны определены,
то в этом же уровне код из папки `src/l1/anne` инстанциирует их для
ВСЕХ будущих целей.
- - `gen/l1/geom.h` -- файл со структурами
векторов и матриц, их методами для операций из линейной алгебры.
- - В `gen/l1/pixel_masses.h` инстанциированы матрицы пикселей,
или же проще - текстуры,
хранящиеся в памяти максимально тривиальным способом.
#block(width: 100%, inset: 5pt, fill: luma(90%), radius: 4pt)[#par()[
В `gen/l1` лежат
много много крутых заголовков, но `geom.h` и `pixel_masses.h` особенны тем,
что они не содержат в себе осведомление о высших уровнях, они пришли от самого `l1`,
а не просто зашли в `l1`, поскольку здесь впервые стали доступны методы для их
генерации.
]]
- `l1_4` -- Тут просто лежат тесты для шаблонов `l1`.
- `l1_5`
- - Инстанциация `Box`, `Ref`, `RefMut` для трейта (инстанциация
'громадных' ссылок).
- - `gen/l1_5/marie/clipping.h` - дурацкий бесполезный файл для
пересечения треугольников. Даже не тестил его. Удалю его когда-нибудь.
- - Шаблоны `BuffRBTree_Set<T>`, `BuffRBTree_Map<K, V>`
- `l2`
- - Тесты для `l1_5`.
- - `src/l2/liza` -- пространство имён для обёрток над `pipewire` и работы со звуком.
- - `src/l2/margaret/time_utils.h` -- утилиты для замерки времени с использованием
системных часов.
- - `src/l2/margaret/vulkan_utils.h` -- утилиты для использования Vulkan.
- - `src/l2/marie/graphics_geom.h`, `src/l2/marie/shape_geom.h` -- больше
алгебраических утилит, более специализированных.
- - `src/l2/marie/rasterization.h` -- Растеризация треугольников на процессоре.
- - `src/l2/marie/texture_processing.h` -- Утилиты для обработки текстур
на процессоре. Как и код, куча текстур генерируется автоматически, для этих целей
создан этот файл.
- - Тесты к `l1_5`
- - Особые тесты, названные `r` + `<число>`. Они не строгие их просто надо
запустить и проверить, что они запускаются.
Этот список не полный.
#heading(depth: 2)[Утилиты и классификация типов]
Весь код в Дарье сильно отличается от привычного кода на C из-за многочисленных
утилит, использующихся повсеместно. Тот, кто не знает всех утилит, определяемых в
`l1`, не сможет дальше понять ничего.
Нигде в Дарье я не принимаю во внимание случай Out of Memory. Когда `malloc`
возвращает `NULL` я немедленно завершаю программу. Поэтому я ввёл
функции `safe_malloc`, `safe_calloc`, `safe_realloc`. Они работают как и их
аналоги из стандартной библиотеки, но тебе не нужно беспокоиться о том что они вернут
ошибку. Хочешь понять код Дарьи - начни читать `src/l1/core/int_primitives.h`, там
определены слиасы для имён числовых типов. Вместо `uint32_t` надо писать
`U32`, дабы уподобиться `Holy C`. Дальше читай `src/l1/core/util.h` чтобы понять
все утилиты ничего незнакомого. Не всё здесь уникально для Дарьи, например,
```c #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) ```
здесь определён как и во многих других проектах на C.
Дальше логически идёт `src/l1/core/VecU8_as_str.h`. Но у этого файла есть одна
"движущаяся" зависимость - откуда он берёт `VecU8`. Первоначально он берёт
часть методов `VecU8` из `src/l1/core/chicken_VecU8.h`. Прочитай его дабы понять какие
методы будут у вектора. `src/l1/core/VecU8_as_str.h`
инклюдит `src/l1/core/chicken_VecU8.h` когда определён "флаг"
`PROTOTYPE1_L1_CODEGEN_BOOTSTRAP_USE_CHICKEN_VECU8`. Поти всегда, когда что-то будет
иметь дело строками, он будет инклюдить `VecU8_as_str.h`
Теперь можешь заглянуть в `Makfile` и убедиться, что его структура совпадает с той,
что я описал. Для некоторых тестов есть своя отдельная Makefile-цель `run_` +
`<имя теста>`.
Определяются следуюущие трейты для типов

280
src/l1/sobiralka.py Normal file
View File

@ -0,0 +1,280 @@
import sys
import os
from argparse import ArgumentParser
from typing import List, Tuple, Set
from dataclasses import dataclass
token_type_if = 0
token_type_elif = 1
token_type_ifdef = 2
token_type_elifdef = 3
token_type_ifndef = 4
token_type_elifndef = 5
token_type_else = 6
token_type_endif = 7
token_type_regular = 8
token_type_include_local = 9
token_type_define = 10
@dataclass
class LineToken:
type: int = 5
symbol: str = ""
filepath: str = ""
original: str = ""
@dataclass
class IfDirBlock:
new_symbol_set: Set[str]
text: str = ""
@dataclass
class IfDirCondAndBlock:
cond: LineToken
inside: IfDirBlock
class ParserObj:
s: str
index = 0
def __init__(self, _s):
self.s = _s
def skip_whitespace(self):
while self.index < len(self.s) and self.s[self.index].isspace():
self.index += 1
def skip_word(self):
while self.index < len(self.s) and (self.s[self.index].isalnum() or self.s[self.index] == '_'):
self.index += 1
def parse_first_word(s):
p = ParserObj(s)
length = len(s)
p.skip_whitespace()
if p.index >= length or s[p.index] != '#':
return ""
p.index += 1
start = p.index
p.skip_word()
return s[start:p.index]
def parse_second_symbol(s):
p = ParserObj(s)
length = len(s)
p.skip_whitespace()
if p.index >= length or s[p.index] != '#':
raise RuntimeError("How????")
p.index += 1 # Skip the '#'
p.skip_word()
p.skip_whitespace()
start = p.index
p.skip_word()
if start == p.index:
raise ValueError("This is just fucking disappointing")
return s[start:p.index]
def parse_second_quoted_path(s):
p = ParserObj(s)
length = len(s)
p.skip_whitespace()
if p.index >= length or s[p.index] != '#':
return None
p.index += 1
p.skip_word()
p.skip_whitespace()
if p.index >= length or s[p.index] != '"':
return None
p.index += 1
start = p.index
# Find the closing quote
while p.index < length and s[p.index] != '"':
p.index += 1
if p.index >= length:
return None
return s[start:p.index]
def analyze_file(lines_dirty) -> List[LineToken]:
lines = list(map(lambda x: x.rstrip(), lines_dirty))
i = 0
tokens = []
while i < len(lines):
original_lines = ""
line = ""
while True:
original_lines = original_lines + lines[i] + "\n"
partial_line = lines[i]
i += 1 # i variable should not be used or changed in the rest of the cycle
if partial_line.endswith("\\"):
partial_line = partial_line[:-1]
line += partial_line
else:
line += partial_line
break
directive = parse_first_word(line)
if directive == 'include':
local_file = parse_second_quoted_path(line)
if local_file is not None:
tokens.append(LineToken(type = token_type_include_local, filepath=local_file,
original=original_lines))
else:
tokens.append(LineToken(type = token_type_regular, original=original_lines))
elif directive == 'define':
symbol = parse_second_symbol(line)
tokens.append(LineToken(type=token_type_define, symbol=symbol, original=original_lines))
elif directive == 'ifdef':
symbol = parse_second_symbol(line)
tokens.append(LineToken(type=token_type_ifdef, symbol=symbol, original=original_lines))
elif directive == 'ifndef':
symbol = parse_second_symbol(line)
tokens.append(LineToken(type=token_type_ifndef, symbol=symbol, original=original_lines))
elif directive == 'elifdef':
symbol = parse_second_symbol(line)
tokens.append(LineToken(type=token_type_elifdef, symbol=symbol, original=original_lines))
elif directive == 'elifndef':
symbol = parse_second_symbol(line)
tokens.append(LineToken(type=token_type_elifndef, symbol=symbol, original=original_lines))
elif directive == 'else':
tokens.append(LineToken(type=token_type_else, original=original_lines))
elif directive == 'endif':
tokens.append(LineToken(type=token_type_endif, original=original_lines))
elif directive == 'if':
tokens.append(LineToken(type=token_type_if, original=original_lines))
elif directive == 'elif':
tokens.append(LineToken(type=token_type_elif, original=original_lines))
else:
tokens.append(LineToken(type=token_type_regular, original=original_lines))
return tokens
# Returns new_i, block (text + new_symbol_set)
def process_part_of_file(tokens: List[LineToken], defined_symbols_outside: Set[str], i: int, input_dir,
take_seriously: bool) -> Tuple[int, IfDirBlock]:
output = ""
symbols_here = defined_symbols_outside.copy()
in_if: List[IfDirCondAndBlock] = []
saw_unevaluatable_condition = False
saw_else = False
saw_match = -1
while i < len(tokens):
token = tokens[i]
t = token.type
i += 1
if in_if and (t == token_type_if or t == token_type_ifdef or t == token_type_ifndef or
t == token_type_include_local or t == token_type_define or
t == token_type_regular):
raise ValueError("Okay, listen here, you little fuck")
# When saw_else is true, we are definitely in_if
if saw_else and t != token_type_endif:
raise ValueError("Skill issue lol")
if not in_if and t in [token_type_elif, token_type_elifdef, token_type_elifndef,
token_type_endif, token_type_else]:
i -= 1
break
if t == token_type_if or t == token_type_elif:
saw_unevaluatable_condition = True
# If saw_unevaluatable_condition, nobody cares if we saw_match
if saw_match == -1:
if (t == token_type_ifdef or t == token_type_elifdef) and (token.symbol in symbols_here):
saw_match = len(in_if)
if (t == token_type_ifndef or t == token_type_elifndef) and (token.symbol not in symbols_here):
saw_match = len(in_if)
if t == token_type_else:
saw_match = len(in_if)
if t == token_type_else:
saw_else = True
if t in [token_type_if, token_type_ifdef, token_type_ifndef,
token_type_elif, token_type_elifdef, token_type_elifndef, token_type_else]:
i, block_in_front = process_part_of_file(tokens, symbols_here, i, input_dir,
(saw_match == len(in_if) or (t in [token_type_if, token_type_elif]) or
(t == token_type_else and saw_unevaluatable_condition)) and take_seriously)
in_if.append(IfDirCondAndBlock(cond=token, inside=block_in_front))
if t == token_type_endif:
assert in_if
if saw_unevaluatable_condition:
for if_block in in_if:
output += if_block.cond.original
output += if_block.inside.text
output += token.original
elif saw_match > -1:
output += in_if[saw_match].inside.text
symbols_here = in_if[saw_match].inside.new_symbol_set
saw_unevaluatable_condition = False
saw_else = False
saw_match = -1
in_if = []
if t == token_type_define:
symbols_here.add(token.symbol)
output += token.original
if t == token_type_include_local:
if take_seriously:
include_path = os.path.join(input_dir, token.filepath)
if not os.path.isfile(include_path):
raise ValueError(f"No such file {include_path}")
included_output = process_file(include_path, symbols_here)
output += included_output.text
symbols_here = included_output.new_symbol_set
else:
output += token.original
if t == token_type_regular:
output += token.original
if in_if:
ValueError("You reached for the EOF too soon")
return i, IfDirBlock(text=output, new_symbol_set=symbols_here)
# Returns block (text + new_symbols)
def process_file(file_path, symbols_here) -> IfDirBlock:
input_dir = os.path.dirname(os.path.abspath(file_path))
with open(file_path, 'r') as f:
lines = f.readlines()
tokens = analyze_file(lines)
end_i, block = process_part_of_file(tokens, symbols_here, 0, input_dir, True)
assert end_i == len(tokens)
return block
def main():
parser = ArgumentParser(description="Sobiralka")
parser.add_argument('input_file', type=str, help="Input C file path")
parser.add_argument('output_file', type=str, help="Output C file path")
parser.add_argument('-D', action='append', default=[], help="Define symbols")
args = parser.parse_args()
defined_symbols = set(args.D)
entire_program = process_file(args.input_file, defined_symbols)
with open(args.output_file, 'w') as f:
f.write(entire_program.text)
if __name__ == "__main__":
main()

View File

@ -2,7 +2,7 @@
#define PROTOTYPE1_SRC_L1_SYSTEM_FSMANIP_H
/* For posix */
#include "sys/stat.h"
#include <sys/stat.h>
#include <errno.h>
#include "../core/util.h"

View File