Как стать автором
Обновить

Комментарии 21

Здравствуйте спасибо за статью.
Позвольте пару уточнений:
1. А что представляет из себя сам язык (или это "general C++ подобное (без мультинаследования надеюсь) в образовательных целях), а то как-то обсуждать технические моменты без а) общей идеи б) технической реализации -- не очень удобно.

2. Вы в прошлой статье обмолвились, что "компилятор делится на 2 части front-end и back-end". Мне кажется, что сегодня уже лучше говорить как в LLVM: front-end (всё до преобразования в IR), middle-end (аппаратно-независимые оптимизации над IR), back-end (аппаратно-зависимые оптимизации над IR и планирование+кодогенерация).



Спасибо.

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

  • Язык имеет синтаксис чем-то схожий с Typescript, Rust (т. е. С++ подобный синтаксис, с упрощенным видом определений переменных и функций, для исключения неоднозначностей языка);

  • Есть несколько базовых типов void, int, float, char и string (так же есть bool, но пользователь не может объявлять переменные данного типа). Все переменные должны объявлены с указанием их типа;

  • На основе базовых типов пользователь может создавать массивы, указатели, функции, структуры и классы, функции поддерживают перегрузку по количеству и типу принимаемых параметров;

  • Язык поддерживает одиночное наследование и в языке есть виртуальные функции, в случае необходимости компилятор генерирует конструкторы и деструкторы, а так же их вызовы при создании и уничтожении объектов;

  • Язык поддерживает new, del для работы с памятью;

  • В языке есть if, while, for, break, continue и return;

  • Поддерживает все арифметические операции над типами, включая арифметику над указателями.

Hidden text
fn printLn(s1: string, s2: string) {
  print(s1);
  print(s2);
  printLn("");
}

fn printLn(s: string, i: int) {
  print(s);
  print(i);
  printLn("");
}

class Shape {
  virt draw() {
  }
}

class Square extends Shape {
  side: int = 0;

  new(s: int) {
    side = s;
  }

  impl draw() {
    printLn("Square.draw side: ", side);
  }
}

class Circle extends Shape {
  radius: int = 0;

  new(r: int) {
    radius = r;
  }

  impl draw() {
    printLn("Circle.draw radius: ", radius);
  }
}

fn main(): float {
  for let i: int = 0; i < 10; ++i {
    let p: Shape* = 0;
    
    if i % 2 == 0 {
      p = new Square((i + 1) * 10);
    } else {
      p = new Circle((i + 1) * 5);
    }

    p.draw();
    del p;
  }

  return 0.0;
}

В первых 3-х частях серии рассматривается только подмножество языка в котором есть только базовые типы int, float, void и bool и функции (без их перегрузки), все остальное будет добавлено в последующих статьях.

  1. Тут я полностью согласен с вами.

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

ПС
Я правильно понял, что язык в целом учебный и служит целью "чтобы было вокруг чего писать компилятор" и какие-то каверзные вопросы (почему проблему if {} if {} else {} не поправили) вам задавать не стоит?

  1. Язык изначально задумывался для серии статей, но т. к. в рамках серии статей достаточно сложно рассмотреть что-то по сложности сопоставимое с C++, Rust, Swift и другими языками, то я решил сделать что-то более простое, что можно было написать за 2 недели работы. Но этой базы уже достаточно для того, что бы реализовать что-то более сложное путем добавления нужного функционала. (Финальная версия уже принимает тот код, который я привел в предыдущем комментарии. Пока я решил не публиковать полную версию на github, т. к. в процессе публикации первых статей, были найдены некоторые моменты, которые не были обнаружены при написании изначальной версии 11 лет назад);

  2. Тут я не совсем понял что именно за проблема имеется в виду, т. к. если это dangling if, то в текущим синтаксисе она в принципе не может возникнуть, т. к. {} обязательны после условия (в изначальной версии проблема решалась, по аналогии со многими другими компиляторами — путем привязки else к самому вложенному if).

  1. Смысл всего пункта - вопросы по языку принимаются, или это not the point. Вот у вас тип возвращаемого значения можно не указывать, если он void, почему так? Вот у вас let, а не let mut (т.е. по-умолчанияю данные мутабельны), почему так? Вот у вас return следовательно {} не является expression (следовательно switch expression надо будет приделывать к языку отдельным оператором), почему так? Вот вы собираетесь делать вывод типов (вводить auto по-простому), уже думали насколько мощным он может быть в такой системе типизации? ...

    2.1 Изначальная проблема: dangling if (не знал термина) на пустом месте усложняет язык для программиста (т.е. плохой неинтуитивный дизайн). Т.е. программисту надо не только помнить 100500 объективно сложных моментов, а ещё и вдобавок 100500 неоднозначностей, которых при лучшем дизайне не было бы.

    2.2 Извините был невнимателен, действительно примеры нельзя интерпретировать никак иначе, кроме обязательности скобок.

Теперь я вас понял. Как я уже указал в предыдущем комментарии, язык был взят такой, что бы его можно было просто реализовать, поэтому синтаксис на тот момент был взят с C++ (т. к. на тот момент я с ним больше всего работал), за исключением того, что переменные объявлялись в виде:

var int p;

это было сделано для того, что бы исключить неоднозначностей вида:

a * b;

Что это объявление переменной или выражение?

Аналогично и для функций:

def name(int a, int b): int;

При подготовке к публикации первой статьи я внес некоторые правки и заменил var, на let, а тип нужно теперь указывать после имени переменной (что больше походит на синтаксис Typescript, Rust и F#). Аналогично были убраны обязательные скобки после if, for, while и добавлены скобки {} после этих конструкций для их тел.

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

Для реализации switch нужно решить много различных вопросов, разрешать ли метки в виде диапазонов, нужно ли разрешать конструкции на подобии (https://en.wikipedia.org/wiki/Duff%27s_device) или нет? Поэтому было принято решение взять только базовые конструкции if, for, while.

Вывод типов не ограничивается только выводом типов для переменных, но и выводом параметров функций, что конечно лучше делать в разрезе создания аналога generic.

А так да, в дизайне языков достаточно много различных нюансов (не даром их количество превышает количество пальцев на руках), которые не подразумевались решаться в рамках данной серии, т. к. цель была именно в предоставлении инструмента для создания своего языка программирования, под свой синтаксис и свои правила. Т.к. все статьи, которые я видел в основном заканчивались созданием простого языка вида Kaleidoscope (Если есть что-то более серьезное, то было бы интересно посмотреть).

Да спасибо понял.

Не буду писать дальше, чтобы не быть похожим на ворчливого деда ))
Разумеется написать компилятор к боле-менее взрослому языку крайне объёмная задача и лучше работающий прототип, чем идеальная модель без единой строчки кода.

Удачи вам.

Спасибо, за интересные вопросы. Я думаю тем, кто будет создавать свой собственный полноценный язык программирования, наша дискуссия пригодится).

На всякий случай скину пару ссылок для тех, кому это будет интересно:

  1. Concepts of Programming Languages

  2. Programming Language Concepts

  3. Programming Language Design and Implementation

  4. Programming Language Pragmatics

А, к стати если уж речь зашла об этом - вы какие из этих книг читали и можете посоветовать?
Судя по отзывам - у них у всех есть один большой недостаток - их писали люди из университетов, а не индустрии. И поэтому они сосредоточены на том, как делать "правильно в сферическом вакууме", а не как делать так, чтобы потом на этом было удобно программировать.

А вот почитать захотелось на эту тему что-то:
1. боле-менее фундаментальное
2. боле-менее с практической стороны
а нету. Лекция что я привёл, соответствует только критерию (2)

Это было давно, но я читал Programming Language Pragmatics и не всю, а интересующие меня части (например пропустил все, что дублировало информацию из книги Compilers: Principles, Techniques, and Tools). Но при желании все эти книги можно найти и пролистать, перед прочтением. Для себя я их отложил.

Работаю сейчас в этой теме. Фронтенд в целом не интересует, подожду статьи про оптимизацию и тбаа

Данная серия направленна на то, что бы дать представление о том, как написать свой язык высокого уровня и как различные высокоуровневые конструкции могут быть реализованы при использовании LLVM IR. Поэтому интересующие вас вопросы, в них затрагиваться не будут.

А просвятите пожалуйста (если серьёзно работаете):
1. TBAA же фронтэнд (делается на call-graph, то есть до IR), разве нет, т.е. как понимать вашу фразу "форнтэнд не интересует, интересует TBAA"?

2. А разве с C\C++ подобных языках чего-то ждут от TBAA?
Насколько я понял, что консенсус:
- не только алгоритмически неразрешимо, но и каких-то прорывных подходов ждать не стоит => если за N^1.5 (условно) не удалось доказать эквивалентность всё считаем разным.
- ну и strict aliasing для тех, кто ССЗБ.

Да я в принципе не сильно понимаю, как работает тбаа, и есть ли от него серьезный профит. Если это условные +10% к производительности, то заниматься не будем

Языков у нас много и работа в целом заключается в маппинге проприетарного IR на LLVM IR. До фронтенда там добраться довольно сложно. Если интересно, есть старая презентация https://llvm.org/devmtg/2017-10/slides/Reagan-Porting OpenVMS Using LLVM.pdf

Я не совсем понял что значит "проприетарный IR" (просто тут может быть условно и JIT-объектник (который почти не отличается от исходного кода), а может быть уже почти готовый бинарнит (этот вариант намного хуже). Если "проприетарный IR" похож на бинарник (например появились архитектурно-специфичные команды, есть конкретный флаговый регистр, виртуальные регистры распределены....) то задача похожа на бинарную трансляцию.

Если это именно только-только сгенерированный IR - то можете дальше не читать.

====================
Если задача похожа на бинарную трансляцию, то:

  1. Дооптимизировать код, лежащий в бинарнике "generally" идея так себе. Сделать бы так, чтобы работало (что не отменяет того факта, что если у вас изначально очень слабый компилятор - можно иногда что-то и улучшить).

  2. Делать TBAA на IR это боль. Слишком много паразитной семантики в бинарник уже внесли (люди по незнанию пишут "слишком мало семантики в бинарнике" - это обратно реальному положению дел)

  3. Один из известных подходов - делать отдельную фазу компиляции RTMD (run time memory delimitation), которая в run-time проверяет пересекаются ли два "массива" (указатель и отдельный атрибут длины) - если удалось доказать, что не пересекаются, выполняют оптимизированный код, если нет (проверка консервативна) - код предусматривающий возможность пересечения.

Как под это дело приспособить LLVM и можно ли (как бы он ещё свою паразитную семантику не внёс, свзязанную с ограничениями LLVM-IR, надо проверять), я не знаю.

Спасибо за этот цикл статей, с интересом жду продолжения.
С меня плюсик в статью и карму:)

Это ошибка. Спасибо, исправил.

Хотелось бы увидеть грамматику языка в нотации YACC или ANTLR.
По трем Книгам дракона зеленой, красной и фиолетовой написал лабораторки по трансляторам со всякими LL1, LL(k), LR0, LR1, LR(k), SLR(k), LALR, LALR(k), YAAC, Core, Closure. Хорошо-бы взять грамматику и попроверять, сгенерироать таблицы разбора и погонять синтаксические тесты.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории