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

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

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

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

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

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

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

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

Из заданных ограничений следует, что имена объявлений, сделанные внутри функции могут совпадать с именами объявлений, сделанных в других функциях. Также, имя внутри функции может совпадать с именем одной из функций, объявленных ниже по тексту. Это послабление нужно для возможности создания однопроходного транслятора.
module 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. Невозможность использования объявления с именем, совпадающим с ключевым словом, от модуля, написанного под другой синтаксис.
Для разрешния этого можно использовать спец-символ, обозначающий, что помеченное имя имеет пользовательское происхождение.
module @const { // @ как в C#
  const (+@module = 1)
}

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

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

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

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

Организующей программной единицей является раздел. В ряде языков их называют модулями [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 (
    d.m;  /* в качестве идентификатора раздела после импорта     */
    d.m2; /* используется только вторая часть имени после точки */
    bm = b.m/* переименование, чтобы избежать ошибки совпадения имён */
  )
  const (c4 = m.c1 + m2.c2 + bm.c3)
}.

Разграничение

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

section path+content+full {
  
  type (+t)
  
  section content+full {

    type (
     +t = {
       +name string;
       +next *t
      }
    )

    section full {
      proc +new(name string, subpath *t) (path *t) { ... }
    } full;
 
  } content+full;

} path+content+full.

section editor {
  import (
    /* path+full; */ /* после импорта должен быть доступен по имени path, 
                        но если у editor нет доступа к полному разделу, 
                        то здесь была бы ошибка трансляции */
                        
    path /* эта часть позволяет получать доступ к ресурсам, пути к
            которым явно переданы параметрами, но не позволяет 
            самостоятельно указывать, с какими ресурсами можно работать, 
            как и не даёт доступа к самому имени */
  )
  proc +do(file path:t) { ... }
}.

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

Трансляция

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

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

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

$ sc build util.do

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


Дополнительные сведения:

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

Чтобы понять почему так, нужно ответить на вопрос:

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

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

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

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

Синтаксис

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

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

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

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

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

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