OS Inferno: Programming Limbo

Версия: 1.1.1.

Оглавление

Программы или библиотеки

В Inferno нет отличия между программой (application, script) и библиотекой (.so - shared object, .dll).

Все программы на Limbo являются модулями, и могут быть использованы в этом качестве любой другой программой/модулем. Т.е. одна "программа" может в любой момент подгрузить другую "программу" в память как обычную библиотеку и начать вызывать её функции. В связи с этим дальше в тексте вместо термина "программа" будет использоваться термин "модуль", а термин "программа" будет означать модули, которые умеет подгружать и выполнять модуль shell.

Да, в Inferno когда в shell набирается имя программы для выполнения, фактически shell просто подгружает модуль с этим именем и передаёт ему управление.

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

Файлы модуля

При программировании на Limbo обычно реализация модуля помещается в файл с расширением .b, а описание интерфейса модуля в файл с расширением .m. Это даёт возможность другим программам подгружать описание этого интерфейса через include .m файла (а-ля как подгружаются .h файлы в C).

Но, в принципе, можно .m файл не создавать, и описать интерфейс внутри .b файла - это обычно используется модулями имеющими интерфейс "Command", предназначенный для запуска модулей из shell.

Фактически никто не мешает описывать один и тот же интерфейс ручками в каждом .b файле вместо того, чтобы вынести его в .m файл и include-ом подгрузить во все нужные .b файлы.

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

Структура файлов модуля

В Limbo весь выполняемый код должен находиться внутри функций. Вне функций могут находиться:

Таким образом в .b файле должна быть строка implement и реализации функций. А всё остальное может быть либо в .b либо в подгружаемом .m файле.

Пример модуля

Это пример модуля реализующего интерфейс Command, т.е. модуля который shell может запускать из командной строки. Он может использоваться как шаблон для экспериментов с описываемыми здесь возможностями Limbo. implement Command; include "sys.m"; include "draw.m"; sys: Sys; Command: module { init: fn(nil: ref Draw->Context, argv: list of string); }; init(nil: ref Draw->Context, argv: list of string) { sys = load Sys Sys->PATH; i := 5; r := 5.0; s := "five"; sys->print("int=%d real=%f string=%s\n", i, r, s); }

Если этот модуль был сохранён в файл /usr/inferno/example.b (от корня файловой системы Inferno), то запустить его можно так: $ emu -r/path/to/inferno_root/ ; cd /usr/inferno ; limbo example.b # создаёт example.dis ; example # запускает example.dis int=5 real=5.000000 string=five ;

Для упрощения выполнения команд откомпилировать/запустить можно создать функцию shell (эти команды нужно будет повторять после каждого перезапуска emu, либо нужно их прописать в /lib/sh/profile): ; load std ; fn x { limbo example.b; example } ; x int=5 real=5.000000 string=five ;

Unicode

Limbo использует UTF8 для I/O, и UTF16 для представления строк в памяти.

Т.е., например при считывании исходника модуля с диска в нём может использоваться UTF8 в комментариях, строках и символьных константах.

Если есть массив байт (array of byte) и он конвертируется в строку, то байты из массива обрабатываются как UTF8 и конвертируются в строке в UTF16; а при преобразовании строки в массив байт происходит обратное преобразование и в массиве оказывается UTF8.

Для конвертирования массивов байт в/из другие кодировки см. convcs(2).

Лексика

Комментарии только однострочные, от # и до конца строки.

Идентификаторы должны начинаться на A-Za-z_ и могут содержать A-Za-z0-9_ плюс любые юникодные символы (с кодами >0xA0). Макс. размер идентификатора 256 байт.

Выражения (expression) должны заканчиваться ; (может быть пустое выражение: одинокая ; без выражения - например для "пустого" тела цикла).

Везде где допустимо одиночное выражение можно использовать несколько выражений в блоке: { ... }, напрмер: # одно выражение if (i == 1) i++; # блок вместо одного выражения if (i == 1) { i++; j++; } # просто отдельный блок { k := 1; }

Цифровые константы можно задавать в любой не-десятичной системе счисления в формате BASE[rR]NUMBER, напр. 16r89ab. В качестве "цифр" старше 9 нужно использовать буквы a-z. Система счисления может быть от 2 до 36.
Их тип: int, если <= 2**31-1, иначе big.

Дробные константы задаются десятичными числами с точкой и/или экспонентой (e или E).
Их тип: real.

Строки записываются в двойных кавычках. (Никакой интерполяции переменных в строках а-ля bash/perl не поддерживается.) В строках можно использовать: \\ # \ \' # ' \" # " \a # bell \b # backspace \t # horiz tab \n # LF \v # vert tab \f # form feed \r # CR \u89af # unicode \0 # NUL Строки не могут быть многострочными (т.е. хотя они могут включать \n, но закрывающая кавычка должна в исходнике модуля находиться в той же строке, что и открывающая кавычка).
Их тип: string.

Одиночные символы можно записывать в одинарных кавычках. В них можно использовать те же \-комбинации, что и в строках.
Их тип: int (юникодный код этого символа).

nil это пустая ссылка.

Типы данных

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

Тип Значение / ссылка Размер
byte value 8 bit unsigned
int value 32 bit signed
big value 64 bit signed
real value 64 bit (IEEE long float)
fixed value 32 bit signed
string value  
( список типов, т.н. tuple ) value  
array of любой_тип ref  
list of любой_тип ref  
chan of любой_тип ref  
adt-тип value  
ref adt-тип ref  
module ref  
fn  
ref fn ref  

Например: array of chan of (int, list of string) это массив хранящий каналы, по которым передаются tuple состоящие из int и списка строк.

Типы помеченные value передаются по значению, т.е. если, напр., передать переменные этих типов параметрами в функцию, то функция изменяя свои параметры не сможет изменить переменные в вызывающем коде; а если их присвоить в другую переменную то значение будет скопировано. А при присвоении переменных типов помеченных ref фактически копируется только ссылка, а не данные, и обе полученные переменные начинают указывать на одни и те же данные.

Объявление/инициализация переменных

Переменные можно отдельно объявлять и отдельно инициализировать, а можно делать это одновременно: i, j : int; # объявление i = 5; # инициализация i : int = 5; # одновременно объявление и инициализация i := 5; # аналогично предыдущему, тип определяется k := i; # автоматически из присваеваемого значения (a, b) := (5, "qwe"); # типы берутся из типов элементов tuple (c, d, e) := my_adt; # типы и значения берутся из полей этой adt # (в порядке объявления полей в adt)

Переменные можно объявлять/инициализировать в любом месте программы. Область видимости у них лексическая, внутри блока (файл, функция, блоки в if/for/..., etc.). Когда переменная выходит из области видимости память выделенная под неё автоматически освобождается (обычные данные - немедленно, структуры с циклическими ссылками освобождаются спустя некоторое время).

На верхнем уровне (вне функций) переменные можно инициализировать только константными значениями (или array с константным размером и константным содержимым). list и ref adt на верхнем уровне инициализировать нельзя.

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

В Limbo вообще очень ограниченная работа со ссылками/указателями. Напрямую изменять значение указателя как в C нельзя, любой указатель должен либо быть nil, либо указывать на реально существующую структуру.

Числовые типы: byte, int, big, real, fixed

b : byte; i, j: int; l : big; r : real; b = byte 5; # необходимое преобразование типов i = 5; j = 5; r = 5.0; b1 := byte 5; # без преобразования был бы int i1 := 5; i2 := 'X'; # один символ это его код в int i3 := mystring[5]; # срез строки это один символ i4 := int 1.0 + int 2.0; i5 := int(1.0 + 2.0); i6 := 2147483647; # 2**31-1 (max int) l1 := 2147483648; # 2**31 (big) l2 := big 5; # без преобразования был бы int r1 := 5.0; r2 := 1e-5; b = byte i; b = byte l; b = byte r; i = int b; i = int l; i = int r; l = big b; l = big i; l = big r; r = real b; r = real i; r = real l; s : string; s = string b; # преобразования между s = string i; # текстовым и числовым s = string l; # представлением чисел s = string r; b = byte s; i = int s; l = big s; r = real s; b++; ++b; b--; --b; i++; ++i; i--; --i; l++; ++l; l--; --l; r++; ++r; r--; --r; b = b + b; b = b - b; b += b; b -= b; i = i + i; i = i - i; i += i; i -= i; l = l + l; l = l - l; l += l; l -= l; r = r + r; r = r - r; r += r; r -= r; i = !i; # ! для 0 равен 1, для остальных значений 0 b = ~b; i = ~i; b = b * b; b = b / b; b = b % b; i = i * i; i = i / i; i = i % i; l = l * l; l = l / l; l = l % l; r = r * r; r = r / r; b *= b; b /= b; b %= b; i *= i; i /= i; i %= i; l *= l; l /= l; l %= l; r *= r; r /= r; b = b ** i; i = i ** i; l = l ** i; b = b << i; b = b >> i; i = i << i; i = i >> i; l = l << i; l = l >> i; b <<= i; b >>= i; i <<= i; i >>= i; l <<= i; l >>= i; b = b & b; b = b | b; b = b ^ b; i = i & i; i = i | i; i = i ^ i; l = l & l; l = l | l; l = l ^ l; b &= b; b |= b; b ^= b; i &= i; i |= i; i ^= i; l &= l; l |= l; l ^= l; i = b && b; i = b || b; i = i && i; i = i || i; i = l && l; i = l || l;

Устанавливать числовые переменные в nil нельзя.

Все числовые типы можно преобразовывать друг в друга.

При преобразовании string в числовые типы из строки берётся (пропустив возможные стартовые пробелы) максимальное кол-во подходящих для этого числового типа символов. При преобразовании числового типа в string создаётся новая строка (для real используется формат а-ля sys->sprint("%g")).

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

Операция битового сдвига << добавляет бит 0, а >> добавляет для byte бит 0, а для int и big бит знака (0 для положительных, 1 для отрицательных значений).

Логические операции && и || возвращают 0 или 1, и не вычисляют правый операнд если результат определяется сразу после вычисления левого.

Поведение при overflow/underflow не определено (вроде просто "перекручивает" и округляет значение, исключение не генерируется).

Тип fixed - см. /doc/limbo/addendum.pdf.

TODO: Заменить "простыню" со всеми возможными операциями на внятную табличку? Добавить описание fixed.

Тип: string

s : string; s = "five"; s1 := ""; s1 = nil; # то же что и "" i := s[0]; # int равный коду символа "f" s2 := s[0:2]; # string содержащая "fi" s3 := s[:2]; # string содержащая "fi" s4 := s[0:]; # копия s s5 := s[0:len s]; # копия s s[len s] = '5'; # s теперь содержит "five5" s[len s] = '!'; # s теперь содержит "five5!" a := array of byte s; # представление s байтами в UTF-8 s = string a; # представление массива байт в UTF-8 # строкой в UTF-16 b := byte s; i := int s; l := big s; r := real s; s = string b; s = string i; s = string l; s = string r; i = len s; s = s + s; # конкатенация строк # начиная с версии 20111222: s := `Строка содержащая переводы строки. Все символы as is, экранировать \\ ничего нельзя. Не может содержать символ обратной кавычки.`;

Хотя строка это не ref-тип, но её можно устанавливать в nil и сравнивать с nil - он эквивалентен пустой строке.

len возвращает длину строки в символах, а не байтах (байт строка в памяти занимает всегда в два раза больше, т.к. используется UTF-16).

Срезы/индексы

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

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

Срез строки возвращает новую строку. Второе число в срезе это индекс символа который не должен войти в возвращаемую подстроку: s := "test"; s1 := s[1:3]; # в s1 будет "es" Если второе число опущено, то вместо него используется длина строки. Если опущено первое число - используется 0.

Присвоение в элемент строки с индексом равным длине строки (т.е. на 1 большим индекса последнего символа в строке) расширяет строку на один символ.

Тип: tuple

i_s : (int, string); i_s = (5, "five"); i_r_s_s := (5, 0.5, "five", "comment"); i : int; s : string; (i, s) = i_s; (i1, s1) := i_s; (nil, nil, s_five, nil) := i_r_s_s; ((i1, s1), (i2, s2)) = (i_s, i_s); i = i_s.t0; s = i_s.t1;

tuple должен содержать 2 или более элемента любых типов. Тип самого tuple фактически определяется по тому, каких типов элементы и в каком порядке он содержит.

Оператора "запятая" отделяющего вычисляемые выражения в Limbo нет, так что круглые скобки вокруг одного элемента это простая группировка, а вокруг двух и более - tuple.

tuple можно присваивать в список отдельных переменных соответствующих типов, возможно проигнорировав некоторые элементы tuple указав в их позициях nil.

Тип: array

a: array of int; a = array[10] of int; i := 5; b := array[i] of byte; c := array[] of { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; d := array[100] of { * => byte 5 }; e := array[10] of { # получим: { "-", "-", "-", 3 => "three", # "three", "-", 5 => "five", # "five", "six", # "six", * => "-", # "-", "-", "-", } }; f := array[0] of int; # пустой array a[0] = 6; i = a[0]; a1 := a; # два указателя на один array a2 := a[:]; # то же самое a3 := a[:i]; # указатель на срез a4 := a[i:]; a5 := a[i:len a]; a6 := a[1:3]; c[7:] = a[0:3]; # копирование элементов d[50:] = b; a[:] = array[] of { 1, 2, 3, 4, 5 }; s := "five"; buf:= array of byte s; # представление s байтами в UTF-8 s = string buf; # представление массива байт в UTF-8 # строкой в UTF-16 i = len a;

array содержит фиксированное кол-во элементов одного типа. Индекс первого элемента равен 0.

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

Фактически в Limbo только два способа динамически выделить память: создать array указав требуемый размер через переменную, и добавить новый элемент в начало list.

Ну, если подумать, то ещё можно расширять строки добавляя по одному символу в конец строки.

Срезы/индексы

Для массива индекс возвращает один элемент.

Срез массива возвращает ссылку на под-массив (в отличие от строк, которые в этой ситуации возвращают копию подстроки): a := array[5] of int; b := a[2:4]; # create 'array[2] of int' a[2] = 100; # also set b[0] = 100;

Эти "указатели на под-массивы" можно использовать как замену арифметике с указателями в C - чтобы получить в отдельной переменной "указатель" на текущую позицию в массиве.

Второе число при срезе это индекс элемента который не должен войти в срез, т.е. a[2:4] это срез из двух элементов: a[2] и a[3]. Если второе число опущено, то вместо него используется размер массива. Если опущено первое число - используется 0.

В срез массива с опущенным вторым индексом можно присваивать массивы того же типа, имеющие размер меньший или равный кол-ву входящих в срез элементов: a := array[] of {1, 2, 3, 4, 5}; a[2:] = array[] of {30, 40}; # получим {1, 2, 30, 40, 5}

Тип: list

l : list of int; l = nil; l = 10 :: 20 :: 30 :: nil; l = 5 :: l; l0 := list of { "one", "two", }; l1 := "one" :: "two" :: nil; i := hd l; # int равный 5 l2 := tl l; # list равный 10 :: 20 :: 30 :: nil l2 = tl l2; # list равный 20 :: 30 :: nil i = len l;

list это последовательность элементов одного типа оптимизированная для стеко-подобных операций (добавить элемент в начало списка, получить первый элемент списка, получить остаток списка (кроме первого элемента)).

nil в середину list вставлять, в принципе, можно (если тип списка совместим со значением nil).

hd и tl должны получать параметром не пустой list.

tl от list содержащего один элемент возвращает nil.

Важный нюанс: в то время как hd mylist возвращает значение первого элемента списка, myarray[i] возвращает ссылку на указанный элемент массива. Т.е. значения элементов массива так менять можно, а списка - нет (в случае необходимости изменить список нужно создавать новый список из других элементов).

Тип: chan

c : chan of int; # объявляет тип c = chan of int; # создаёт канал d := chan of int; c <-= 10; # отправить в канал i := <-c; # принять из канала int <-c; c = nil; # уничтожить канал a := array[5] of chan of (string, byte); (chan_id, s, b) = <-a;

Каналы (chan) позволяют организовывать IPC между локальными процессами передавая атомарно объекты заданного типа.

Чтение/запись канала это блокирующая операция.

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

Буферизованные каналы
c : chan of int; c = chan[1] of int; d := chan[10] of int; e := chan[0] of int; # а-ля: e := chan of int;

Запись в буферизованные каналы не блокируется пока не будет заполнен буфер. Буфер работает как FIFO очередь.

Мультиплексирование каналов: alt

В alt каждая альтернатива должна либо быть выражением содержащим оператор <-, либо быть *. Если есть хоть одно выражение, которое может выполниться без блокирования (канал готов к I/O), то оно выполняется. Если такого выражения нет, и нет альтернативы *, то alt блокируется пока хотя бы один канал не будет готов к I/O; а если есть альтернатива *, то выполняется этот блок. alt { i := <-inchan => sys->print("received: %d\n", i); outchan <-= "message" => sys->print("message sent\n"); } Область видимости переменной i - две строки: выражение и блок за ним.

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

В выражениях alt нужно избегать побочных эффектов (side effect - изменений значений переменных, блокирующих операций, etc.) т.к. при проверке выражения на готовность канала оно выполняется: ch1 <-= getchar() => # bad idea ch2 <-= next++ => # bad idea

Если несколько процессов ожидают I/O по одному каналу через alt, то они будут получать доступ к каналу по очереди (FIFO).

Перед операторами цикла и выбора можно ставить метки, и внутри них можно использовать операторы break и continue (с опциональным указанием метки). break прерывает все шесть операторов - три цикла и три выбора, continue переходит на следующий круг во всех трёх операторах цикла.

TODO: Надо бы как-то яснее переформулировать предыдущий абзац. Не очень понятно, почему в процессе описания каналов вдруг сделан резкий прыжок в сторону операторов цикла/выбора и меток (alt - один из операторов выбора и его можно прервать с помощью break).

Тип: adt

Point: adt { x, y: int; color: string; }; p : Point; p.x = 5; (p.y, p.color) = (10, "red"); p = (1, 2, "green"); tuple := (2, 4, "blue"); p1 := Point tuple; p2 := Point(2, 4, "blue"); (x, y, color) := p2; tuple = p2;

adt это т.н. "abstract data type", нечто среднее между простой структурой и объектом. adt может содержать поля любых типов и функции.

adt можно объявлять либо на верхнем уровне (вне функций), либо внутри объявления интерфейса модуля.

adt в простом варианте это просто структура из нескольких полей, имя которой можно использовать как тип при объявлении переменных или преобразовании tuple в adt.

ref adt
A : adt { x : int; }; a : A; r : ref A; a.x = 5; r = ref A; r.x = 5; a = *r; # ref в value r = ref a; # value в ref

ref adt позволяет передавать adt по ссылке.

Расширенные возможности
List: adt { value: string; prev, next: cyclic ref List; EMPTYVALUE: con "empty"; new: fn(value: string) : ref List; get: fn(l: self List) : string; set: fn(l: self ref List, value: string); insert: fn(l: self ref List, next: ref List); delete: fn(l: self ref List); }; List.new(value: string) : ref List { l : List; l.value = value; if (l.value == "") l.value = l.EMPTYVALUE; return ref l; } List.get(l: self List) : string { return l.value; } List.set(l: self ref List, value: string) { l.value = value; if (l.value == "") l.value = l.EMPTYVALUE; } List.insert(l: self ref List, next: ref List) { next.prev = l; next.next = l.next; if (l.next != nil) l.next.prev = next; l.next = next; } List.delete(l: self ref List) { if (l.prev != nil) l.prev.next = l.next; if (l.next != nil) l.next.prev = l.prev; } start := List.new("item1"); start.insert(List.new("item2")); start.insert(List.new(nil)); start.insert(List.new("wrong")); start.next.delete(); s1 := start.value; s2 := (*start).get();

Помимо обычных типов данных в adt могут быть константы, функции (для работы с данными adt, а-ля методы объектов), могут быть циклические ссылки (на сам этот adt) - их нужно помечать идентификатором cyclic.

Если у функции в adt первый параметр помечен self (или self ref), то в этом параметре автоматически передаётся переменная (adt или ref adt), от которой была вызвана функция. self это просто "синтаксический сахар", чтобы не приходилось писать так: myadt.myfunc(myadt, otherparam);

pick adt
Constant : adt { name: string; print: fn(this: self ref Constant); pick { Str or MultiStr => s : string; Real => r : real; Coord => x : int; y : int; } }; Constant.print(this: self ref Constant) { sys->print("%s: ", this.name); pick c := this { Str => sys->print("%s", c.s); MultiStr => sys->print("[%s]", c.s); Real => sys->print("%f", c.r); Coord => sys->print("\n"); sys->print("\tx=%d y=%d\n", c.x, c.y); }; } c := ref Constant.Coord("mypoint", 10, 20); c.print(); i1 := tagof c; i2 := tagof Constant.Coord;

pick - это способ иметь разные варианты одного adt.

Блок pick может быть только один и обязан быть последним элементом adt.

adt включающие pick могут использоваться только как ref adt.

Оператор pick позволяет передать управление в один из блоков, определив нужный блок по варианту adt. Если несколько вариантов совместимы по типам полей adt или код не обращается к этим полям можно указывать сразу несколько вариантов (или "любой вариант": *) на один блок: pick c := this { Str or MultiStr => sys->print("%s", c.s); * => raise "Unable to print Real/Coord constant"; };

Оператор tagof возвращает int - номер pick-варианта. Его можно применять как к переменной (чтобы узнать номер её варианта), так и к типу adt (чтобы узнать номер конкретного варианта и иметь возможность сравнить его с номером варианта конкретной переменной).

Тип: module

MyInterface: module { var: int; PI: con 3.14; TheInt: type int; Point: adt { x, y: int; draw: fn(p: self Point); }; myinit: fn(); }; point : MyInterface->Point; pi := MyInterface->PI; i : TheInt; myinterface : MyInterface; myinterface = load MyInterface "myinterface.dis"; myinterface->myinit();

module объявляет внешний интерфейс модуля, и может включать в себя переменные, константы, типы, adt, функции. Обращаться к константам/типам-adt модуля можно либо через имя модуля (интерфейса), либо через идентификатор модуля (возвращённый load). Обращаться к переменным/функциям можно только через идентификатор модуля.

load & PATH
sys: Sys; sys = load Sys Sys->PATH; my := load MyInterface "mymodule.dis";

Все переменные в Limbo ассоциированы с каким-нить модулем. Код модуля подгружается когда он нужен кому-нить, и шарится между всеми кто его использует. Но данные модуля (глобальные переменные) у каждого пользователя модуля свои. Для выделения памяти под мою копию данных какого-нить модуля (и загрузки кода модуля если он ещё не загружен) используется команда load. Она возвращает идентификатор этого модуля, который можно использовать как для доступа к этой копии данных модуля так и для вызова его функций.

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

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

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

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

Если модуль загрузить не удалось, load возвращает nil.

import
myinterface: MyInterface; PI, myinit: import myinterface; ... myinterface = load MyInterface "myinterface.dis"; a := PI; myinit();

import позволяет импортировать имена переменные/константы/типы/adt/функции модуля чтобы не использовать идентификатор модуля для обращения к ним. При импортировании констант/типов параметром import достаточно указать интерфейс модуля, но для импортирования функций/adt содержащих функции необходимо указывать идентификатор модуля, можно ещё не инициализированный load.

Когда в модуле встречается объявление того интерфейса, которые реализует (implement) этот модуль, то выполняется автоматический import всех имён из этого интерфейса.

Тип: fn

myproc: fn(); myfunc: fn(i, k: int, s: string) : (list of string, int); myfunc: fn(nil: int);

Тип "функция" (fn) используется для объявления функций внутри adt. При указании типа fn к нему обязательно добавляется информация о типах принимаемых функцией параметров и тип возвращаемого значения.

Если имя параметра функции указано nil, значит этот параметр не используется (в тип функции он входит, при вызове функции передавать его нужно, но функция его использовать не будет). Прикольная фича. :-)

Имя функции не должно совпадать с именем adt, иначе запись i := name(1, 2); будет воспринята как преобразование tuple (1, 2) к типу adt name, а не вызов функции name(1, 2).

Параметры функции при вызове всегда копируются, так что функция не может их изменить изнутри (но может изменить значения, на которые они указывают - если это параметры типа ref).

ref fn
f: ref fn(i: int, s: string) : real; f1(i: int, s: string) : real { ... } f2(code: f) { r := code(1, "one"); } f2(f1);

ref fn позволяет передавать функции параметрами другим функциям.

Синонимы типов: type

MyByte: type byte; MyFunc: type ref fn(param1: int, param2: string) : (int, int, real); b := MyByte 5; somefunc(code: MyFunc) { (i, j, r) := code(5, "five"); }

type позволяет объявить синоним для типа, разницы между новым и исходным типом для компилятора нет.

TODO: Уточнить, в чём разница между f: ref fn ... и f: type ref fn ...

Объявление/инициализация констант

При объявлении (через двоеточие) констант у них задаётся значение, которое должно само быть константным: FIVE: con 2+3; PATH: con "/path/to/my.dis"; N0, N1, N2, N3: con iota; M1, M2, M4, M8: con (1<<iota);

После чего константу можно использовать везде, где можно использовать тип значения в которое её установили.

Компилятор встраивает значения констант в код при компиляции.

Объявлять константы содержащие последовательные числовые значения (аналог enum) можно используя в значении константы идентификатор iota, который при каждом обращении принимает значение на 1 больше: 0, 1, 2, ... Ключевое слово iota обладает особым смыслом только внутри выражения con.

Операторы

При сравнении можно использовать <, >, <=, >= с числовыми типами и строками (строки сравниваются по unicode-кодам символов), тип обоих операндов должен быть одинаковый. Проверка на равенство ==, != может использоваться для любых типов (тип обоих операндов должен быть одинаковый), строки можно сравнивать с nil (это то же что и ""). Результат сравнения имеет тип int: 1 если истина, 0 если ложь.

Условие в if, while, do ... while и for должно иметь тип int (0 - ложь, не 0 - истина).

Условный оператор: if

Условный оператор один - if, с опциональным else. Если else идёт после нескольких if-ов, и не явно к которому из них он относится, то он относится всегда к ближайшему. "elsif" делается вот так: if (i == 1) i++; else if (i == 2) i--; else i = 5;

Операторы цикла: while, do ... while, for

while (i != 0) do_something(i); do do_something(i); while (i != 0); for (i := 10; i != 0; i--) do_something_else(i);

while выполняет тело цикла пока условие истинно.

do ... while аналогичен while, но выполняется один раз перед проверкой условия.

for такой же, как в C.

Операторы выбора: case, alt, pick

В case тип вариантов должен соответствовать типу тестируемой переменной. Допустимые типы переменной - int и string. Если ни один вариант не подходит, и нет варианта *, то ни один блок не выполняется - иначе выполняется блок *. case i { 1 or 8 => sys->print("1... or 8\n"); 0 or 2 to 7 or 9 => sys->print("0... or 9\n"); sys->print(" or between 2 and 7!\n"); 10 => sys->print("ten\n"); * => sys->print("unknown value\n"); }

alt описан в разделе Мультиплексирование каналов: alt

pick описан в разделе pick adt

return

return возвращает значение из функции, тип которого должен соответствовать указанному при объявлении функции. Если функция не возвращает никакого значения, то можно вызвать return без значения либо не вызывать его вообще.

Если функция не возвращает значения, и последняя операция в ней вызов другой, тоже не возвращающей значения функции, то можно помочь компилятору оптимизировать tail recursion такой записью: # do this: return other_func(...); # instead of this: other_func(...); return;

return из функции запущенной через spawn работает как exit.

Threads

spawn my_func(i, s); my_func(i : int, s : string) { ... exit; }

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

exit завершает эту нить и освобождает любые ресурсы, которые никто кроме неё не использовал.

Исключения

raise генерирует исключение. Параметр raise может быть либо string, либо пользовательский тип объявленный вот так: MyException: exception(int, string); raise MyException(i, "one"); raise "fail: something happens"; Перехват исключений возникших внутри блока осуществляется так: { some_bad_code(); } exception e { MyException => (i, s) := e; sys->print("error (%d) %s\n", i, s); exit; "fail:*" or "warn:*" => raise; # или: raise e "*" => raise "unknown string exception"; * => sys->print("unknown user type exception\n"); } Если строковое исключение подходит под несколько вариантов (напр.: "aaa*", "a*" и "*"), то выбирается самый специфичный.

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

Если в блоке exception не нашлось подходящего варианта, то исключение передаётся дальше.

При объявлении/реализации функции генерирующей исключения пользовательского типа нужно указать их список: my_func(i: int) : real raises (MyException, OtherException) { ... }

Приоритет операций

Эта таблица не корректна и должна быть обновлена. Пока можно посмотреть приоритет операторов здесь.

ОператорОписаниеАссоциативность

.Доступ к элементу adtслева

->Доступ к элементу модуляслева

f()Вызов функциислева
a[]Доступ к элементу массива или строкислева

!Логическое НЕслева
~Битовое НЕслева
++Инкрементслева
--Декрементслева
-Унарный минусслева
+Унарный плюсслева
*Разыменование ref adtслева
hdПервый элемент списканет
tlСписок без первого элементанет
refСсылка на adtслева
loadПодгрузка модуляслева
tagofВариант pick adtнет
lenРазмернет
типПриведение типовслева

*Умножениеслева
/Делениеслева
%По модулюслева

+Сложениеслева
-Вычитаниеслева

<<Логический сдвиг влевослева
>>Логический сдвиг вправослева

<Меньшеслева
>Большеслева
<=Меньше или равнослева
>=Больше или равнослева

==Равнослева
!=Не равнослева

&Битовое Ислева

^Битовое исключающее ИЛИслева

|Битовое ИЛИслева

::Конструктор спискасправа

&&Логическое Ислева

||Логическое ИЛИслева

=Присвоениесправа
:=Объявление и присвоениеслева
+=Сложение и присвоениесправа
-=Вычитание и присвоениесправа
*=Умножение и присвоениесправа
/=Деление и присвоениесправа
%=По модулю и присвоениесправа
&=Битовое И и присвоениесправа
|=Битовое ИЛИ и присвоениесправа
^=Битовое исключающее ИЛИ и присвоениесправа
<<=Логический сдвиг влево и присвоениесправа
>>=Логический сдвиг вправо и присвоениесправа
<-Присвоение в/из каналасправа