惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
U
Unit 42
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
IT之家
IT之家
P
Privacy International News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
酷 壳 – CoolShell
酷 壳 – CoolShell
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
爱范儿
爱范儿
Cyberwarzone
Cyberwarzone
罗磊的独立博客
Latest news
Latest news
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
美团技术团队
G
GRAHAM CLULEY
Help Net Security
Help Net Security
S
Secure Thoughts
博客园 - Franky
博客园 - 叶小钗
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
Microsoft Security Blog
Microsoft Security Blog
S
Security Archives - TechRepublic
博客园 - 司徒正美
Schneier on Security
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Hacker News - Newest:
Hacker News - Newest: "LLM"
Forbes - Security
Forbes - Security
Google Online Security Blog
Google Online Security Blog
A
About on SuperTechFans
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
D
Docker
Martin Fowler
Martin Fowler
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
S
Securelist
N
News | PayPal Newsroom
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Blog — PlanetScale
Blog — PlanetScale
Recent Commits to openclaw:main
Recent Commits to openclaw:main
B
Blog RSS Feed

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Ненормальное марковское программирование
Николай Меркин · 2026-06-15 · via Все публикации подряд на Хабре

Ненормальное марковское программирование

Сложный

11 мин

14

Какие программы могут быть по-настоящему достойны хаба "ненормальное программирование"?

Конечно же, программы для нормальных марковских алгорифмов! (Далее - НАМ).

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

Поэтому поставим задачу со звёздочкой: научимся писать для НАМ в компайл-тайме С++!
(Далее - компайл-тайм - КТ, рантайм - РТ)

Историческое отступление

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

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

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

Какие-то базовые извращения с кодом я сохранил в каталоге experimental, а какие-то безжалостно выкинул. Но можно посмотреть по истории коммитов на гитхабе https://github.com/nickolaym/nenormal.

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

План следующий:

  1. познакомимся с НАМ и научимся писать на них программы (в этой части)

  2. превозможем неприятности, которые нас ждут в КТ

  3. унифицируем с РТ

Нормальные Марковские Алгорифмы

Машина НАМ устроена следующим образом.

  • Программа - это последовательность правил подстановки - троек

    • строка поиска

    • строка замены

    • признак "обычное/финальное правило" (то есть: продолжать/остановиться)

  • Данные - это строка, над которой выполняется программа

Синтаксис НАМ

Традиционно, правила записываются в такой нотации:

"search" ->  "replace" # обычное правило
"search" ->. "replace" # финальное правило

(или без кавычек, если строки непустые и без пробелов и стрелок)

Расширение синтаксиса

Для наглядности, я введу понятие подпрограмм. По сути, это прото макросы, группирующие одно или несколько правил под произвольным именем.

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

Ну и комментарии в классическом стиле # .....

abc:
    "a" -> "b"
    "b" -> "c"

cde:
    "c" -> "d"
    "d" -> "e"

main: # если уж завели подпрограммы, то заведём и главную среди них
    cde
    abc

# общий список
main

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

DISCLAIMER

Этот синтаксис - чисто для статьи. В коде на C++ всё построено на обычном синтаксическом дереве C++, где уже сразу есть и строки, и элементарные правила, и подпрограммы любой вложенности. Но без колдунства со стрелочками и точками, просто  RULE("s", "r") и т.п.

(TODO: ещё и парсер забабахать в КТ... ну, это была бы вообще жесть).

Логика работы

За один такт машина

  • ищет первое подходящее правило (искомая строка входит в строку данных),

  • выполняет замену подстроки (только самое первое вхожение),

  • и если правило финальное - завершает работу, а если обычное - уходит на следующий такт

Машина останавливается,

  • когда было применено любое финальное правило

  • когда ничего не удалось найти (и заменить).

Можно считать, что в конце программы всегда есть правило аварийной остановки "" ->. ""

Попробуем что-нибудь попрограммировать.

Hello world

Программа из единственного правила

"" ->. "hello, world!"

для любой исходной строки просто добавит в её начало "hello, world!" и остановится.

(для любой, потому что, очевидно, пустая строка входит в любую строку, прямо с первой позиции).

Privet mir

Пусть у нас данные - это "hello, world!".

"world" -> "mir"
"hello" -> "privet"

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

  1. "hello, world!"

  2. "hello, mir!"

  3. "privet, mir!"

Порядок имеет значение

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

Применим программу к строке "eeecccaaa":

"a" ->  "B"
"c" ->. "D"
"e" ->  "F"
  1. "eeecccaaa" - a/B

  2. "eeecccBaa" - a/B

  3. "eeecccBBa" - a/B

  4. "eeecccBBB" - c/D, стоп

  5. "eeeDccBBB"

Оказалось, что третье правило вообще не достигнуто.

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

Вот тут у нас будет цикл

"BB"  -> "stop"
"aaa" -> "B"
"a"   -> "aa"
  1. "aaaa" - aaa/B - второе правило

  2. "Ba" - a/aa - третье

  3. "Baa" - a/aa

  4. "Baaa" - aaa/B - снова второе

  5. "BB" - BB/stop - первое

  6. "stop"

Это даёт нам подсказку, как программировать на НАМ.

  • Мы должны расставлять правила в порядке их приоритетов

  • и можем делать циклы, добавляя-убирая куски строк так, что вышестоящие правила снова подойдут

Правильная скобочная последовательность

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

Если они сбалансированы, то надо вывести "GOOD", а если нет - то "BAD".

Алгоритм работы:

  • сперва сократим все парные скобки: "()" -> ""

  • затем приведём все непарные к одному виду:

    • "(" -> "_"

    • ")" -> "_"

  • затем сократим всю серию непарных:

    • "__" -> "_"

  • затем, если осталась непарная (единственная) - выведем и остановимся

    • "_" ->. "BAD"

  • наконец, если ничего не осталось - выведем и остановимся

    • "" ->. "GOOD"

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

"()" ->  ""
"("  ->  "_"
")"  ->  "_"
"__" ->  "_"
"_"  ->. "BAD"
""   ->. "GOOD"

По сути, программа распалась на последовательность циклов:

  • пока есть пары, срабатывает первое правило, дальше не идём

  • пока есть открывающие скобки (теперь только непарные), первое правило не срабатывает, но срабатывает второе

  • аналогично, пока есть закрывающие

  • пока есть несколько подчёркиваний (а скобок больше не осталось), срабатывает четвёртое

  • ну и два финала

Мы не можем перемешать правила, но и не хотим.

А что там с проблемой останова?

Ведь НАМ эквивалентны машине Тьюринга.

А на чём бы её проверить? Конечно же, на всеми любимой гипотезе Коллаца!

Последовательность Коллаца

Пусть на входе строка из N единичек, N >= 1. (То есть, будем в единеричной системе счисления работать).

  • Если N=1, остановимся.

  • Если N чётно, то сократим их вдвое

  • Если нечётно - утроим и прибавим единицу.

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

  1. Если в строке ровно одна единичка, остановимся "1" ->. "STOP"

  2. Проверим чётность (а заодно и поделим пополам)

  • добавим в начало строки маркер деления "" -> "[div]"

  • и побежим вправо "[div]11" -> "1[div]"

  1. Когда маркер добежал до конца строки, там либо ничего не осталось, либо осталась одна единичка.

  • если ничего, то просто удалим маркер, "[div]" -> "" и вернёмся к началу

  • если единичка, то перейдём к шагу 3 - выполним N*3+1

  1. Выполним утроение, с учётом того, что слева (N-1)/2 единичек, а справа 1

  • учтём, что N*3+1 = (N-1)/2*6 + 4

  • запустим новый маркер "[div]1" -> "[mul]1111"

  • и побежим влево "1[mul]" -> "[mul]111111"

  1. Когда маркер добежал до начала строки, просто удалим его "[mul]" -> "" и вернёмся к началу

stop:         "1"       ->. "STOP"
div_start:    ""        ->  "[div]"
div_continue: "[div]11" ->  "1[div]"
div_end:      "[div]"   ->  ""
mul_start:    "[div]1"  ->  "[mul]1111"
mul_continue: "1[mul]"  ->  "[mul]111111"
mul_end:      "[mul]"   ->  ""

но очевидно же, что если собрать их в таком порядке, то мы мгновенно остановимся и получим строку вида "STOP111111одинодинодин!!!" (шучу, просто "STOP11111")

Поэтому stop должно быть в самом конце.

Тогда очевидно, что div_start нельзя делать первым, иначе получим взрыв "[div][div][div].....111"

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

Но не совсем! Например, если мы поместим mul_end перед mul_continue, то просто удалим маркер умножения, не добежав до левого края.

По сути, нам надо упорядочить правила так, чтобы искомые НАД-строки предшествовали ПОД-строкам.

mul_continue: "1[mul]"  ->  "[mul]111111"
mul_end:      "[mul]"   ->  ""

div_continue: "[div]11" ->  "1[div]"
mul_start:    "[div]1"  ->  "[mul]1111"
div_end:      "[div]"   ->  ""

stop:         "1"       ->. "STOP"
div_start:    ""        ->  "[div]"

Но вот незадача:

  • если мы поместим stop перед div_start, то получим старое доброе "STOP111111одинодинодин!"

  • а если наоборот, то зациклимся в 1-4-2-1, потому что всегда будет работать div_start.

    • "1" -> "[div]1" -> "[mul]1111" -> "1111"

    • "1111" -> "[div]1111" -> "1[div]11" -> "11[div]" -> "11"

    • "11" -> "[div]11" -> "1[div]" -> "1"

Решение - запускать деление не с 0, а с 2.

mul_continue: "1[mul]"  ->  "[mul]111111"
mul_end:      "[mul]"   ->  ""

div_continue: "[div]11" ->  "1[div]"
mul_start:    "[div]1"  ->  "[mul]1111"
div_end:      "[div]"   ->  ""

div_start:    "11"      ->  "1[div]"
stop:         "1"       ->. "STOP"
zero:         ""        ->. "ZERO"

Теперь мы даже можем расширить последовательность до неотрицательных чисел! Просто добавим правило zero, соответствующее вырожденному циклу 0 / 2 => 0

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

Давайте теперь мысленно оттрассируем.

3 - 10 - 5 - 16 - 8 - 4 - 2 - 1 - STOP

шаг

текст

N

правило

0

111

3

div_start

1

1[div]1

1*2+1

mul_start

2

1[mul]1111

1*6+4

mul_continue

3

[mul]1111111111

0*6+10

mul_end

4

1111111111

10

div_start

5

1[div]11111111

1*2+8

div_continue

6

11[div]111111

2*2+6

div_continue

7

111[div]1111

3*2+4

div_continue

8

1111[div]11

4*2+2

div_continue

9

11111[div]

5*2+0

div_end

10

11111

5

div_start

11

1[div]111

1*2+3

div_continue

12

11[div]1

2*2+1

mul_start

13

11[mul]1111

2*6+4

mul_continue

14

1[mul]1111111111

1*6+10

mul_continue

15

[mul]1111111111111111

0*6+16

mul_end

16

1111111111111111

16

div_start

17

1[div]11111111111111

1*2+14

div_continue

18

11[div]111111111111

2*2+12

div_continue

19

111[div]1111111111

3*2+10

div_continue

20

1111[div]11111111

4*2+8

div_continue

21

11111[div]111111

5*2+6

div_continue

22

111111[div]1111

6*2+4

div_continue

23

1111111[div]11

7*2+2

div_continue

24

11111111[div]

8*2+0

div_end

25

11111111

8

div_start

26

1[div]111111

1*2+6

div_continue

27

11[div]1111

2*2+4

div_continue

28

111[div]11

3*2+2

div_continue

29

1111[div]

4*2+0

div_end

30

1111

4

div_start

31

1[div]11

1*2+2

div_continue

32

11[div]

2*2+0

div_end

33

11

2

div_start

34

1[div]

1*2+0

div_end

35

1

1

stop

36

STOP

Мы умеем делать вычисления в единеричной системе счисления!

Как ещё можно программировать?

Определённую головную боль доставляет распознавание начала и конца строки.

У НАМ нет метасимволов ^ и $, но программисту подконтрольна спецификация на формат входных данных.

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

Тот же мини-Коллац: если строка обрамлена, то мы можем вместо хитрого порядка правил ввести уникальные тэги для каждого состояния алгоритма.

Тогда в каждый момент времени подойдёт ровно одно правило, а значит, их порядок неважен!

^$        ->. ZERO
^1$       ->. STOP

^11       -> ^[0][*2+]11 # изолировали маркер начала и запустили цикл деления
[*2+]11   -> 1[*2+]      # цикл деления (слева направо)

[*2+]$    -> [+]$        # закончили деление: чётное - запустили цикл сложения
1[+]      -> [+]1        # цикл сложения (справа налево)
^[0][+]   -> ^           # закончили сложение и убрали изолятор

[*2+]1$   -> [*6+]1111$  # закончили деление: нечётное - запустили цикл умножения
1[*6+]    -> [*6+]111111 # цикл умножения (справа налево)
^[0][*6+] -> ^           # закончили умножение и убрали изолятор

(Я намеренно не стал оптимизировать эту программу).

Строка данных в общем случае принимает один из следующих видов

  • ^11111$ - N единиц - основное состояние

  • ^[0]1111[*2+]11111$ - N = 0 + D*2 + R

  • ^[0]1111[*6+]11111$ - N' = 0 + D*6 + R 

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

Красиво же?

Десятичный Коллац

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

Например, умножение на константу - это такой же забег справа налево, с переносом.

Но программа при этом заметно распухнет, потому что нам нужно обработать все сочетания (цифра, перенос).

0[*3+0] -> [*3+0]0  # 00
1[*3+0] -> [*3+0]3  # 01
2[*3+0] -> [*3+0]6  # 01
3[*3+0] -> [*3+0]9  # 01
4[*3+0] -> [*3+1]2  # 12
5[*3+0] -> [*3+1]5  # 15
6[*3+0] -> [*3+1]8  # 18
7[*3+0] -> [*3+2]1  # 21
8[*3+0] -> [*3+2]4  # 24
9[*3+0] -> [*3+2]7  # 27

0[*3+1] -> [*3+0]0  # 01
...
9[*3+1] -> [*3+2]8  # 28

1[*3+2] -> [*3+0]5  # 05
...
9[*3+2] -> [*3+2]9  # 29

# когда кончились старшие разряды, - оставляем перенос
[*3+0]  ->
[*3+1]  -> 1
[*3+2]  -> 2

Деление на константу должно происходить "лесенкой", слева направо, но к счастью, в десятичной системе деление на 2 - это умножение на 5 с одним сдвигом.

0[/2]   -> [*5+0]   # частное кладём в перенос
2[/2]   -> [*5+1]
4[/2]   -> [*5+2]
6[/2]   -> [*5+3]
8[/2]   -> [*5+4]

0[*5+0] -> [*5+0]0  # обычное умножение на 5 с переносом
1[*5+0] -> [*5+0]5
.....
8[*5+4] -> [*5+4]4
9[*5+4] -> [*5+4]9

Полностью это выглядит так.
Пусть на каждом шаге последовательности строка имеет вид "111^12345$", где 111 - это счётчик шагов последовательности, а 12345 - текущее значение.
Начинаем вообще без счётчика, "^12345$".
Когда дойдём до финиша, уберём "^1$" и оставим только счётчик - за сколько шагов достигли 1.

# начало - изолируем конец строки символом !, чтобы не было гонки стартов
# и по признаку чётности решаем, будем ли мы делить на 2 или умножать на 3 +1
start:
  0$ -> 0[/2]!$
  1$ -> 1[*3+1]!$
  2$ -> 2[/2]!$
  3$ -> 3[*3+1]$
  ...
  9$ -> 9[*3+1]!$

# деление на 2
div2:
  0[/2] -> [*5+0]
  2[/2] -> [*5+1]
  ...
  8[/2] -> [*5+4]

# умножение на 3 и 5 с переносом
mul3:
  0[*3+0] -> [*3+0]0
  0[*3+1] -> [*3+0]1
  ...
  9[*3+1] -> [*3+2]8
  9[*3+2] -> [*3+2]9

mul3stop:
  ^[*3+0] -> (inc)^
  ^[*3+1] -> (inc)^1
  ^[*3+2] -> (inc)^2

mul5:
  0[*5+0] -> [*5+0]0
  0[*5+1] -> [*5+0]1
  ...
  9[*5+3] -> [*5+4]8
  9[*5+4] -> [*5+4]9

mul5stop:
  ^[*5+0] -> [+1]^
  ^[*5+1] -> [+1]^1
  ...
  ^[*5+4] -> [+1]^4

# когда умножение закончено, инкрементируем счётчик
inc:
  0[+1] -> 1
  1[+1] -> 2
  ...
  8[+1] -> 9
  9[+1] -> [+1]0
  # если нуля вообще не было (на первом шаге)
  [+1]  -> 1

# когда все действия одного шага выполнены, убираем изолятор
restart:
  !$ -> $

# в конце концов, стираем значение и останавливаем работу
finish:
  # если счётчик уже есть (проверяем по наличию младшего разряда) 
  0^1$ ->. 0
  1^1$ ->. 1
  ...
  9^1$ ->. 9
  # тот случай, если мы сразу начинали с 0 или 1 и не успели создать счётчик
  ^1$ ->. "0"
  ^0$ ->. "0"

main:
  # проверка конца последовательности идёт первой
  finish
  # правила рабочего цикла независимы, можем расположить как угодно
  start
  div2
  mul3
  mul3stop
  mul5
  mul5stop
  inc
  # рестарт цикла - строго в конце
  restart

Некоторые шаги можно склеить (начало умножения-деления), но я оставил их для наглядности.

Десятичное сложение

Научившись делать одноместные операции (а умножение на константу - одноместное), попробуем двуместные. Например, сложение.

Пусть входные данные выглядят так: "xxxxx+yyyyy=" (произвольной и необязательно одинаковой разрядности), и мы хотим получить "zzzzz".

Алгоритм

Пусть исходная строка "Xx+Yy=", где X и Y - цепочки старших разрядов, а x и y - одиночные младшие разряды

  1. Возьмём младший разряд x и перенесём его рядом к y

  2. Найдём (x+y) = z или 1z (то есть, с переносом)

  3. Вынесем z направо, и распространим перенос влево по Y, если это нужно

(кстати, мы же понимаем, что перенос - это частный случай умножения, на 1).

Будем повторять, пока не кончатся X и-или Y.

Один такт сложения разрядов - это преобразование строк

  • было "Xx+Yy=R" где R - ранее вынесенные разряды суммы

  • стало "X+U=zR" где z = (x+y) mod 10c = (x+y) div 10U = Y+c

Математически это означает следующее

"XXXXXx           +  YYYYYy           =              RRR" - было
(X*10+x)*10^n     + (Y*10+y)*10^n     +              RRR
 X      *10^(n+1) +  Y      *10^(n+1) + (x+y)*10^n + RRR
 X      *10^(n+1) + (Y+c   )*10^(n+1) +  z   *10^n + RRR
"XXXXX            + uUUUUU            =  z           RRR" - стало

В конце у нас возможны три ситуации:

  • "+=ZZZZZ" - числа одинаковой разрядности (с учётом переноса в Y) были сложены

  • "XXX+=ZZZZZ" - Y кончился, слева старшие разряды X, справа - младшие разряды Z

  • "+YYY=ZZZZZ" - X кончился, слева старшие разряды Y, справа - младшие разряды Z

Просто выбросим знаки + и =, тем самым, склеим (X+0)*10^n + Z или (0+Y)*10^n + Z.

Реализация

Не буду здесь расписывать всю программу, покажу лишь эскиз. С метасимволами xy и т.п., обозначающими цифры.

take_digit:
    "x+"   ->  "+[x]"

move_digit_to_the_right:
    "[x]y" ->  "y[x]"

add_digits:
    "y[x]=" ->  "=z"  # если x+y < 10, z = (x+y)
    "y[x]+" ->  "^=z" # если x+y >= 10, z = (x+y) mod 10

add_carry:
    "y^"    ->  "u"   # если y+1 < 10, u = u+1
    "y^"    ->  "^u"  # если y+1 >= 10, u = (u+1) mod 10
    "+^"    ->  "+1"  # добежали до левого края, там мысленный старший 0

plus_nothing:
    "x+="  -> ""      # особый случай: справа кончились разряды

cleanup:
    "+"     ->  ""
    "="     ->  ""

that_s_all:
    ""      ->. ""    # для приличия, добавим явную остановку

main:  # правила должны идти в таком порядке, чтобы не устраивать гонки!
    # эти три группы независимы, их можно перетасовать как угодно
    move_digits_to_the_right
    add_digits
    add_carry

    # начало сложения очередного разряда (только когда закончили с предыдущим)
    plus_nothing   # сперва обработаем особый случай
    take_digit

    # к этому моменту мы сложили все разряды, осталось склеить голову и хвост
    # (убрать лишние значки)
    cleanup

    # когда всё сделано, перейдём в финальное состояние
    that_s_all

Продолжение следует!...

В следующей части - магия компайл-тайма.