ACNews #63
26 декабря 2016

Описание - Описание языка программирования NedoLang

        Описание языка программирования NedoLang
                     by Alone Coder

Язык в текущей версии не обеспечивает аналогов следующих
конструкций Си:

- многомерные массивы (можно взамен использовать вычисления
индекса или массивы указателей);
- switch...case (предполагается реализовать, т.к. на этой
конструкции строится ассемблер);
- числа с плавающей точкой (предполагается реализовать,
синтаксически они уже разбираются);
- константы const;
- константные массивы const;
- sizeof(<type>);
- #include (предполагается реализовать, если на этой конструкции
будет строиться проект);
- константы #define;
- макросы #define (нерекомендуемое поведение);
- условная компиляция (#if, #ifdef, #ifndef );
- for (следует взамен использовать while или repeat );
- структуры;
- объединения (нерекомендуемое поведение);
- выход из середины функции по return (нерекомендуемое
поведение);
- выход из середины программы по exit (нерекомендуемое
поведение);
- вызов функции по указателю (нерекомендуемое поведение);

Команды собираются в блоки, окружённые фигурными скобками
              {<command><command>...<command>}
Такой блок эквивалентен одиночной команде и допустим везде, где
допустима одиночная команда.
После каждой команды для совместимости с Си рекомендуется (но не
требуется) ставить точку с запятой.

В одной строке может быть сколько угодно команд. Перевод строки
между любыми словами ни на что не влияет.
Номера строк в исходном тексте не пишутся. Поэтому при ошибках
выводится номер строки исходного текстового файла (первая строка
имеет номер 00001).

Стандартные комментарии оформляются как /**комментарий*/ . Если
впереди только одна звёздочка, то это не комментарий (нужно для
оформления вызова процедур и функций, см. ниже).
Стандартные комментарии не могут быть вложенными.
Есть также стандартные однострочные комментарии от // до конца
строки.
Есть также нестандартные комментарии, которые могут быть
вложенными, но они несовместимы с Си:
{комментарий{вложенный комментарий}продолжение комментария}
Комментарии можно ставить между любыми словами, кроме как между
именем переменной и квадратной скобкой индекса массива, то есть
так нельзя:
                    a/**комментарий*/[10]
Все комментарии при компиляции передаются в ассемблерный текст
тоже в виде комментариев.

Полное имя переменной (которое используется в ассемблере)
строится из имени текущей области видимости, имени самой
переменной и постфикса.

- Полное имя "petya.pet." означает: область видимости "petya",
переменная "pet", заданная в исходном тексте (отмечено точкой
".").
-Полное имя "_boolarr." означает: глобальная переменная
"_boolarr", заданная в исходном тексте (отмечено точкой ".").
- Полное имя "vasya.nemain.l00003" означает: область видимости
"vasya.nemain", переменная "l00003", созданная автоматически (не
содержит точки в конце).

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

Переменные и прочие имена доступны из языка следующим образом:

- _boolarr для доступа к глобальной переменной _boolarr из
любого места программы (так же можно получить доступ к
глобальной процедуре или функции).
- i для доступа к переменной i, определённой в текущей области
видимости.
- .func2 для доступа к функции func2, определённой снаружи
текущей области видимости (через такие имена вызываются
процедуры и функции из других процедур и функций).
- ..anothermodule.func5 для доступа к функции func5,
определённой в соседнем модуле anothermodule (то есть в области
видимости, соседней к области видимости, где определена текущая
функция).
- submodule.v для доступа к переменной v, определённой в
дочерней области видимости submodule (на практике такое может
быть только при объявлении вложенных функций с использованием
goto).

Автоматически созданные переменные недоступны из языка.

Идентификаторы (имена переменных, процедур, функций, меток)
должны начинаться с буквы (или знака подчёркивания для
глобальных переменных и глобальных процедур/функций) и состоять
из букв, цифр и знаков подчёркивания. Полное имя переменной не
должно быть длиннее 254 знаков. Разрешается использовать русские
буквы в идентификаторах. Русские буквы остаются в том же виде,
как они закодированы в исходном тексте (в кодировке cp866,
cp1251 или utf8).

Все команды начинаются со слова:

* var - определить переменную:
                     var<type><variable>
* var с квадратными скобками - определить массив фиксированного
размера:
            var<type><variable><[><expression><]>
ячейки массива нумеруются с 0, то есть в массиве a[10] не
существует ячейки с индексом 10.
* recursivevar - определить переменную внутри рекурсивной
процедуры/функции. Оформляется так же, как var. Если внутри
рекурсивной процедуры/функции определить обычную переменную, то
её значение испортится при рекурсии. Нельзя так объявлять
массивы.
* let - вычислить и записать в переменную:
                    let<var>=<expression>
тип выражения должен соответствовать типу переменной, кроме
одного исключения: в переменную типа pointer можно записать
адрес массива, то есть let poi=arr (реально в переменную типа
pointer можно записать что угодно, но при использовании byte или
char результат не определён, а использование long приведёт к
ошибке исполнения).
* let с квадратными скобками - вычислить и записать в ячейку
массива:
           let<var><[><expression><]>=<expression>
* let* - вычислить и записать в память с нужным типом:
       let*(<type>*)(<pointerexpression>)=<expression>
тип (<type>*) и скобки вокруг (<pointerexpression>) пропускать
нельзя.
* module - определить модуль (область видимости):
                    module<label><command>
команда <command> создаётся в области видимости внутри текущей
области видимости (например, если была область видимости
mainmodule, то внутри команды "module submodule{...}" будет
область видимости mainmodule.submodule. Можно повторно
определять одну и ту же область видимости, чтобы добавлять туда
что-то новое.
* proc - определить процедуру:
          proc<procname>([<type><par>,...])<command>
тоже создаёт область видимости внутри текущей области видимости.
Поэтому <procname> должно быть уникальным внутри текущей области
видимости.
* func - определить функцию:
       func<type><funcname>([<type><par>,...])<command>
тоже создаёт область видимости внутри текущей области видимости.
Поэтому <funcname> должно быть уникальным внутри текущей
области видимости.
* if - альтернатива:
     if<boolexpression>then<command>[else<command>]endif
слово then против ошибки "if(expr);cmd", слово endif против
ошибки вложенных неполных альтернатив. Для совместимости с Си
условие рекомендуется писать в круглых скобках.
* call - вызвать процедуру:
call<label>[recursive]([<type><label>.<par>=<expression>,...])
для совместимости с Си надо писать в виде:
                call/*.*/<procname>[recursive]
       ([/*<type>.<procname>.<par>=*/<expression>,...])
Имя вызываемой функции и параметры пишутся в области видимости
вызывающей функции, поэтому используется точка перед именем (для
глобальных процедур она не нужна). Слово recursive обязательно
для вызова рекурсивных процедур, иначе при рекурсивном вызове
параметры будут испорчены.
* while - цикл с предусловием:
              while<boolexpression>loop<command>
слово loop против ошибки "while(expr);cmd". Для совместимости с
Си условие рекомендуется писать в круглых скобках.
* repeat - цикл с постусловием:
             repeat<command>until<boolexpression>
для совместимости с Си условие рекомендуется писать в круглых
скобках.
* break - выйти из цикла while или repeat: параметров не имеет,
просто break
* return - вернуть значение из функции:
                      return<expression>
должна быть последней командой в функции. Тип возвращаемого
значения должен соответствовать типу функции.
* _label - определить метку для перехода:
                     _label<labelname><:>
метка должна быть уникальной внутри текущей области видимости.
Подчёркивание добавлено, чтобы метку было лучше видно в тексте
программы.
* goto - перейти на метку:
                       goto<labelname>
разрешается только внутри текущей процедуры или функции.
* asm - ассемблерная вставка:
                     asm("cmd1""cmd2"...)
каждая команда генерируется как отдельная строка.

Имеются следующие типы данных:

* byte (имеет размер 1 байт, без знака)
* bool (допустимы только значения /*?*/_TRUE и /*?*/_FALSE ,
размер не регламентируется)
* char (имеет размер 1 байт, знаковость не регламентируется)
* int (знаковое целое, имеет размер в одно слово процессора)
* uint (беззнаковое целое, имеет размер в одно слово процессора)
* long (длинное целое, имеет размер в два слова процессора)
[*float ]
* pointer (указатель - имеет размер в одно слово процессора)

Тип bool в текущей версии компилятора имеет размер 1 байт,
/*?*/_TRUE и /*?*/_FALSE определены как 0xff и 0x00. В Си они

могут быть определены так же или по-другому, но в любом случае
не рекомендуется смешивать логические значения с числовыми.
Любое значение, отличное от /*?*/_FALSE, воспринимается как
/*?*/_TRUE, но не рекомендуется использовать этот факт для
будущей совместимости.
Переменные типа long нельзя использовать в сравнениях,
умножениях и делениях, поэтому нельзя говорить об их знаковости.

В выражениях <expression> имеются следующие приоритеты операций:

1. Cкобки и префиксы (+, -, ~ (инверсия), ! (логическое
отрицание), & (взятие адреса переменной), * (чтение по
указателю), а также нестандартные < (сдвиг влево на 1 бит), >
(сдвиг вправо на 1 бит), [<constexpr>] (константное выражение)).
2. Умножение, деление, &, &&.
3. Сложение, вычитание, |, ||, ^, ^^.
4. Сравнения и сдвиги ( <<, >> ). Сравнения и сдвиги работают
только для типов byte, char, int, uint. Для bool разрешены
только сравнения на равенство и неравенство.

В выражениях допустимы следующие виды значений:

* идентификатор переменной - получает тип по типу переменной.
* целая числовая константа без знака - получает тип byte (если
запись в стиле 0xff с не более чем 2 цифрами или в стиле 0b111 с
не более чем 8 цифрами), long (если в конце стоит L ) или uint
(в остальных случаях).
* целая числовая константа со знаком - получает тип int.
* числовая константа с плавающей точкой - получает тип float
(который пока не поддерживается).
* булева константа /*?*/_TRUE или /*?*/_FALSE - получает тип
bool.
* символьная константа 'c' или 'c' (допустимы только 'n',
'r', 't', '', ''', '"', '\' ), где c - один символ -
получает тип char.
* строковая константа "строка" - получает тип pointer.
Допускается склеивание строковых констант типа "str1""str2"
(между ними допустим перевод строки). Строковые константы
создаются автоматически с нулевым кодом в конце.

Целые числовые константы могут быть десятичные ( 100 ),
шестнадцатеричные ( 0x10 ), двоичные ( 0b11 ), восьмеричные
( 0o177 или как вариант 0177 с выдачей предупреждения из-за
двусмысленности записи).

В выражениях строго проверяются типы. В любой операции с двумя
операндами типы операндов должны совпадать, кроме следующих
случаев:

* сложение указателя с int или uint (слагаемое при этом не
домножается на размер типа, т.к. указатели нетипизированные).
* сложение / вычитание / & / | / ^ char с byte (тип берётся по
левому операнду).
* операция & .
* 0L + <uintexpression> .

Для смещения указателя на 1 байт назад используется выражение
<pointerexpression>+-1 . 
Для приведения uint, byte или char к типу int используется
выражение +<expression> (без смены знака) или -<expression> (со
сменой знака).
Для приведения int или long к типу uint используется выражение
<expression>&0xffff . 
Для приведения char, int, uint или long к типу byte используется
выражение <expression>&0xff .
Для приведения uint к типу long используется выражение
0L + <expression> . Запрещается складывать long+uint в остальных
случаях.

Вызов функций оформляется как
              /*@<type>.*/<procname>[recursive]
       ([/*<type>.<procname>.<par>=*/<expression>,...])
(см. выше про /*...*/ для совместимости с Си и про слово
recursive для вызова рекурсивных процедур/функций). Для вызова
глобальных функций точка перед <procname> не нужна (см. выше про
области видимости).
Процедуры и функции технически имеют плавающее число параметров,
но не рекомендуется использовать это поведение.

Чтение по указателю допускается только с непосредственным
приведением типа:
               *(<type>*)(<pointerexpression>)

Рекомендуется все ключевые слова языка писать большими буквами.
Чтобы скомпилировать такой же исходник с помощью компилятора Си,
нужно определить следующие дефайны:

#define POINTER byte*
#define BYTE byte
#define CHAR unsigned char
#define INT short int
#define UINT unsigned short int
#define LONG long
#define FLOAT double
#define BOOL unsigned char
#define _TRUE 0xff
#define _FALSE 0x00
#define FUNC /**/
#define PROC /**/
#define RETURN return
#define VAR /**/
#define LET /**/
#define CALL /**/
#define LABEL /**/
#define LOOP /**/
#define REPEAT do
#define IF if
#define THEN /**/
#define ELSE else
#define ENDIF /**/
#define WHILE while
#define UNTIL(x) while(!(x))

Такой синтаксис - один из вариантов, получившихся из следующих
требований:

- при анализе команд использовать только левый контекст;
- любая команда начинается со слова, чтобы не приходилось
предварительно делать долгий поиск по меткам;
- области видимости меток не объединяются (в Си обращение к
метке может означать обращение к текущей области видимости или
любой из родительских областей видимости);
- при этом должна обеспечиваться совместимость с Си.
Можно переделать синтаксис, например, путём добавления префикса
'+' для тайпкаста (преобразования типов) и вызова функций:
                          +(type)var
                       +(type)func(...)

Сейчас моя система компилирует только под Z80, но
машиннозависимая часть составляет менее 40% объёма исходного
текста, причём это довольно однообразная часть. Так что можно
будет сделать язык и многоплатформенным. По предварительным
оценкам (компиляция старых версий компилятора на IAR и SDCC ),
код компилятора займёт порядка 20-30 килобайт, то есть пока есть
пространство для развития. Но с какой скоростью он будет
компилировать на Спектруме - пока неизвестно. Я стараюсь
соблюдать компромисс между неэффективным кодом и преждевременной
оптимизацией, то есть во всяком случае выбираю такую структуру
кода, которую удобно развивать.

Вот статистика по разным компиляторам (пока самокомпиляции нет):

SDCC 3.6.0 --opt-code-speed --all-callee-saves:
 #6630 байт код с константами + #0036 байт константные массивы +
 #19c7 байт под данные = #802d

IAR Z80/64180 C-Compiler V4.06A/W32 
 Command line  =  -v0 -ml -uua -q -e -K -gA -s9 -t4 -T -Llist
                  -Alist -I../z80/inc/ main.c
 #38b4 байт код + #1076 байт константы + #19c7 байт под данные
 (пишет уже сдвинутый адрес перед defs) = #62f1

Особенности компиляторов и проблемы, которые встанут при
самокомпиляции:

# SDCC 2.9.0 инициализирует константные массивы вручную. SDCC 
3.4.0 нет - всё через ".area _INITIALIZER" (даже нет LDIR и 
полностью отсутствует код инициализации) 

# SDCC 2.9.0 генерирует код stack frame для пустых процедур. 
SDCC 3.4.0 уже нет. мой кодогенератор тоже нет. 
IAR генерирует push-pop регистров в полупустых процедурах 
непонятно зачем. 
SDCC 3.6.0 при opt-size вместо push ix:ld ix,0:add ix,sp пишет 
call ___sdcc_enter_ix (поэтому надо opt-speed). 
ещё при opt-size появляется лишний inc sp:pop af в конце 
emitinvreg, emitnegreg. 
ещё при opt-size (а SDCC 3.4.0 всегда) считает REGNAME[rnew] по 
одному разу (умеет даже push-pop иногда, при --all-callee-saves 
чаще). я так не смогу. 
никаких других различий в SDCC 3.6.0 между opt-size и opt-speed 
не замечено (даже остаются inc sp..dec sp для 8-битных 
параметров). 

# в SDCC все выражения с && || считаются арифметически (в IAR 
нет). самому будет сложно преобразовать 
либо надо делать and if, or else, либо кучу веток в условии, 
либо islalpha... делать на массиве (инициализировать его 
вручную?) 
если разбить на части типа a&&b (это не даёт проигрыша в IAR), 
то SDCC переводит && в джампы (3.6.0 даже в return a&&b) - как 
такое повторить? 
если разбить || на части типа if (a) else if (b) else if (c) с 
одинаковой командой, то SDCC её не объединяет (IAR объединяет),
но работать должно быстрее (в IAR код вообще не изменился) 
если сделать вместо этого if (!a) if (!b) if (!c), то в SDCC код
лучше, в IAR почти идеальный (только со странным выбором 
регистров) 
выражения типа (c<='z') в IAR компилируются невыгодно (не как 
c<('z'+1) ). переписать через +1? мой текущий кодогенератор не 
получит выигрыша, т.к. не умеет CP 

# у большинства процедур есть параметр. IAR передаёт один 
параметр в регистре (SDCC нет). особенно заметно на печати числа 
(впрочем, это отладочная процедура). 
сделать так же один параметр в регистре? (но для этого надо 
уметь хранить переменную в регистре!) 
иногда можно убрать один параметр, если isfunc объединить с 
functype. 
SDCC 3.4.0 достаёт первый параметр через pop:pop:push:push, это 
выгодно при отсутствии стекфрейма в ix. но лучше через регистр. 

# при работе со строками надо одновременно контексты 8 бит и 
слов. Написать весь модуль syscalls на ассемблере? 

# при работе со строками (они как массивы) генерируется ужас. 
ввести указатели? и хранить указатели в регистрах? как 
определить, где что хранить? слово register? 
написать весь модуль syscalls на ассемблере? но строки ещё 
используются в compiler (обкусывание title, запись/чтение меток) 

# SDCC и IAR оптимизируют CALL:RET до JP. как сделать так же? 
ленивый CALL? (не получится при CALL в локале - а без локала 
надо вернуть запись строк в стек!) 
SDCC 3.4.0 генерирует jr:ret (getnothingword), раньше был просто 
jr. В SDCC 3.6.0 исправлено. 

# SDCC 3.4.0 (3.6.0 при opt-size) считает REGNAME[rnew] по 
одному разу (умеет даже push-pop иногда, при --all-callee-saves 
чаще, других различий не видел). я так не смогу. 
todo переписать исходник через переменную (тут надо две, т.к. 
две буквы) 

# stringappend (бывает только для command и joinwordsresult) 
надо inline, но #define для инлайнов у меня нет 

# read, write надо inline (только syscalls инлайном? или emits 
инлайном? или оба? в перспективе останется только системный 
макрос), но #define для инлайнов у меня нет 

# SDCC 3.6.0 вместо ld a,()...ld (),a генерирует 
ld iy:ld a,(iy)...ld (iy),a (readchar_skipdieresis_concat) 
--reserve-regs-iy делает через hl, но возвращает стекфреймы с 
ix, где не надо, и перестаёт уметь такое: 
       ld hl,(_fin)
       push hl
       call _eof
вместо него генерит: 
       ld hl,#_fin
       ld c, (hl)
       inc hl
       ld b, (hl)
       push bc
       call _eof         



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

Содержание - содержание газеты

Новости - написал 3D-движок типа Total Eclipse, Продолжаю собирать материалы по истории Спектрума, плотно занялся компилятором собственного Си-подобного языка.

News - I wrote a 3D engine like Total Eclipse, occupied myself with my own compiler of C-like language, still collect information about the history of ZX Spectrum.

Описание - Описание языка программирования NedoLang

Review - NedoLang programming language

Описание - Операционная система PQ-DOS

Review - PQ-DOS operating system


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

Похожие статьи:
Enlight'97 - правила фестиваля.
BBS - список станций BBS ZXNet.
Hardware - Cистемное П3У Kворумa, плюсы и минусы.

В этот день...   23 июля