Info Guide #12
31 декабря 2017

Системки - NedoLang: Начало - самый простой процедурный язык (часть 1).

<b>Системки</b> - NedoLang: Начало - самый простой процедурный язык (часть 1).
      NedoLang: Начало
Alone Coder

   В  сентябре  2016 года на работе встала
задача  -  найти  отечественный компилятор
промежуточного  языка  под процессоры ARM,
который  можно применить в военной аппара─
туре. Мы  разрабатываем  новую  систему, и
загружаемые  программы  на  языке высокого
уровня нам бы очень помогли.
   Всякие  Phyton'ы мы поставлять не могли
- они  работают  только под Windows. Был в
теории  вариант компилятора под ARM в сос─
таве версии Astra Linux под ARM же (на ка─
ком  компьютере  его запускать?). А "прог─
раммы  из  интернета" в военной аппаратуре
использовать нельзя, всё должно быть сдано
с децимальным  номером  в  соответствии  с
ГОСТами. Можно, конечно, было  распечатать
миллионы  строк  исходника GCC и сдать как
свою программу... со всеми закладками :)
   В прошлом номере Info Guide как раз об─
суждались компиляторы. Возможно,я читал те
статьи подробнее, чем вы,- я же их перево─
дил :) В голове  после этого была картина,
которой не было 10 лет назад,когда я хотел
писать ОС  и начинал собирать идеи по сис─
темам программирования в одну папочку.
   Не  было особой надежды, что на ZX поя─
вятся  нативные  версии  Oberon  и ZX Like
Pascal, полного исходника Hitech C не наш─ 
лось,а под компиляторы z88dk и SDCC просто
не хватит памяти. Хотя, впрочем,был деком─
пилированный  исходник Turbo Pascal, но он
написан на ассемблере,такое развивать тру─
дно. Даже  авторы iS-DOS не сделали норма─
льный  язык высокого уровня для разработки
под свою систему.
   Почему бы не написать продукт, полезный
и тут, и там?

                  * * *

   Как  раз  в это  время я в общих чертах
закончил  3D  движок в стиле Total Eclipse
(я  его  показывал  на  ближайшей  тусовке
NedoPC ), и голова несколько освободилась.
   В  общем, ради эксперимента я сел и на─
писал  самый простой процедурный язык, ка─
кой  пришёл  в голову. Он компилировался в
ассемблерный текст и переваливал на ассем─
блер  всю работу с метками. Сначала у него
даже не было типов. Самое сложное там было
- вычисление  выражений. И то несложное. А
работало  всё в формочке Delphi и выдавало
примерно такой код:

;PUSHNUM 1 
   PUSH HL
   LD HL,0+1
;POPVAR _nemain_v1 
   LD [_nemain_v1],HL
   POP HL
;PUSHNUM 2 
   PUSH HL
   LD HL,0+2
;PUSHNUM 2 
   PUSH HL
   LD HL,0+2
;OPERATION + 
   POP DE
   ADD HL,DE
;POPVAR _nemain_v2 
   LD [_nemain_v2],HL
   POP HL
;CALL tnemain 
   CALL tnemain
;PUSHNUM 2 
   PUSH HL
   LD HL,0+2
;NEG 
   XOR A
   SUB L
   LD L,A
   SBC A,H
   SUB L
   LD H,A
;POPVAR _main_b 
   LD [_main_b],HL
   POP HL

   У каждого  регистра была своя роль. Это
что-то вроде кодогенератора в C Warp.
   Важно  отметить, что это уже изначально
был  именно язык высокого уровня, а не ас─
семблер  с  процедурным  синтаксисом  типа
"Sphinx C--" by Peter Cellik, "Трамплина" 
С. Веремеенко, "PLM" Александра Корюшкина 
или  "K65" уKK для Atari 2600. Планирова─
лись только  подсказки компилятору для бо─
лее эффективной кодогенерации.

   У  меня  были давние тёрки с языком Си,
поэтому  первые  версии  компилятора имели
очень далёкий от него синтаксис.
   Для начала, уже на уровне лексера исхо─
дник  воспринимался как чередование слов и
односимвольных знаков. Это чрезвычайно ле─
гко разбирать. Была только пара исключений
типа  скобок, но  они не создавали больших
затруднений - просто  игнорировалось "пус─
тое" слово.
   Параметры  функций принципиально не от─
личались  от локальных меток, в параметрах
стоял  оператор присваивания, а когда поя─
вились  типы - и  тип.  И хранились они (и
пока до сих пор хранятся) именно как лока─
льные переменные.Рекурсия делалась и дела─
ется  путём сохранения на стеке старых па─
раметров (во время  вызова) и старых лока─
льных переменных (во время входа-выхода).

   Когда я набросал интерфейс вызова функ─
ций (с именованными  параметрами), я пока─
зал компилятор паре спектрумистов. Разуме─
ется, отзывов особых не было - ведь реаль─
но  в этом  компиляторе ничего нельзя было
собрать. Я тогда даже не проверял, что ре─
зультирующий  ассемблерный текст ассембли─
руется. А что он работает...Как это вообще
проверить? Я мог только проверить,что каж─
дый  оборот языка транслируется в конкрет─
ный оборот ассемблерного кода.Но это нель─
зя зафиксировать навечно,иначе не получишь
хороший выходной код!

   Потом, подумав в сторону крупных проек─
тов, я  внедрил концепцию вложенных прост─
ранств имён, так что внутри функции другая
функция  выглядела  как... ну, представьте
себе, как  вы обращаетесь к файлу в другой
директории:
   ../dir2/filename
   Вот так же и с функциями, только вместо
слешей тоже использовались точки:
   .func2.variable
   Шикарно? Сам придумал:) А использование
_variable для обозначения глобальных пере─
менных подсказал сосед Гриша,до этого гло─
бальные переменные помечались знаком$ или
# справа.
   На самом деле странно, что в Си и UNIX,
которые  вроде как писали одни и те же лю─
ди,совершенно разное понимание пространств
имён (а заодно и форматов вызова).
   Но   сразу   пришлось   отказаться   от
.func2.variable в параметрах вызова - сли─
шком длинно для реальной работы. Но снача─
ла  просто  не  было  возможности  сделать
по-другому - ведь  даже если запомнить имя
(точнее, полный путь) вызываемой функции и
приклеивать к нему variable, то внутри па─
раметра может быть ещё вызов функции, и мы
тут  же забудем текущее имя! Да-да, в ком─
пиляторе  была рекурсия и строки в локаль─
ных переменных. А как это будет самокомпи─
лироваться, когда станет продуктом?

   Строки сначала лежали вAnsiString. Это
же Delphi. Но долго они там лежать не мог─
ли.Я не планировал реализовывать объектно-
ориентированное  программирование. (Вообще
много чего не планировал, но потом появля─
лась  возможность. Может, приду  и к ООП.)
Логично  было  предположить, что  строки в
конце  концов  будут  лежать  в статически
выделенном массиве длиной как максимальный
идентификатор. Но  сохранять  такой массив
на  стеке... Стек  не резиновый. Так что я
решил убрать массивы из локальных перемен─
ных, а грязную работу по склейке меток пе─
ревалить на ассемблер.

   На первом этапе  язык удерживался в как
можно  малых размерах. Исходник состоял из
парсера (порядка1000 строк) и кодогенера─
тора (порядка500 ).Система на ZX Spectrum
предполагалась как постоянно висящая в па─
мяти вместе с текстовым редактором и одним
текущим  исходником. Я  даже  предполагал,
что  48K  хватит  на  такой сервис. Почему
48K? А это чтобы не поддерживать банкинг в
компиляторе.

            А теперь подробно:

  14-16.09.2016 - начат проект. В дневни─
ке  написано  "писал  компилятор". Судя по
логам #mhm, первая  версия  от 14  числа
весила  всего200 строк. Она компилировала
исходный текст из одного поля окна в псев─
докод в другом поле того же окна.Но от неё
сохранились только файлы*.res и .dpr. 

  22.09.2016 - отправил  первый  релиз на
оценку спектрумистам:
┌────────────────────────────────────────┐
   Здравствуйте, товарищи!
   Представляю вашему  вниманию новый язык
программирования :) Язык оптимизирован под
удобство компиляции (переобероним Оберон:)
следующим образом:
   1) после слова всегда ожидается символ,
он сразу считывается, никаких "подглядыва─
ний на символ вперёд".
   2) все символы однобайтовые.Поэтому при
разборе какого-нибудь"<" не надо смотреть
в  следующий  символ. Все  редкие операции
кодируются  через escape-коды (> для >=,
< для<=,= для !=,* для <<,/ для >> ), 
в разбиральщик  они уже поступают односим─
вольными.
   3) нет  ни  одного   зарезервированного
слова. В  поле  команды  ожидается  только
команда.  Поэтому   присваивание  делается
командой=var(expression) (для переменных)
или командой*value(expression) (для ячеек
памяти).
   4) отсутствует  таблица меток. Это воз─
лагается  на  ассемблер. Точнее, в текущей
реализации  есть  список  меток, но только
для того, чтобы их красиво сгенерить в ко─
нце исходника (но можно так же успешно ге─
нерить  их  в  середине с помощью org'ов с
переназначаемыми метками). Из-за этого ло─
кальные  и  глобальные метки различаются с
помощью постфикса#.
   5) отсутствует C-like вызов функций.Па─
раметры передаются по имени,прямо в ячейку
памяти.
   6) рекурсия  описывается явно - с помо─
щью программных скобок
   var:int(parameter)command
(так описываются все локальные переменные,
которые  затрагиваются  рекурсией).  После
рекурсивного вызова локалы считаются испо─
рченными, их можно использовать только вне
этих программных скобок.
   7) константы  в выражениях  описываются
явно - через=(constexpr)
   Фичи:
  - комментарии вложенные.
  - нет точек с запятой.
   Косяки:
  - цикл толькоwhile (поправимо).
  - метки  для  переходов только локальные
(поправимо).
  - нет функций с возвращаемым значением.
  - нет  типов. Сейчас все переменные типа
u16. Нет ли идей, как  реализовать функции
и типы, не сильно усложняя компилятор?
   Вот как выглядит исходный текст:
int(a)
proc(nemain)
(
par:int(v1) ( {recursive param}
par:int(v2) ( {recursive param}
=a# (=(1>5)+2*5)
=a# (*(22/#33/-v1)--6*7+-a#)
*a# ("as")
)
)
)
proc(main)(
int(b)
int(c)
call(nemain,v1(1),v2(2+2))
=b (-2)
{=a# (0)} 
while ((a#<5)|b&#ff)(
 =a# (a#+%1)
 goto(mylabel)
 while (c=0) =c(1)
 :mylabel:
 =b (b+a#)
)
) {proc}

   Вот как выглядит результат:

   ORG #8000
begin:
;.CSEG 
tnemain:
;PUSHVAR _nemain_nemain_v1 
   PUSH HL
   LD HL,[_nemain_nemain_v1]
;PUSHVAR _nemain_nemain_v2 
   PUSH HL
   LD HL,[_nemain_nemain_v2]
;PUSHNUM (1>5) 
   PUSH HL
   LD HL,0+(1>5)
;PUSHNUM 2 
   PUSH HL
   LD HL,0+2
;PUSHNUM 5 
   PUSH HL
   LD HL,0+5
;OPERATION * 
   LD B,L
   POP HL
   ADD HL,HL
   DJNZ $-1
;OPERATION + 
   POP DE
   ADD HL,DE
;POPVAR __a 
   LD [__a],HL
   POP HL
;PUSHNUM 22 
   PUSH HL
   LD HL,0+22
;PUSHNUM #33 
   PUSH HL
   LD HL,0+#33
;OPERATION / 
   POP DE
   CALL DIV_DE_HL
;PUSHVAR _nemain_v1 
   PUSH HL
   LD HL,[_nemain_v1]
;NEG 
   XOR A
   SUB L
   LD L,A
   SBC A,H
   SUB L
   LD H,A
;OPERATION / 
   POP DE
   CALL DIV_DE_HL
;PEEK 
   LD A,[HL]
   INC HL
   LD H,[HL]
   LD L,A
;PUSHNUM 6 
   PUSH HL
   LD HL,0+6
;NEG 
   XOR A
   SUB L
   LD L,A
   SBC A,H
   SUB L
   LD H,A
;PUSHNUM 7 
   PUSH HL
   LD HL,0+7
;OPERATION * 
   POP BC
   CALL MUL_BC_HL
;OPERATION - 
   EX DE,HL
   POP HL
   OR A
   SBC HL,DE
;PUSHVAR __a 
   PUSH HL
   LD HL,[__a]
;NEG 
   XOR A
   SUB L
   LD L,A
   SBC A,H
   SUB L
   LD H,A
;OPERATION + 
   POP DE
   ADD HL,DE
;POPVAR __a 
   LD [__a],HL
   POP HL
;PUSHVAR __a 
   PUSH HL
   LD HL,[__a]
;PUSHNUM "as" 
   PUSH HL
   LD HL,0+"as"
;POKE 
   EX DE,HL
   POP HL
   LD [HL],E
   INC HL
   LD [HL],D
   POP HL
;POPVAR _nemain_nemain_v2 
   LD [_nemain_nemain_v2],HL
   POP HL
;POPVAR _nemain_nemain_v1 
   LD [_nemain_nemain_v1],HL
   POP HL
   RET
...
;.DSEG 
__a:
   DW 0
_nemain_v1:
   DW 0
_nemain_v2:
   DW 0
_main_b:
   DW 0
_main_c:
   DW 0
end:
└────────────────────────────────────────┘
   К письму прилагался исходник компилято─
ра. Судьба  сложилась  так, что эта первая
релизная версия так и сохранилась только в
письме. Зато  все следующие версии склади─
ровались в архив.

  23.09.2016:
  - добавил  экспорт  комментов  и номеров
строк в тело выходного ассемблерного файла
  - добавил массивы какint(A[15]). Доступ
к ячейкам  пока  только через вычисление с
использованием константы[WORDSIZE] и раз─
адресации@var
  - сменил=(constexpr) на [constexpr]
  - добавилif()then()else()

  05.10.2016 - добавлена поддержка имено─
ванных пространств имён (командаmodule ):

module(vasya)(
int(a2)
int(array[15])
proc(dup)(
 int(p1)
 int(p2)
 if(p1#0)then(=p2(p1))else()
 *(@array$+[7*WORDSIZE])(555)
 =p2(*(@array$+p1*[WORDSIZE]))
 return(p1+p1)
)
...
) {module}
) {end}

   Из этого выдавался код в таком стиле:
;PUSHNUM 0 
   PUSH HL
   LD HL,0+0
;OPERATION = 
   POP DE
   OR A
   SBC HL,DE
   JR Z,$+5
   LD HL,#FFFF
;JNZ l.vasya.dup.0 
   LD A,L
   OR H
   POP HL
;END COUNT 
   JP NZ,l.vasya.dup.0
;line 8 
;BEGIN COUNT 
;PUSHVAR _.vasya.dup.p1 
   PUSH HL
   LD HL,[_.vasya.dup.p1]
;POPVAR _.vasya.dup.p2 
   LD [_.vasya.dup.p2],HL
   POP HL
;END COUNT 
l.vasya.dup.0:
;JUMP l.vasya.dup.1 
   JP l.vasya.dup.1
l.vasya.dup.1:

  11.10.2016:
  - появились типы(INT и UINT) - отличаю─
тся наличием знака у констант.Неудобно,что
проверка int/uint стоит в двух местах - в
compile_command  и  вcompile_var. Надо бы 
символ-префикс, который  обрабатывать  как
команду? Тогда  не  будет и пересечения по
первой букве. (Потом просто выделил чтение
типа в отдельную процедуру,а много позже -
внутрь  чтения идентификатора, так что имя
типа лежит в таблице меток.)
  - команда вызоваCALL заменена на ?
  -ELSE заменено на ~ , ибо было обязате─
льным (позже я откатил это изменение)

  12.10.2016:
  - добавлены типы BOOL, CHAR, STRING (но
пока не поддерживаются)
  - появилась проверка типа по таблице ме─
ток. До  этого  были надежды вообще убрать
таблицу меток:
   контроль типов можно возложить на ассе─
мблер,если к каждому слову приписывать тип 
   но  для этого надо откуда-то знать типы
переменных, и при присваивании, и при чте─ 
нии! 
   случаи:
  1. параметр функции - добавить тип легко
(внутри /**/) 
  2. чтение глобала
  3. запись глобала
  4. чтение локала
  5. запись  неменяющегося  локала  (можно
совместить с определением) 
  6. запись меняюшегося локала
  7. вызов  функции - добавить  тип  легко
(внутри /**/) 
   можно  _xxxxxxx в контексте терма заре─
зервировать не под глобалы,а под подсказки 
компилятору (в Си там будет пусто) 
   или  весь  набор типов продублировать с
подчёркиванием?  (как тогда расширять сис─ 
тему типов?) 
   или  кодировать  тип в имени переменной
   (как тогда расширять систему типов?) 
ivar 
uvar - нет в венгерской нотации? 
lvar 
cvar - нет в венгерской нотации 
bvar - в венг. нотации это bool или byte 
fvar - хотелось бы для flag (бывает иногда 
       в венгерской нотации) 
pvar - в венгерской нотации не пишется тип 
       указателя?
   Это всё - плохой стиль венгерской нота─
ции (хороший - это  класс значения или его 
размерность) 

  - переменные и параметры объявляются ко─
мандой+ ,а локальные переменные рекурсив─
ных  функций  оформляются какstacked (при
вызове тоже):

func(nemain)stacked(uint(z1)int(z2))
(
+uint(v1) {parameter}
+int(v2) {parameter}
(
 =a$(?dup(p1(+15)p2(11))){call function}
 *a$("as"){write memory}
)
)

  - в компиляторе  отдельные процедуры вы─
вода комментариев(emitcomment) и отладоч─
ной информации(emithint)
  - в кодогенераторе проверяется занятость
регистров  (до  этого  одинаковые  команды
всегда компилировались одинаково),но прак─
тически это не использовано,текущее значе─
ние всегда вhl или de
  - добавлены  операции  сдвига  влево   и
вправо на 1 бит (префиксные< , > )
  - добавлен типBYTE для переменных

  13.10.2016:
  - типы у функций
  - типBYTE поддержан и в параметрах фун─
кций, ветвление по типу перенесено из ком─
пилятора в кодогенератор
  - в кодогенераторе  проверяется  глубина
стека
  - выделены  модули emits (вывод сообще─
ний)  иemitdirectives (вывод директив ас─
семблера, которые  предполагались машинно─
независимыми)
  - кодогенератор  разделён  на процедуры,
не проверяющие занятость регистров (досту─
пные  компилятору),  и  проверяющие  их  и
генерирующие  непосредственно  код (нижний
уровень - недоступные компилятору)

  14.10.2016:
  - добавлен типLONG для переменных.Лонги
сделаны равными по размеру двум интам. Был
вопрос, в  каком порядке хранить половинки
в стеке, я выбрал младшую часть на вершине
(так можно складывать-вычитать, имея всего
три регистра)
  - лексер  выделен в модульreads, компи─
лятор  выделен в модульcompile (в главном
модуле остался GUI)

  17.10.2016:
  - типLONG поддержан у функций
  - в  кодогенераторе   процедуры  занятия
регистров emitgethl, emitgetde, emitgetbc
заменены наemitgetreg с параметром, доба─
влен  байтовый  контекст с другим порядком
использования регистров. Все возможные со─
стояния регистров выглядели так:
   -
   hl
   hl,de
   a
   c,a
   hl,a
   hl,c,a
   bc,hl,de
   bc,hl

  18.10.2016
  - в кодогенераторе в отдельные процедуры
передаётся  номер регистра, который мы по─
лучаем черезgetnewreg (вершина стека) или
getoldreg  (предыдущее  значение) - теперь
сравнения  "больше"  и  "меньше"  делаются
одинаково
  - для сравнений сделана отдельная проце─
дура вычитания,которая выдаёт только флаги
  - часть кодогенератора, доступная компи─
лятору  (машиннонезависимая),  выделена  в
модульemitcommands
  - в заголовке  рекурсивных функций слово
stacked  с перечнем  локальных  переменных
заменено  на local (всё  равно  я не смог
придумать код, который одинаково обрабаты─
вает оба словаstacked - здесь и при вызо─
ве)

  19.10.2016:
  - добавлена перенумерация регистров (мо─
дульregs ), теперь с точки зрения кодоге─
нератора  все  регистры  равноправны. Но в
самом модуле regs пока что ограничены воз─
можные состояния регистров
  - в 8-битном контексте  возможные состо─
яния  регистров теперь (решил сэкономить с
помощьюpush bc...pop af ):
   -
   b
   a
   b,a

  20.10.2016:
  - регистры  выделяются не по списку воз─
можных состояний,а по приоритетам (hl,de,
bc для 16-битного контекста,a, h для бай─ 
тового контекста)
  - изучал  систему команд ARM Thumb и был
в шоке, что нет простого способа использо─
вать константу. Отложил ARM на потом,когда
отлажу Z80. В комплекте компилятора появи─
лась памятка по командам ARM.
   Возник  вопрос - как  компилировать под
несколько процессоров?
  а) ветки   в  каждой  процедуре emit...
(другие варианты,другие коды команд,другие 
таблицы названий регистров) 
число регистров не должно отличаться 
  б) отдельный кодогенератор как программа
неудобно будет отладить передачу данных от 
компилятора к кодогенератору 
и будет тормозить 
  в) объектыemitasmz80,emitasmarm,насле─
дованные от одного классаemitasm
неудобно  будет переписывать компилятор на 
свой язык 
  г) структура  с адресами  всех  процедур
emit...
заполняется  вinitz80иinitarm(вызываем 
одну из двух) 
  д) код  для  каждого таргета в отдельных
файлах, генерируется отдельный экзешник 
как собирать из разных файлов? через копи─ 
рование? или  собрать все машиннозависимые 
инклюды в одном главном модуле? 

   Выбрал последний вариант.

  21.10.2016 - багфиксы  (особенно приме─
чательно былоSLA вместо SRL )



Другие статьи номера:

Помощь - об оболочке: произошли некоторые изменения в кнопках.

Предисловие - от авторов: Прошедшие два года были очень насыщенными.

Комьюнити - ZX Spectrum: Как это было в Рязани (1980-е).

Комьюнити - ZX Spectrum: Как это было в Рязани (1991-1993).

Комьюнити - ZX Spectrum: Как это было в Рязани (1993-1995).

Комьюнити - ZX Spectrum: Как это было в Рязани (1995-1997).

Комьюнити - сценеры шутят.

Код - этюды: вызов процедур по списку адресов.

Код - 3D демы на ZX Spectrum: история развития 3д движков.

Код - 3D движок: оптимизация на прообразе 3D Construction Kit.

Код - 3D движок: фрагменты.

Код - Посекторный движок для 3D-шутера от Destr.

Код - 3D скролл на ZX Spectrum (часть 1).

Код - 3D скролл на ZX Spectrum: реализация (часть 2).

Графика - графические редакторы: Старый софт от Alone Coder'а.

Графика - палитра: Палитровые эффекты в играх.

Музыка - биперные движки: Двоичная модуляция (часть 1).

Музыка - биперные движки: Двоичная модуляция (часть 1).

Системки - история операционной системы CP/M для Спектрума (часть 1).

Системки - история операционной системы CP/M для Спектрума: ограничения (часть 2).

Системки - NedoLang: Начало - самый простой процедурный язык (часть 1).

Системки - NedoLang: Путь к самокомпиляции (часть 2).

Системки - NedoLang: Проклятие языка Си (часть 3).

Системки - NedoLang: Памяти под самокомпиляцию не хватало (часть 4).

Системки - NedoLang: ускорение (часть 5).

Системки - NedoLang: Куда плыть дальше (часть 6).

Металлолом - Знакомьтесь, ATM-turbo 3! ATM-turbo 3 (v8.0) - что это такое и с чем его едят.

Металлолом - Из истории Betadisk'а: Дисковый интерфейс от Technology Research был.

Дикий ум - Компрессия: Первые компрессоры графики на Speccy (часть 1).

Дикий ум - Компрессия: Фичи с эвристикой, Потоковая декомпрессия, Сжатие музыки (часть 2).

Игрушки - От редакции: 2017-й год вышел очень богатым на события.

Игрушки - интервью с автором игры Mickey the Basic game (Sergio).

Игрушки - квест "Неожиданное Путешествие" - взгляд изнутри.

Игрушки - Nomad: интервью с автором скролл-шутера Nomad (Hippiman).

Игрушки - Скроллинг в Evo SDK.

Игрушки - Hints & Tips: Mickey, Nomad.

Мыльница - Errata: ошибки в Info Guide #11, ACNews #65.

Письма - отзывы о журнале от: raver, destr, sirx, survivor, Ellvis, Utz и Николая Амосова.

Об авторах - Авторы журнала.


Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Умора - Как ломаются полуоси (глава 1-3).
Тема - Бесплатный сыр: что такое shareware программы и как на них заработать.
Рек-тайм - Реклама и объявления ...
SOFTWARE - Описание проходилка игры "Операция Р.Р."
Cafe - правила предстоящего Cafe 2003.

В этот день...   21 ноября