четверг, 28 июля 2016 г.

Объявления и области видимости

В этом вопросе язык для надёжного программирования ближе всего к Оберону, но с более жёсткими условиями по недопущению перекрытия.

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

Объявление-d, сделанное на уровне раздела-s доступно по имени от начала его появления в тексте до конца s. В других разделах-so d доступно только в том случае, если оно было помечено как экспортируемое в s, и только в тех из них, где s был импортирован. Доступ к d в so осуществляется черезимпортированное имя s - ins, следующей за ним разделяющей точкой и именем d

section s {
  const(c = d + 1; // ошибка - d ещё не объявлена
       +d = 0;     // пометка объявления "+" как экспортированное
        e = d + 1)
  
  proc p() {
    const(f = d;
          e = 6) // ошибка - е уже объявлено в этой области уровнем выше
    ...
  }
}.

section so {
  import (ins = so) // переименовываем, s недоступна для использования
  const (d = ins.d;
         e = so.d;  // ошибка, раздел s был переименован для использования
         f = ins.e) // ошибка, объявление е не было экспортировано в s
  
  proc p() {
    { const (a = 3)
      ...
    }
    { const (a = 0.6)// ошибка - хотя a=0.6 объявлена в другой области
      ...            // видимости, но в пределах одной процедуры,
    }                // что запрещено
  }
}.
Имя объявления, сделанного на уровне структуры должно быть уникальным лишь в пределах самой структуры. Имя может совпадать как с предопределёнными именами, так и с именами, объявленными на уровне раздела, функций и других структур, в том числе вложенных, так как из-за строго иерархического обращения к элементам структуры не может быть никакого кофликта имён.
section a {
  const (b = 1)
  var (r struct {
           r struct {
              r, b int
           }
         }
      )
  proc p() {
    r.r.r = 0;
    r.r.b = b
  }
}

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

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

section a {
  const(+a = 10)// обращение к собственному модулю не предусмотрено, 
                // поэтому имя объявления может совпадать с именем модуля.
  proc +b() {
    const(c = a;
          d = 4)// d объявлена ниже, поэтому не входит в текущую 
    ...         // область видимости
  }
  
  proc +d() { // область видимости d = 4 закончилась внутри b()
    const(c = a + 1)// хотя с уже встречается в b(), но так как области 
                    // b() и d() не пересекаются, то объявление правильно. 
    ...
  }
}

Запрет на перекрытие имён и возможность иметь несколько конкретизаций синтаксисов создают трудности, требующие разрешения:

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

Для разрешния этого можно использовать спец-символ, обозначающий, что помеченное имя имеет пользовательское происхождение.

section @const { // @ как в C#
  const (+@section = 1)
}

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

@section m {
  @const (c = 1)
  @type  (t @int)
  @var   (v t)
  
  @func f(i t) (res @bool) {
     res = i < @max(t) / 2
  }
}

понедельник, 18 июля 2016 г.

Разделяемость (модульность)

Организующей программной единицей является раздел. В ряде языков, например, в Oberon их называют модулями [0].

section name {
 /* Это место перечисления используемых разделов 
    и упорядоченных объявлений в строгом порядке:
    констант, типов, переменных, функций,
    отдела начализации переменных */
}.
Oberon
MODULE name;
 (* Это место перечисления используемых разделов 
    и упорядоченных объявлений в строгом порядке:
    констант, типов, переменных, функций,
    отдела начализации переменных *)
END name.

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

Для обеспечения доступности объявлений раздела другим разделам, они должны быть экспортированы. Упорядоченная совокупность экспортируемых объявлений составляет интерфейс раздела. Для работы с экспортированными объявлениями требуется произвести импорт разделов, в которых они объявлены.

section m1 {
  const (+a = 0;/* экспорт обозначен «+» перед именем */
          b = a + 1)
  ...
}.

section m2 {
  import (m1) /* доступ к элементам раздела через двоеточие */
  const (a = m1:a;/* a = 0 */
         b = m1:b)/* ошибка - константа "b" не экспортирована в "m1" */
  ...
}.

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

section m1 { import (m2) }.

section m2 {
  import (m1)/* ошибка - m1 уже ссылается на m2 */
}.

Назначение разделов

Смысл использования разделов может существенно отличаться, что желательно отобразить в языке для ясности и возможности дополнительной защиты со стороны транслятора.

  • Раздел может использоваться исключительно как вспомогательный, не влияя на интерфейс импортирующего раздела. Использование такого раздела может быть заменёно другим кодом, выполняющим те же задачи.
  • Раздел, являющийся частью интерфейса импортирующего его раздела. Объявленные в нём типы могут становиться видимой частью экспортируемых объявлений импортирующего.
  • Обобщённый раздел-заготовка, чьё определение задаётся не только его непосредственным содержимым, но также и импортированными разделами-параметрами.
  • Опциональные разделы, наличие которых можно проверить в коде. Могут использоваться либо для создания более ограниченной версии раздела, либо для задействования более эффективных средств, не приводя к жёсткой зависимости от них.
  • Декоративные разделы, служащие для удобства использования. Они каким-либо образом перестраивают взаимодействие с основным функционалом, практически не меняя его основной сути, например, объединяя интерфейсы разных разделов.

Поскольку разделов, особенно с учётом сторонних библиотек, может быть много, то возникает необходимость в создании иерархий:

section a/m  { const (+c1 = 1) }.
section a/m2 { const (+c2 = 2) }.
section b/m  { const (+c3 = 3) }.

section m {
  import (
    a/m;  /* в качестве идентификатора раздела после импорта     */
    a/m2; /* используется только вторая часть имени после точки */
    bm = b/m/* переименование, чтобы избежать ошибки совпадения имён */
  )
  const (c4 = m:c1 + m2:c2 + bm:c3)
}.

Версии разделов

Система в целом может одновременно поддерживать разные версии одного и того же раздела. С её точки зрения это просто разные разделы, их типы считаются разными даже при полном совпадении объявлений. Таким образом разрешается проблема несовместимых зависимостей. Возможна и более тонкая настройка, когда учитывается не столько раздел целиком, сколько его отдельные объявления. Если объявления совпадают полностью, включая поведение, обеспечиваемое тем же самым исполняемым кодом, то их всё же можно считать совместимыми, не переводя на них несовместимость других объявлений.

В примере версии разделов указаны напрямую, но также их можно задать в проекте маршрутизации разделов[1], продолжая оперировать только именами в коде самих разделов.

section lib.0.1 { ... }
section lib.0.2 { ... }

section a { import (lib.0.1) }
section b { import (lib.0.2) }

section u { import (a; b; lib1 = lib.0.1; lib2 = lib.0.2) }

Разграничение, подразделы

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

/* path — это имя раздела, а name, clear и all — имена подразделов, 
  открывающие дополнительный функционал пользователям path, 
  которым дан доступ к этим подразделам и если 
  они их указывают при импорте */
section path +name+clear+all {

  type (+t) /* можно обозначить, не раскрывая содержимое */

section +name+clear+all:

  type (+t = { +name string })

section +clear+all:

  type (+t = { +up (*)t })

section +all:
  /* Только в этом подразделе позволено задавать путь, и без доступа 
    к нему путь к произвольным ресурсам закрыт по определению */
  proc +new(name string, indir (*)t) (path *t) { ...;
    path.name = name; path.up = indir }
  proc +str(spath string) (path *t) { ... }

} path.

section editor {
  import (
    /* path+all; */ /* после импорта должен быть доступен по имени path, 
                        но у editor нет доступа к полному разделу, 
                       и здесь была бы ошибка трансляции */

    path; /* эта часть позволяет получать доступ к ресурсам, пути к
            которым явно переданы параметрами, но не позволяет 
            самостоятельно указывать, с какими ресурсами можно работать, 
            как и не даёт доступа к самому имени */
    io 
  )
  proc +do(file path:t) {
    ... io.open(file)
    ...
    // io.open(path.str("~/.ssh/id_rsa")) // ошибка трансляции
  }
}.

Подразделы могут вносить следующие изменения относительно предыдущих подразделов:

  • Добавление объявлений, включая элементы в ранее объявленные типы.
  • Добавление экспортированности ранее закрытым объявлениям, включая отдельные элементы.

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

Трансляция

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

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

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

$ sc build util.do

Что приведёт к созданию исполнимого файла util, полученного из одноименного раздела с точкой входа do — экспортированной процедуры без параметров.


Сноски:

[0] Почему раздел — это не модуль/unit?

Необходимо ответить на вопрос: «Если в разделе несовместимо изменилось экспортированное объявление, должна ли потеряться совместимость с разделами, использующими только неизменившиеся его объявления?»

Если ответ — «не должен», значит истинными модулями являются отдельные объявления раздела, а не сам раздел. Раздел же служит организационной цели, так как отдельные объявления слишком мелки для этого. Объявления пронизаны неявным импортом других объявлений раздела для избежания чрезмерного перечисления используемых связей, что было бы нужно в случае раздельного их оформления.

[1] Маршрутизация разделов в защитном языке.
[2] Оберон умер, да здравствует Оберон! Часть 2. Модули
[3] Михаэль Франц Динамическая кодогенерация: ключ к разработке переносимого программного обеспечения
[4] Webassembly: Design Rationale

воскресенье, 17 июля 2016 г.

Синтаксис

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

Язык не обязан иметь лишь одну терминальную часть синтаксиса, которая отвечает за условную кодировку программы - вполне возможна такая реализация средств программирования, которые позволяли бы конкретные детали синтаксиса выбирать по усмотрению разработчика, оставляя неизменной лишь ту центральную часть, которая ответственна за определения конструкций языка. Хотите си-подобный вид программ — пожалуйста. Больше по вкусу паскалевский подход — и это без проблем. Нужны ключевые слова на родном языке для обучения — всё для вас. Более того, исходный код не обязан быть представлен в виде традиционного печатного формата, а может использовать более богатое представление, наподобие того, которым оперируют текстовые процессоры, а также и не обязан вообще иметь строковое представление, а быть оформленным, к примеру, в виде диаграмм. В развитых средствах программирования возможен учёт особых потребностей людей с инвалидностью, например, слепых или страдающих ДЦП.

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

Необходимость угодить среднестатистическому кодировщику не оставляет выбора — основная лексика должна быть Би-подобный как ныне наиболее популярная. Именно в языке B Кен Томпсон заложил основы вида кодировки, которая сейчас известна благодаря С.

Обсуждение хороших свойств некоторых языков в половине случаев застревает в обсуждении чего-то подобного begin и end, поэтому можно просто дать программистам их любимые {}, и вместо ненужных споров сосредоточиться на главном. Единственное, что нужно сделать, учитывая особенности Си — это привести его синтаксис к более понятному и ошибкоустойчивому виду, наподобие того, как это получилось у создателей Go. Собственно, ради экономии энергии в первом приближении можно взять синтаксис Go за основу, но без трепетного отношения ко всем решениям его создателей, так как иная семантика связана с иным синтаксисом.

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

понедельник, 11 июля 2016 г.

Архитектура языка

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

  1. Правильность исходного кода
  2. Простота использования языка
  3. Лёгкость воплощения транслятора
  4. Эффективность выходного кода

Учитывается, что:

  1. Язык ценен не только тем, что в нём есть, но и тем, чего в нём нет.
  2. Добавить новую возможность гораздо легче, чем удалить старую.
  3. И маловероятные ошибки могут становиться большими проблемами.
  4. Возможность негарантированного обнаружения ошибки лучше гарантированного необнаружния.

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

Конструктивные особенности:

  1. Синтаксис
  2. Разделяемость
  3. Маршрутизация разделов
  4. Объявления и области видимости
  5. Состояние правильности
  6. Ошибочное состояние
  7. Целочисленные типы

Другое:

воскресенье, 10 июля 2016 г.

Свойства. Цельный

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

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

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

четверг, 7 июля 2016 г.

Свойства. Эффективный

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

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