программирование

Недокументированные возможности bc

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

Я уже писал про недокументированную и сломанную функцию random(), которая починена в новой версии моими усилиями. Но кроме этого, есть ещё две вещи отсутствующие в документации.

Во-первых, у самой утилиты есть ключ -c, показывающий байт-код, в который переводится программа на языке bc. Во-вторых, есть альтернативный синтаксис для передачи массива по ссылке — кроме символа *, как везде, есть нестандартный символ &.

Скриншот раздела «Передача массивов по ссылке» с кодом yacc и пояснением двух синтаксисов: *array[] и &array[].

Но это я сам нашёл, руками. Стало интересно посмотреть, что могут найти языковые модели. Для исследования я выбрал редактор кода «Курсор» и модель Claude Sonnet 4.

С первого захода «Клод» нашёл много всего, но бо́льшая часть — вполне обычные возможности, присутствующие в документации. Засучил рукава, порылся в найденном. Кроме мусора там нашлось и полезное — ключ и функцию random() он нашёл, но про символ & не упомянул.

Пришлось подтолкнуть — поместить в контекст нужный файл (bc.y) и спросить нет чего-то недокументированного в нём. Со второго раза «Клод» обнаружил искомое, правда, и в этот раз добавил в список много пунктов не по делу.

С минимальным участием человека нашлись все недокументированные возможности о которых я знаю — три из трёх, правда эти жемчужины пришлось искать вручную в кучке навоза.

Какой вывод?

Я считаю, что это, в определённом смысле, успех — «Клод» помог мне вычленить искомое гораздо быстрее, чем в своё время это сделал я. Да, без моей подсказки третью возможность он не нашёл, но и задача очень и очень нетривиальная.

2 комментария

Текст в PDF

Попалась задача — преобразовать огромный (полтора миллиона строк) тестовый файл в кодировке UTF-8 в PDF. Не ожидал с ней каких-то сложностей, но оказалось, что это не так-то просто.

Проблемы две — объём текста и кодировка. Графические утилиты на таком объёме умирают, а с кодировкой UTF, как оказалось, работают далеко не все утилиты командной строки.

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

Ещё в моей задаче первый лист должен быть пустым и на каждом листе должен быть номер страницы. В результате строка запуска выглядит так:

paps --footer --footer-center={page_idx} <(printf \\f;cat listing.txt) -o listing.pdf

Небольшое примечание: при помощи printf я вставляю текстовый символ разрыва страницы (\f), чтобы первая страница оказалась пустой.

5 комментариев

Архиватор HA на «Эльбрусе»

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

Внутри стоит восьмиядерный процессор «Эльбрус-8C» 1,3ГГц. Он тогда-то был экзотикой, а после санкций стал буквально раритетом.

Я, чтобы прикинуть его производительностью, решил попробовать повторить на нём свой недавний эксперимент — скомпилировать архиватор ha и засечь время, — за сколько он на этой машине сожмёт «Дум».

Там довольно старая операционка стоит с компилятором gcc 7.3.0 и нет cmake. Пришлось переписать компиляцию на make и компилировать тем, что есть. Наверное новый компилятор соптимизировал бы код лучше, но взять его некуда — надо переставлять операционную систему, а это целая история.

Скриншот консоли программы HA 0.999β (1995, Harri Hirvola) — создание архива doom.ha с упаковкой файлов doom.exe, doom.wad и др. методами HSC/ASC; завершено за 18.77 сек.

На «Эльбрусе» «Дум» сжался за 18,77 секунды против 4,3 секунды на моём ноутбуке с M3 Max. Разница более чем в четыре раза. Я даже не знаю — много это или мало. Надо, наверное, посмотреть на каком-нибудь среднем компьютере, чтобы оценить результат.

2 комментария

Архиватор HA и современные компьютеры

Узнал сегодня о существовании архиватора lzip — им сжат исходный код свежей версии утилиты bc. Какой-то современный архиватор из мира Линукса, я не сталкивался ни разу.

Пока ехал домой, мысли крутились вокруг архиваторов и как-то незаметно докрутились до воспоминаний об архиваторе ha эпохи ДОС. Он тогда сжимал лучше всех, но делал это очень медленно. Я как-то уже вспоминал, что над «Думом» он трудился несколько часов. Это были ранние 90-е.

Я подумал — было бы интересно скомпилировать этот архиватор под мой процессор M3 Max, который сейчас считается довольно мощным, и посмотреть за какое время на нём сожмётся «Дум».

Скриншот терминала: архиватор HA 0.999β (1995, Harri Hirvola) сжимает файлы doom.ha, включая doom.exe и doom.wad, показывая прогресс упаковки и время выполнения.

Ответ — «Дум» теперь сжимается за 4,3 секунды! Надо будет повторить эксперимент лет через 10.

Кстати, результаты по коэффициенту сжатия тоже довольно достойные — сжалось чуть лучше, чем утилитой gzip и чуть хуже, чем bzip2. Но lzip, что было для меня полной неожиданностью, оставил всех далеко позади:

-rw-r--r--   1 bolk  staff   3688370 May 29 21:37 doom.tar.lz
-rw-r--r--   1 bolk  staff   5226359 May 29 21:39 doom.tar.bz2
-rw-rw-r--   1 bolk  staff   5312857 May 29 21:35 doom.ha
-rw-r--r--   1 bolk  staff   5497638 May 29 21:39 doom.tar.gz

4 комментария

Немного о сложностях написания ассемблера на m4

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

Для этого я чуть-чуть больше расскажу о m4 и ассемблере 8080A.

Ассемблер 8080А довольно простой. Будучи школьником, я его выучил по комментариям к кускам кода, которые печатались в журнале «Радио» — другой литературы в моём доступе не было. В нём всего несколько команд, большинство из них имеют один или два параметра.

Посмотрим, например, на команды ADD B и ADD C. Первая эквивалентна A += B на других языках, вторая — A += C. Буквами обозначаются регистры, для простоты можно считать их именованными переменными. Их мало, всего несколько штук и они имеют свои особенности. Например, в этих двух командах мы видим особенность регистра A — к нему можно прибавлять значения других регистров.

Команды ADD B и ADD C для исполнения компьютером переводятся в числа — в машинные коды. Обычно их записывают в шестандцатеричном виде, я тоже так буду делать. Данные команды сложения кодируются как числа 80 и 81 соответственно.

Теперь немного про m4. Я писал выше, что m4 — макропроцессор, если упрощать, язык макропроцессора позволяет указать как одни «слова» заменить на другие. «Слова» при этом должны начинаться с буквы или подчёркивания и содержать только буквы, цифры или знак подчёркивания. Это ограничение очень важное. Не будь его, я бы задал по одному правилу на каждую команду: ADD B → 80, ADD C → 81 и был таков.

К сожалению, так это не работает. Из-за этого ограничения, когда макропроцессор встречает ADD, он ещё не знает что будет дальше, поэтому не может выбрать на какой код заменить это слово, а когда дальше он видит B или C, ему надо помнить, что было до этого, так как с этими регистрами работает много команд. Другими словами, макропроцессору нужно запоминать контекст.

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

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

При этом m4 не делает различий между, так сказать, текстом программы и обрабатываемым текстом, для него это одно и то же. То есть макропроцессор делает возможным метапрограммирование — позволяет менять программе свой собственный текст.

Вот как это выглядит в коде:

dnl заменяем строку ADD на определение макроса, сама строка ADD при этом пропадёт
define(`ADD', `define(`__cmd', `__s_ADD')')dnl

dnl когда встречаем B, смотрим чему равно определение макроса __cmd
define(`B', `
ifelse(
    defn(`__cmd'), `__s_ADC', 88,
    defn(`__cmd'), `__s_ADD', 80,
    …тут остальные команды…
    `')
')dnl

dnl когда встречаем C, смотрим чему равно определение макроса __cmd
define(`C', `
ifelse(
    defn(`__cmd'), `__s_ADC', 89,
    defn(`__cmd'), `__s_ADD', 81,
    …тут остальные команды…
    `')
')dnl

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

2 комментария

Ассемблер процессора i8080A на m4

Сегодня опять сычевал почти до полуночи — вечером попался кусок кода на m4 и я, пробормотав вечное «когда-нибудь надо бы его выучить», вдруг подумал — а почему бы и не сейчас? Я и так всё время откладываю, а этот вечер ничем для этой цели не хуже, чем любой другой.

Чтобы разбираться было веселее, придумал себе задачу — написать на m4 ассемблер, желательно не очень сложный, чтобы занятие на вечерок не разрослось потом до недельного проекта. Взял инструкции интеловского процессора 8080A — это был мой первый ассемблер. Советской аналог процессора стоял на компьютере «Радио-86РК», с  которого я когда-то начинал.

Скриншот веб-эмулятора РАДИО-86РК: в консоли выводится «HELLO WORLD», в окне ассемблера виден код на языке ассемблера для процессора Z80, URL rk86.ru/console.html.
Строка «HELLO WORLD», выведенная программой, переведённой в машинный код моим ассемблером

Несколько слов о том что такое m4.

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

Парочка примеров того как выглядит его синтаксис:

dnl Макрос, заменяющий одну строку на другую
define(`WORLD', `KAZAN')dnl

dnl Макрос, оставляющий первый свой параметр как есть, а
dnl второй складывающий со значением другого макроса,
dnl если второй параметр не указан, его значение — единица
define(`__pr', `define(`__ip', eval(__ip + ifelse($2, `', 1, $2)))$1')dnl

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

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

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

Вот программа на этом ассемблере, в действии её можно посмотреть на скриншоте:

ORG(h1100)
LXI H, ADDR(TEXT)
CALL hF818
JMP hF86C

LABEL(TEXT)
BYTE(h48, h45, h4C, h4C, h4F, h20, h57, h4F, h52, h4C, h44, hD, hA, h0)

Не слишком отличается от привычного вида, но отличия всё-таки есть.

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

Во-вторых, многие вещи, не генерирующие код, выглядят чуть иначе — адрес запуска задаётся конструкцией ORG(…), метка — через LABEL(…), а её адрес получается через ADDR(…).

В-третьих, произвольные цепочки байт задаются не директивой DB, а конструкцией BYTE(…).

Результат можно увидеть у меня в репозитории.

Оставить комментарий

«Курсор»

Что-то я опять по кривой Гартнера свалился на дно разочарования в «Курсоре» — это редактор, где можно программировать с помощью нейросетей. Из семи последних задач «Курсор» запорол все семь, да ещё как эпично! Задачи, на мой взгляд, я описывал понятно — так, как описал бы живому программисту.

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

Попросил три понятных изменения сделать, любой дурак справится:

  • a@b заменить на вызов функции floor(a, b), которую я написал;
  • a$ заменить на вызов floor(a, 0);
  • там, где оператор else перенесен на другую строку от конструкции if, надо либо заэкранировать перенос через обратный слеш, либо вернуть else на одну строку с if.

Задача простая, но даже тут «Курсор» накосячил, не справившись с подсчётом скобок. В итоге, тщательно проверял все замены и считал скобки я сам. К чёрту такую автоматизацию, по сумме, я бы сам быстрее сделал.

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

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

6 комментариев

Таблица умножения

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

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

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

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

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

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

Скриншот обучающей игры: на фоне пейзажа с видом на город и море — мультяшный ёжик с вопросом «6×1 = ?» и миска с бананами, счёт 17, поле ввода внизу.

17 комментариев

Что происходит, когда main — переменная

Хочу провести работу над ошибками.

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

Напомню, речь идёт о программе, которая состоит из одного слова — main; или int main;, если забыть о стандарте Си89. Я думал дело в том, что попытка запустить программу приходит к чтению переменной и передаче управления на адрес ноль, но один читателей попенял, что в оригинальной статье указана другая причина.

Я решил глянуть получившийся ассемблер, чтобы разобраться где же правда. Для этого удобно использовать утилиту objdump:

objdump -D -d -M intel ./small

Оказалось, что я был неправ.

Чтобы это увидеть, надо посмотреть в ассемблерном листинге код, который вызывает функцию __libc_start_main() — она передаёт управление функции main(), указатель на которую указывается в регистре rdi. Как мы видим, адрес, записываемый в этот регистр указывает на секцию .data, код в которой запрещён к запуску, именно поэтому программа и падает.

Почему именно туда? Там хранятся все переменные, в том числе переменная main, то есть используется не адрес, записанный в этой переменной, как я думал раньше, а выполнение передаётся прямо на адрес, где хранится сама переменная.

Скриншот кода x86-64: секция _start инициализирует стек, указывает на _init, _fini, main и вызывает __libc_start_main@GLIBC_2.2.5.

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

Оставить комментарий

Маленькая сбойная программа на Си

Попалась мне тут статья про задачку, которую автору задали в университете — написать самую маленькую (по объёму текста) программу на Си, которая компилируется, но выполняется с ошибкой. Прежде чем читать, я попробовал написать такую самостоятельно.

Сначала я подумал о рекурсии:

main(){main();}

Для экономии байт можно использовать стандарт Си89, где всё, у чего нет при определении задания типа, имеет тип int — это позволяет убрать тип функции main.

Программа компилируется, но, как и задумывалось, падает с ошибкой:

bolk@note ~$ gcc-14 -std=c89 small.c && ./a.out
Segmentation fault: 11

Дальше я подумал, что вряд ли в статье используется такой очевидный способ и, перебрав в уме несколько вариантов, решил сосредоточится на делении на ноль. Лучший вариант, который мне пришёл в голову, вот такой, он на один байт меньше:

main(a){a/=0;}

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

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

Поэтому мы можем поделить его на ноль и программа упадёт при вызове:

bolk@note ~$ gcc-14 -std=c89 -Wno-div-by-zero small.c && ./a.out
Floating point exception: 8

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

main;

Она компилируется, хоть и с замечанием, и падает, как и требуется по условию:

bolk@note ~$ gcc-14 -std=c89 small.c && ./a.out
small.c:1:1: warning: data definition has no type or storage class
    1 | main;
      | ^~~~
Bus error: 10

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

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

Добавлено: Оказалось, что я неправильно разобрался в происходящем.

2 комментария

Митап по PHP

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

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

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

Мы начали примерно за два месяца, тем не менее, кое-что пришлось делать буквально в последнюю неделю — например, только в последнюю неделю стало понятно сколько примерно народу придёт, потому что люди записывались и записывались. От количества зависело сколько арендовать стульев для сидения и столов для кофе-брейка, а так же количество еды.

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

Большое спасибо докладчиками — Александру Макарову, Максиму Пестову и Станиславу Кошевому за то, что вызвались поучаствовать и интересные доклады.

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

Два мужчины в белых футболках смотрят на экран смартфона; один держит бутылку воды, другой — часы на запястье, на фоне зелёных растений и оранжевых труб.
Я (справа) объясняю что-то Максиму Пестову — одному из выступающих на нашем митапе
Спикер в чёрной футболке и кепке выступает перед аудиторией в светлом зале с растениями; на экране — код с заголовком «array_merge за циклом» и примерами PHP-кода.
Александр Макаров, один из лидеров разработки фреймворка yii выступает на нашем митапе

2 комментария

Первая дочкина программа

Время не сохранило мою первую программу. Из своих дневников жизни я прочитал, что она была на Бейсике для компьютера «Радио-86РК». Я едва помню сам этот факт, не говоря уже о том, что же я тогда написал. Жаль я не записал в дневник что делала та программа, сейчас мне самому интересно было бы взглянуть.

Поэтому я решил сохранить первую дочкину самостоятельную программу. Она написана на языке «Скретч». Два дня назад мы вместе попробовали написать программу по уроку на «Ютубе», а вчера дочка перед сном написала свою собственную.

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

Скриншот интерфейса Scratch с блоками кода: при нажатии пробела повторяется 3 раза смена фона и воспроизводится звук Bark, справа — спрайт собаки на фоне кирпичной стены.

2 комментария

С днём старого программиста!

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

Скриншот: выделен номер 2204 «Программное обеспечение...». Заголовок: «190. ХАРЬКОВСКИЙ ОРДЕНА ЛЕНИНА АВИАЦИОННЫЙ ИНСТИТУТ им. Н. Е. ЖУКОВСКОГО (Гособразование СССР)».
«Справочник для поступающих в высшие учебные заведения Российской Федерации в 1990 году», гос. из-во «Советская наука», 1990

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

Тем не менее название «День старого программиста» мне кажется смешным и, кажется, я больше чувствую этот праздник своим, чем тот, который в мой день рождения.

12 комментариев

Кроссплатформенность и bc

Как мы все знаем, в мире существует две версии bc — более скромная по возможностям версия GNU, которая используется в Линуксе, и более развитая, которая, например, используется в командной строке МакОСи.

Они довольно серьёзно различаются — когда я писал игру «Виселица» на этом языке, мне пришлось сделать две версии для разных синтаксисов.

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

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

Каким же образом мы можем это сделать?

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

Такую особенность я нашёл — она заключается в приоритете обработки операции отрицания, которая записывается как восклицательный знак.

Из-за этой разницы две версии по-разному вычисляют конструкцию !1+1. В продвинутой версии порядок выполнения следующий: (!1)+1, что даёт нам единицу, а в гнушной приоритет другой и конструкция выполнится так: !(1+1), что даёт нам ноль.

Основываясь на этом, мы можем написать универсальную функцию для получения случайного числа:

define rnd() {
    /* Checking the bc version */
    if (!1+1) {
        return rand()
    } else {
        return random() * random()
    }
}

P. S. Надо помнить, что в гну-версии функция получения случайного числа сломана, но есть надежда, что скоро её починят и ей можно будет пользоваться.

P. P. S. Я умножаю random() на random(), чтобы хоть немного выровнять по диапазону значений rand() и random(), правда распределение, конечно, изменится.

12 комментариев

Анонимные функции в PHP

Относительно недавно в ПХП появился новый способ создания анонимных функций. Раньше мы писали array_filter($strs, 'strlen'), теперь можем писать array_filter($strs, strlen(...)).

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

Когда этот синтаксис появился, в моей голове он стал частным случаем стрелочного синтаксиса. В самом деле, ведь fn($t) => sin($t) и sin(...) — это одно и то же. Но недавно я понял, что у второго синтаксиса есть побочный эффект. По какой-то причине раньше мне это было не очевидно.

Если посмотреть на код ниже, всё должно стать понятно, но если нет, то я поясню.

Тут мы видим класс, конструктор которого печатает код, находящийся в строке, которая его вызвала. Ниже класса создаются две анонимные функции, одна — стрелочная через fn(), вторая — через многоточие.

Этот код позволяет проиллюстрировать разницу.

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

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

То есть каждый способ имеет особенности, которые надо учитывать.

class Sample
{
    public function __construct()
    {
        $bt = debug_backtrace();
        ['line' => $line, 'file' => $filename] = end($bt);

        echo file($filename)[$line - 1];
    }

    public function method(): void
    {
    }
}

$a = fn() => (new Sample)->method(); // не выведет ничего
$b = (new Sample)->method(...); // выведется «$b = (new Sample)->method(...);»

$a(); $a(); // выведется «$a(); $a();» два раза
$b(); $b(); // не выведет ничего

5 комментариев

GNU bc и random()

Я не задумывался об этом раньше, но это здорово сбивает с толку, что в мире есть две различающиеся версии bc — одна является частью проекта GNU и поставляется в дистрибутивах Линукса, а вторая развивается другим коллективом авторов и входит, например, в состав МакОСи.

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

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

Я подумал — чем чёрт не шутит? Если предложить своё исправление и в этот проект, то, может, его тоже примут? Так как открытого репозитория кода у bc проекта GNU нет, свой патч я отправил на почту мантейнеру.

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

Оставить комментарий

Задача «Шпионы и яд»

Давно хотел проверить не забыл ли я язык «Эр» («R»). В 2017-м много на нём программировал — было интересно изучить, но с тех пор как-то и не трогал. Интересно было посмотреть что в мозге осталось, а что улетучилось за восемь лет.

А тут — оказия, у Романа Парпалака в блоге появилась небольшая заметка о задаче «Шпионы за круглым столом»:

На банкете за круглым столом сидят n шпионов. Каждый шпион независимо от остальных случайным образом выбирает одного из двух соседей — левого или правого — и подсыпает яд ему в бокал. Каково математическое ожидание числа выживших шпионов?

Он там её решает аналитически, а я подумал, что это удобный случай запрограммировать что-нибудь на «Эре». Притворимся, что не верим Роману и попробуем сравнить найденное решение и эксперимент (у меня проводится сто тысяч экспериментов на каждое число шпионов за столом).

Таблица результатов скрипта survivors.R: столбцы n, Expected, Computed, Error (%) для n от 2 до 18, погрешность до ±0.23%.

Написал сначала, разумеется, «в лоб» — цикл в цикле в цикле: для каждого числа шпионов цикл по количеству экспериментов, где каждый шпион в цикле решает кого он будет травить. Получилось медленно — «Эр» не очень быстрый язык.

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

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

Если кому-нибудь хочется посмотреть на синтаксис «Эра», код есть у меня в репозитории.

2 комментария

99 бутылок: Входной язык ПЭВМ Искра-1256

Сегодня у меня рубрике «99 бутылок» язык настолько редкий, что у него даже отдельного названия нет. Иногда его называют «русскоязычным Бейсиком Искры-1256», но это вряд ли верно — на какой-либо Бейсик он едва ли похож. Там где на нём много программировали, конечно, выдумывали какие-то локальные названия — например, «Дуб» за дубовость синтаксиса или «Кузик».

В редкой профессиональной литераторе по теме его называют «входной язык…» или «символьный язык ПЭВМ „Искра-1256“».

83. Входной язык ПЭВМ «Искра-1256» — язык программирования с операторами на русском языке, который использовался на отечественных портативных (по тем временам) ЭВМ «Искра-1256», эта машина выпускалась на заводе «Счётмаш» в Курске с 1981 по 1989 год.

Машина работала с алфавитно-цифровым дисплеем, загружала программы с кассетного магнитофона и выпускалась в нескольких модификациях — с оперативной памятью от 4 до 64 килобайт.

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

Страница руководства с описанием программ 8 и 9: вычисление НОК двух чисел и генератор псевдослучайных чисел, включая формулы, код и примеры использования.

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

Номер строки тут, кажется, используется только для удобства набора текста программы, переход же в языке делается не на номер строки, а на метку, причём их минимум два вида — метка для передачи управления, начинающаяся с буквы «М» (например — M01), и метка подпрограммы, которая начинается с «П» (П01).

Операторы записываются русскими буквами. Например ПЕРЕХ П01 — вызов подпрограммы, перед которой стоит метка П01. Оператор перехода часто используется с условным оператором, у которого нет какого-то явно обозначенного начала. Это просто сравнение с переходом, причём оператор перехода записывается впритык, без пробела: И12=0ПЕРЕХ П01.

Переменные так же записываются русскими буквами, их буквенные префиксы значимы. Со всеми префиксами разобраться не удалось, но некоторые я понял: например, буквами «И» и «А» обозначаются переменные, содержащие числа. Разница, кажется, в том, что на вторые каким-то образом отображается массивы, которые имею префикс «АИ». Помимо того есть префиксы «СА» и «СИ» — так обозначаются символьные переменные, второе, думаю, тоже массив.

Необычно выглядит присваивание — оно тут записывается как => и присваивает левое значение правому операнду.

Но самое странное — это функции.

Во-первых, тут они почему-то записываются английскими буквами. Во-вторых, могут стоять после аргумента. В-третьих, тут есть функции, которым, прямо как в Перле, можно присваивать значение: СИ01=>STR(СА00, И02, 1). В-четвёртых, и это самое, наверное, странное, их можно записывать подряд без пробела: (1.234567↑2+8.76543-0.551122)ABSINTLNEXP.

Вот какая программа получилась у меня после попытки освоить этот язык. Не факт, что она написана без ошибок, но попробовать, как вы догадываетесь, негде:

1 ПЕЧАТЬ(HEX(03),,)
2 ЦИКЛ(И01,0,98)
3 И01-99=>A01
4 ПЕРЕХ П02
5 ПЕЧАТЬ(" ON THE WALL, ",,)
6 ПЕРЕХ П02
7 ПЕЧАТЬ("!",HEX(0A0D),"TAKE ONE DOWN, PASS IT AROUND,",)
8 А01-1=>А01
9 ПЕРЕХ П02
10 ПЕЧАТЬ(" ON THE WALL!",)
11 ЦИКЛ*
12 ПЕЧАТЬ("NO MORE BOTTLES OF BEER ON THE WALL,",)
13 ПЕЧАТЬ("NO MORE BOTTLES OF BEER!",)
14 ПЕЧАТЬ("GO TO THE STORE AND BUY SOME MORE,",)
15 ПЕЧАТЬ("99 ON THE WALL!",)
16 П01
17 "S"=>СА01
18 А01=1ПЕРЕХ М01
19 ""=>СА01
20 М01
21 П02
22 A01=0ПЕРЕХ М02
23 ПЕЧАТЬ(А01(2.0),,)
24 ПЕРЕХ М03
25 М02
26 ПЕЧАТЬ("NO",,)
27 М03
28 ПЕРЕХ П01
29 ПЕЧАТЬ(" BOTTLE",СА01," OF BEER",,)

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

  • «Комплекс вычислительный „Искра 1256“. Комплект контрольных тест-программ. 1.320.118 Д15»;
  • «Процессор интерпретирующий „Искра-1256“. Библиотека программ. 3.050.187 Д14»;
  • «Задачник по аналитической химии», Н. Ф. Клещев, Е. А. Алферов и другие, М., «Химия», 1993;
  • Журнал «Вычислительная техника и её применение», №3, 1988.

Самым большим тёмным пятном остался оператор СЕЛЕКТ(). Без чёткого описания понять его функцию мне кажется невозможным. Всё, что я смог о нём узнать, заключено в цитате, найденной в одном из обсуждений: «там половину языка составлял оператор СЕЛЕКТ(n, параметры). В зависимости от этого n он чего только не делал».

Добавлено позднее: действительно, оператор СЕЛЕКТ() чего только не делал, например через него можно было производить смену режимов дисплея или указывать задержку.

3 комментария

RedBoot

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

В салоне самолёта, которым мы летели, в подголовники кресел встроена система развлечений, крайне тормозная, но неплохая. Я пытался с её помощью отвлечься от тягот полёта, пока, совершенно случайно не научился её перезагружать. Из-за низкой производительности не стал её исследовать (каждый раз она загружается примерно 15 минут), но что уловил — на старте запускается загрузчик «RedBoot», который запускает образ какого-то Линукса.

Само приложение, похоже, написано с использованием веб-технологий — под прогрессом загрузки время от времени мелькают строчки про CSS и предзагрузку картинок.

Экран развлекательной системы самолёта показывает загрузочное сообщение Linux (Red Hat), а не рабочий интерфейс; видны кнопки управления и порты USB/Ethernet на панели.

8 комментариев

Странности bc

Чем больше узнаёшь bc, тем больше в нём находишь странностей. Особенно меня поражают массивы.

Например, как вы думаете, чему равна длина массива, в который мы ещё ни разу не инициализировали? Правильно, единице. Почему? Да чёрт его знает, так в коде написано:

void
bc_array_init(BcVec* a, bool nums)
{
    BC_SIG_ASSERT_LOCKED;

    // Set the proper vector.
    if (nums) bc_vec_init(a, sizeof(BcNum), BC_DTOR_NUM);
    else bc_vec_init(a, sizeof(BcVec), BC_DTOR_VEC);

    // We always want at least one item in the array.
    bc_array_expand(a, 1);
}

Или вот такой код:

define void f(*a[]) {
    . = a[99]
}

f(b[]); length(b[])

Чему будет равна длина массива b[]?

Правильный ответ — она будет равна ста. Потому что в функции, куда мы передали этот массив по ссылке, происходит чтение из 99-го (массивы начинаются с нуля) элемента. В коде bc ясно сказано:

// The bc spec says that if an element is accessed that does not
// exist, it should be preinitialized to 0. Well, if we access
// an element *way* out there, we have to preinitialize all
// elements between the current last element and the actual
// accessed element.
if (v->len <= idx)
{
    BC_SIG_LOCK;
    bc_array_expand(v, bc_vm_growSize(idx, 1));
    BC_SIG_UNLOCK;
}

Как только идёт обращение к несуществующему элементу массива, неважно, для чтения или записи, он получается значение «ноль».

В этих случаях я даже уже и не понимаю — баги это или нет. Вроде нет, но выглядит контринтуитивно.

12 комментариев

← 1 8 9 10 11 12 67 →