Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Прочее

Разбор цепочек знаков операций

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

            Поэтому нам необходим алгоритм распознавания знаков операций, идущих во входном тексте подряд. Алгоритм должен работать на этапе лексического анализа, выдавая синтаксическому анализатору уже готовые лексемы. Как правильно разобрать цепочки, приведённые ниже?

    a ==--- b
    c +=- d
    e >>>=! f
    g !=-! h
            С одной стороны, правилами языка, которыми пользуется синтаксический анализатор, может пользоваться и анализатор лексический. Перечень правил можно, по идее, загрузить в лексер и делать разбор согласно им. Но надо ли? Есть в этом смысл? Чем меньше в лексере «интеллекта», тем он проще и быстрее.

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

            Префиксные операции не могут предшествовать инфиксным:
    операнд  -/  операнд   // унарный префиксный минус не может
                           // предшествовать бинарному инфиксному делению
            Соседство знаков операций может быть только таким (в расширенной форме Бэкуса – Наура):
    [инфиксная операция] префиксная операция {префиксная операция} 

Матрица предшествования унарных операций

Предшествующая
операция
Последующая операция
++ -- ! - ? & @
++ да нет
смысла
нет нет нет нет да
-- нет
смысла
да нет нет нет нет да
! да да нет
смысла
да нет нет да
- да да да нет
смысла
нет нет да
? да да нет нет да да нет
смысла
& да да нет нет нет нет да
@ нет нет нет нет нет
смысла
нет да

            Некоторые операции не могут соседствовать (в матрице предшествования это помечено как «нет»).
  • Цепочки «++?», «--?», «!?» и «-?» некорректны, потому что первые операции в таких сочетаниях применяются к адресу. А адресная арифметика в обсуждаемом языке урезана в целях безопасности и надёжности. Она включает в себя только две операции: получение адреса и разрешение адреса. Иные манипуляции с адресами запрещены.
  • Цепочки «?!» и «?-» некорректны, потому что вторые в цепочке операции создают временный объект. Получение адреса временного объекта некорректно, ибо объект временный.
    a = ?++ b   // получение адреса b, который ранее был увеличен на 1
    x = ?- y    // ошибка: (- y) возвращает временный объект
    
    Операция получения адреса так же не может брать адрес временного объекта после предыдущего получения адреса.
    m = ?? n    // первое получение адреса (правый «?») создаёт временный объект, 
                // второе получение адреса (левый «?») ошибочно: 
                // берётся адрес временного объекта
    
    Но в других случаях расположение рядом двух знаков операций получения адреса вполне законно, когда они определяют тип данных «адрес адреса»:
    функция (параметр: ?? _32)
    (структура
    	поле: ?? _31)
    
    В этом случае можно позволить лексическому анализатору считать данную последовательность символов «??» законной. А вывод о правильности или неправильности перекладываем с этапа лексического анализа на этап синтаксического анализа.
  • Сказанное в предыдущих пунктах во многом верно и для ссылок. В дополнение к этому, цепочки «&?» и «??» неправильны, ибо ссылка не может быть адресом адреса. Цепочку «&@» можно считать преобразованием указателя в ссылку.
  • Цепочки «@++», «@--», «@~» и «@-» некорректны, ибо операция разрешения адреса подразумевает, что она применяется к адресу. Но перед операцией «@» должны выполниться операции «++», «--», «!» или «-», которые тоже применяются к адресу, а операции над адресами в языке запрещены.
  • Цепочки «++!», «--!», «++-» и «---» некорректны, ибо правые операции в цепочке создают временные объекты, а операции инкремента и декремента не могут применяться к временным объектам. Ко всему прочему, цепочка «---» будет истолкована, как «-» и «--», а не «--» и «-».
            Так же есть операции, соседство которых бессмысленно:
  • Бессмысленно применение к операнду сначала инкремента, а затем декремента и наоборот.
    a = ++-- b
    
    и
    a = --++ b
    
    равносильны
    a = b
    
  • Нет смысла в двукратном инвертировании.
    a = !! b
    
    равносильно
    a = b
    
  • Нет смысла в двукратной смене знака.
    a = -- b
    
    К тому же эта операция будут истолкована как декремент.
  • Нет смысла в получении адреса, а затем в его разрешении и наоборот:
    // b – какой-то адрес
    a = ?@ b
    и
    a = @? b
    
    равносильны
    a = b
    
            Алгоритм распознавания цепочек знаков операций лексический анализатор должен быть таков:
  • выделяется последовательность символов, не содержащая ничего, кроме знаков операций;
  • делается разбор цепочки символов с конца, сравниваются очередные символы цепочки с элементами перечня унарных операций (от операций с самыми длинными обозначениями к операциям с односимвольным обозначением);
  • проверяется правильность цепочки на основании матрицы предшествования;
  • оставшаяся левая последовательность, от которой нельзя «отщипнуть» какую-нибудь префиксную операцию, является инфиксной операций (возможно неизвестной, т.е. определённой программистом-пользователем, либо ошибочной).
            Возможны ли при такой стратегии разбора коллизии, когда неясно, является ли последовательность повторяющихся знаков операций двумя операциями или одной операцией? Ведь одни и те же символы могут использоваться как в префиксных, так и в инфиксных операциях. Например:
  • «---» – это операции минус и инкремент или инкремента и минус? Ответ: первый вариант, поскольку от конца цепочки в первую очередь «отщипываются» операции с длинными обозначениями.
  • «--» – это инкремент или два минуса? Ответ: инкремент, как самое длинное обозначение.
  • «>>>» – это операция побитового вращения или две операции: «больше» и «сдвиг вправо»? Ответ: поскольку среди префиксных унарных операций нет операций «>>>», «>>» и «>», остаётся единственно возможная инфиксная операция «>>>».
            Предложенный выше разбор цепочек знаков операций накладывает некоторые ограничения на язык. Если мы хотим в языке иметь определяемые программистом новые операции (подобно Algol-68, в C++ такого нет), то обозначения таких операций не могут заканчиваться символами, входящими в перечень унарных операций. Однако определение новых операций – возможность небесспорная. Ведь даже переопределение существующих операций подвергается справедливой критике.

Последняя правка: 2017-05-15    11:26

ОценитеОценки посетителей
   ██████████████ 1 (33.3%)
   ██████████████ 1 (33.3%)
   ▌ 0
   ██████████████ 1 (33.3%)

Отзывы

     2016/07/14 22:09, rst256

Нет смысла в двукратном инвертировании.

Да, однозначно запретить, а то были случаи, когда в с++ такое применяли для приведения к логическому типу :-)

Если мы хотим в языке иметь определяемые программистом новые операции (подобно Algol-68, в C++ такого нет), то обозначения таких операций не могут заканчиваться символами, входящими в перечень унарных операций.

Как в случае с операцией "-", да :-)? С технической стороны тут все в порядке, однако это будет исключением из правил. Это не хорошо, придется с этим разобраться. Раз возникло исключение, значит либо эта операция не может быть однозначно распознана лексером, либо условие задано не верно. Вы доказали, что она может быть распознана, значит не совсем верно условие. Т.к. оно работает для других операций, то возможно, что данная операция чем-то от них отличается?

Нет смысла в двукратной смене знака.

Нужно добавить возможность задавать данный атрибут и для новых операций, ведь для некоторых операций это будет уже не просто бессмысленно, а приведёт к ошибке, как например для операции получения адреса.

     2017/05/05 15:13, utkin

Чем меньше в лексере «интеллекта», тем он проще и быстрее.

Это обобщение. Интеллект как раз вводят для скорости. Например, рассмотрение частных случаев наоборот может значительно ускорить работу лексера по сравнению с простым перебором в лоб.

Перечень правил можно, по идее, загрузить в лексер и делать разбор согласно им. Но надо ли? Есть в этом смысл?

А обязательно «загружать»? Если Лексер и Синтаксический анализатор – в рамках одной программы, то что мешает обращаться к правилам синтаксического анализатора? Про смысл – это зависит от Вас.

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

Иногда нужно попробовать другую точку зрения. Представьте, что у Вас нет унарных операций. Ну например –а это 0-а. Вот такая краткая запись. Тогда ---а это 0-(а-1) или как Вам больше нравится. Вы можете это делать, например как в OCaml символически, или уже когда анализируете лексемы. Первый вариант вроде как проще, второй классический и «мощней» (в смысле можно больше наоптимизировать).

Бессмысленно применение к операнду сначала инкремента, а затем декремента и наоборот.
a = ++-- b

Символическая замена тут по OCaml прям вот просится.

     2017/05/15 11:10, Автор сайта

Большое количество операций сравнения косвенно свидетельствует о сложной логике, оно же замедляет работу процессора – из-за большого количества ветвлений. В общем случае можно говорить о простоте, как о спутнице скорости.

Если Лексер и Синтаксический анализатор – в рамках одной программы, то что мешает обращаться к правилам синтаксического анализатора?

Правила хорошего тона. Если лексический анализ и синтаксический разделены (существуют в виде разных функций одной программы), то логично разделить их данные.

     2017/05/16 14:52, utkin

Можно дать возможность "попросить" один объект представить сведения другому, инкапсулировав их в классе. Все красиво и никто никому не мешает.

     2017/09/16 02:42, Comdiv

На мой взгляд это ужасно. Стоит ли рассуждать о довольно сложной логике распознавания лексем, если ещё нет толкового описания самого языка? Или я просто его не нашёл? Если же грамотно выбрать необходимый минимум лексем, то будет достаточно простого механизма распознавания без "таблицы осмысленности".

     2017/09/16 22:50, Автор сайта

Вообще-то считается, что лексический анализ проще синтаксического. Лексемы чаще всего распознаются регулярными грамматиками (типа 3 – по иерархии Хомского) – самыми простыми из формальных грамматик. Так что сложность распознавания лексем, на мой взгляд, Вами преувеличивается. Описания пока что и вправду нет. Но есть стремление следовать алголоподобной традиции языков, в которой находится место и приоритетам операций, и традиционной записи вида x = 2+2*2. Количество лексем конечно желательно уменьшать, но до определённого предела. В алголоподобных языках есть два вида минуса: унарный и бинарный. От которого надо избавиться в угоду простоте? Да они оба нужны, таковы сложившиеся традиции.

     2017/09/18 11:50, Comdiv

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

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

     2017/09/18 22:30, Автор сайта

Процитирую Евгения Александровича Зуева, «Редкая профессия»:

в синтаксисе есть неоднозначности. Это надо оценить: в Стандарте (!) языка программирования прямо написано, что некоторые конструкции можно трактовать двояко - либо как объявление, либо как оператор! В несколько упрощенном виде формулировка из стандарта выглядит так: "выражение, содержащее в качестве своего самого левого подвыражения явное преобразование типа, которое записано в функциональном стиле, может было неотличимо от объявления, в котором первый декларатор начнается с левой круглой скобки". Классический пример: что такое T(a); если T - некоторый тип? С одной стороны, это как бы объявление переменной с именем a, тип которой задан как T. С другой - конструкцию можно трактовать как преобразование типа уже объявленной где-то ранее переменной a к типу T. Все дело в том, что в Си++ статус операторов и объявлений полностью уравнен; последние даже и называются declaration-statements - операторы-объявления, то есть традиционные операторы и объявления могут записываться вперемежку. Все же радости с круглыми скобками перекочевали в Си++ прямо из Си, в котором типы конструируются подобно выражениям, и тривиальное объявление можно задать либо как "int a;", либо как "int(a);". Все это понятно, но от этого не легче. И такой язык любят миллионы программистов?! Мир сошел с ума. Яду мне, яду!..

Смысл правил разрешения неоднозначностей сводится, по существу, к поразительной фразе, простодушно выведенной в "Зеленой книге": "если конструкция выглядит как объявление, то это и есть объявление. В противном случае это оператор". Иными словами, чтобы разрешить неоднозначность, следует рассмотреть всю конструкцию целиком; фрагмент "T(a)" для анализа недостаточен - за ним сразу может следовать либо точка с запятой, тогда выбор делается в пользу объявления, либо "что-то еще". Например, вся конструкция может выглядеть как "T(a)->m = 7;" или "T(a)++;" - это, конечно, операторы (точнее, операторы-выражения, в терминах стандарта). Ну а как понимать следующее: "T(e)[5];" или "T(c)=7;"? А это, будьте уверены, еще не самые разительные примеры - загляните в разд. 6.8 Стандарта.

Человеку хорошо, он ко всему привыкает, рано или поздно он разберется, но как заставить анализатор понимать эту чехарду? Пока он не доберется до точки с запятой, он, в общем случае, ничего не сможет сказать о конструкции. Друзья, не пишите объявления, которые невозможно отличить от операторов! Пожалейте компилятор, ему же тяжело! Кроме того, можно запросто ошибиться и самому...

Несколько дней прошли в бесплодных попытках выразить неоднозначности на входном языке YACC. Выход был похоже, только в организации просмотра вперед, причем на заранее не известное количество лексем. Алгоритм разбора, заложенный в YACC, этого делать не умеет. В принципе известны и доступны системы, в которых заявлена подобная возможность, однако мы были ограничены требованием: синтаксический анализатор писать на YACCе, более того, на его версии, сделанной в одном европейском университете... Пришлось пойти на ухищрения и "сломать" классическую схему разбора: делать предварительный анализ еще на уровне разбора лексем и, встретив левую скобку после имени типа (а еще пойди распознай, что идентификатор - имя типа, а не какой-то другой сущности!), "отменять" автоматический анализ и организовывать "ручной" перебор последующих лексем, складывая их про запас в буфер.

Спасибо, в "Зеленой книге" подсказали схему такого анализа. Не знаем, как и благодарить, сами бы ни за что не придумали…

Как видите, в упомянутом случае оказалось, что анализ контекста лексическим анализатором не есть табу.

     2017/09/20 01:55, Comdiv

При чём тут табу? Зуев решал задачу, заданную внешними обстоятельствами – стандартом С++, поэтому требовались ухищрения. Вы решаете задачу, которую сами формируете, поскольку пишите не только транслятор, но и язык. Так зачем же усложнять? Если угодно сделать сложно, то никто Вас не останавливает. Но зачем?

     2017/09/22 23:23, Автор сайта

Ну да, для Зуева синтаксис C++ – это внешнее обстоятельство. А тут своя рука – владыка, что хочу, то и ворочу. Авторы языков J и K так и поступили: унарный «минус» в этих языках – это символ нижнего подчёркивания. Ну и пусть, что не интуитивно понятно, зато компилятору легче. Не хочется идти по такому пути. К примеру, есть задумка сделать идентификаторы, состоящими из нескольких слов, между которыми есть разделители. Обычному лексеру это будет не по плечу. Но есть мысль, как сделать несложный «хак», чтобы лексер выдавал одну лексему «идентификатор» для нескольких слов.

Применение научных методов при воплощении компилятора конечно же важно, но только ими обойтись невозможно. Потому что любой развитый язык содержит в себе элементы контекстно-зависимых грамматик, а для них наука ещё не разработала эффективных универсальных методов. Поэтому научные методы соседствуют с эвристикой, применимой в конкретном случае к конкретной грамматике.

     2017/09/25 01:33, Comdiv

1. Я уже приводил пример грамматики, где "одиночный" минус - это лишь часть суммы, что воплощается элементарно.
2. Идентификаторы с пробелами - это совершенно тривиальная задача для обычного лексера. Но в зависимости от того, какими ещё возможностями будет наделён язык, это может привести к плохой ошибкоустойчивости.
3. Когда речь идёт о сложном алгоритме разбора для того чтобы облегчить работу программиста, на самом деле может оказываться медвежья услуга. Сложные правила разбора не только сложней воплощать, но и программисту трудней их воспринимать. Расчёт на интуитивное понимание текста не лучшее решение, когда речь о точности и о желании сотворить идеальный язык. Создателю синтаксиса нужно соблюсти баланс между интуитивным пониманием, простотой формальных правил и ошибкоустойчивостью.

     2017/10/01 23:33, Автор сайта

1.1. С точки зрения C++ – это разные операции:
type operator -(type const&);			// унарная
type operator -(type const&, type const&); //бинарная
1.2. Унарный минус реализуется одной операцией NEG (для целочисленных операций, в архитектуре x86), с бинарной одна операция не всегда возможна.
1.3. Я не отвергаю Ваше предложение, я его намотал на ус.
2. И как бы вы это реализовали? Чтобы лексер видел разницу между «int a» (один идентификатор) и «int» + «a» (два идентификатора)?
3. Собственно говоря, пока нет самой грамматики, рассуждения о ней носят схоластический характер. Когда она появится – разговор можно сделать более предметным.

     2017/10/02 13:23, Comdiv

1. Я уже писал, что операции разные, а знак один и тот же.
1.3. Хорошо, только учтите, что тогда будут доступны записи такого вида:
-a * b
и не будут такого:
a * -b
C моей точки зрения, это хорошо, но у других людей может быть иное мнение.

2. Грамотной грамматикой, естественно.
int a: int
int a - идентификатор
где int – тип

Мы же не хотим, чтобы это решалось на другом уровне вместе с нежелательными последствиями для понимания
int a int
Впрочем, сами идентификаторы с пробелами в языке общего назначения – тоже, на мой взгляд, затея сомнительная.

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

     2017/10/02 13:27, Comdiv

Кстати, вот регулярная грамматика для идентификаторов с пробелами для того, чтобы можно было прочувствовать её на пальцах:
%%
[a-z]+([ ][a-z]+)* puts(yytext);
. ;
%%
Сохраняете её в wf.l и собираете командой:
lex -o wf.c wf.l && cc wf.c -lfl -o wf

     2017/10/04 22:17, Автор сайта

Идентификаторы типа «СКАЗОЧНОЕБАЛИ» смотрятся неестественно. Разделители облегчат чтение и устранят двусмысленности.

Хорошо, давайте уточню постановку задачи насчет идентификаторов, состоящих из нескольких слов. 1) Слова могут быть разделены последовательностью пробелов или символов табуляции. 2) Лидирующие слова, если они являются ключевыми словами или именами встроенных типов. В этом случае не всё так просто. Если поставить задачу различать не только встроенные типы, но и пользовательские, то это уже контекстно-зависимая грамматика. Ещё сложнее, если тип не указан явно, а хранится в типовой переменной.

     2017/10/06 00:03, Comdiv

Чем Вам не нравится СКАЗОЧНОЕ_БАЛИ? Не нужно создавать проблемы на ровном месте.
wf.l можете подредактировать под свои нужды. Если же Вы вступаете на дорогу контекстно зависимой грамматики, то вряд ли у Вас получится хороший язык.

     2017/10/06 00:03, Автор сайта

Чем Вам не нравится СКАЗОЧНОЕ_БАЛИ?

Тем, что когда убраны естественные разделители, то обратная расстановка разделителей, выполняемая в мозгах, может быть неожиданной. Ведь пробел можно вставить не только между «Е» и «Б», но и «О» и «Е».

     2017/10/06 00:14, Comdiv

А знак нижнего подчёркивания мозг не замечает? Почему его недостаточно в ЯП?

     2017/10/09 21:03, Автор сайта

А знак нижнего подчёркивания мозг не замечает?

У кого как. Почему-то всё равно пишут «GetElementById». Подобными примерами кишит любая библиотека. Видимо, нижнее подчёркивание – плохой заменитель пробела, раз придерживаются стиля CamelCase

     2017/10/10 13:32, Comdiv

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

     2017/10/26 22:35, Автор сайта

А мне ситуация видится по-другому: есть и такая попытка обойтись без разделителей, и сякая, и третья, и десятая (CamelCase, lowerCamelCase, Snake case, kebab-case, Train-Case, SCREAMING_SNAKE_CASE), а счастия всё нет. А можно прекратить метания и вернуться к докомпьютерным истокам: разные слова надо разделять пробелами. Человеческий глаз привык к этому

     2018/05/28 18:22, Александр Коновалов aka Маздайщик

Существует алгоритм разбора выражений с префиксными, инфиксными и постфиксными операторами. Причём допускаются случаи, когда один знак операции может быть и одноместным и двуместным (как например «минус»). Алгоритм описан вот здесь на странице 23: http://refal.botik.ru/library/refal2014_issue-I.pdf

Алгоритм управляется только таблицей приоритетов, т.е. допускает возможность пользователям определять свои операции (которые тоже могут быть одновременно двуместными и одноместными).

Насчёт идентификаторов с пробелами. Я согласен с мнением Comdiv’а: нужно тогда описывать грамматику так, чтобы два идентификатора (включая зарезервированные идентификаторы для ключевых слов) никогда не стояли рядом:
text length : unsigned long int;
text : utf8 string;
Как вариант, можно понятие «идентификатор» перенести с уровня лексики на уровень синтаксиса. Лексической единицей будет «слово» — последовательность букв и цифр, начинающаяся с буквы, синтаксической единицей — идентификатор — последовательность слов и чисел, начинающаяся со слова.

Ключевыми словами нужно будет считать не зарезервированные идентификаторы, а зарезервированные слова — они будут просто разделителями:
if text length > max length then
text length := max length
endif

(if text length > max length
text length = max length
)
Кстати, если подумать, то в Паскаль можно добавить идентификаторы с пробелами как последовательности слов, неоднозначностей вроде быть не должно. При этом придётся запретить идентификаторам содержать внутри себя зарезервированные слова языка: if, begin, end…

     2018/05/30 17:14, Автор сайта

За ссылку спасибо, надо почитать. С идентификаторами, содержащими пробелы, можно поступить по-другому. Считывается слово. Оно ключевое? Если да, то отмечаем этот факт и идём дальше. Если нет, то значит начался идентификатор, и теперь он может содержать в себе много что, в том числе ключевые слова, которые теперь ключевыми не являются. Т.е. вводим правило, что все зарезервированные слова идут вначале. Минус: лишаемся операций, записанных словами, нельзя будет писать «здесь и там», только «здесь & там».

     2018/05/31 17:50, Comdiv

Хуже того, создаётся ещё поле для дополнительных ошибок. Если есть переменные "тут" и "там" и программист всё-таки забудется и напишет "тут и там", то транслятор вместо сообщения об ошибке может пропустить такой код.

Написать отзыв

Написать автору можно на электронную почту mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Каким должен быть язык программирования?

Анализ и критика

Описание языка

Компилятор

Надо ли использовать YACC, LEX и подобные инструменты

Выбор кодировки для компилятора

Раскрутка компилятора

Лексический анализатор

●  Разбор цепочек знаков операций

●  Как отличить унарный минус от бинарного

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Прочее

Последние комментарии

2018/10/11 22:29, Автор сайта
Формула расчета точности для умножения

2018/10/08 14:00, Неслучайный читатель
Сколько проходов должно быть у транслятора?

2018/10/06 12:19, Автор сайта
Тексто-графическое представление программы

2018/10/04 17:39, Автор сайта
Об исключенных командах или за что «списали» инструкцию INTO?

2018/09/29 16:52, Автор сайта
Как отличить унарный минус от бинарного

2018/09/22 20:13, Д.Ю.Караваев
Идеальный транслятор

2018/09/22 12:32, Автор сайта
Типы в инженерных задачах

2018/09/22 12:20, Д.Ю.Караваев
О русском языке в программировании