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.funcS для доступа к функции funcS, определённой в соседнем модуле 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 определены как Oxff и 0x00. В Си они могут быть определены так же или по-другому, но в любом случае не рекомендуется смешивать логические значения с числовыми. Любое значение, отличное от /*?*/_FALSE, воспринимается как /*?*/_TRUE, но не рекомендуется использовать этот факт для будущей совместимости. Переменные типа long нельзя использовать в сравнениях, умножениях и делениях, поэтому нельзя говорить об их знаковости. В выражениях <expression> имеются следующие приоритеты операций: 1. Скобки и префиксы (+, -, ~ (инверсия), ! (логическое отрицание), & (взятие адреса переменной), * (чтение по указателю), а также нестандартные < (сдвиг влево на 1 бит), > (сдвиг вправо на 1 бит), [<constexpr>] (константное выражение)). 2. Умножение, деление, &, &&. 3. Сложение, вычитание, |, ||, ^, ^^. 4. Сравнения и сдвиги ( <<, >> ). Сравнения и сдвиги работают только для типов byte, char, int, uint. Для bool разрешены только сравнения на равенство и неравенство. В выражениях допустимы следующие виды значений: * идентификатор переменной - получает тип по типу переменной. * целая числовая константа без знака - получает тип byte (если запись в стиле Oxff с не более чем 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>&Oxffff . Для приведения char, int, uint или long к типу byte используется выражение <expression>&Oxff . Для приведения 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 Oxff #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
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября