Напомню, о чём шла речь в первой части: SwiftII — это мини-среда разработки в духе Swift для Apple II, где компилятор выдаёт байткод, а виртуальная машина его исполняет, — по образцу того, как в 1979 году Apple Pascal приносил на эту машину UCSD p-System. Разобравшись с языком и компилятором, перейдём к тому, что делает среду пригодной для работы: ввод, вывод, редактор и — отдельная большая тема — то, как всё это собиралось.
Что вводишь, что видишь и что сохраняется
На современном компьютере три вещи обычно совпадают: то, что вы набираете на клавиатуре, то, что видите на экране, и то, что попадает в файл. Нажали [ — на экране появился [, в файл лёг байт [. На оригинальном Apple II и II Plus эти три пути расходятся.
Клавиатура II/II+ выдаёт только заглавные буквы и не имеет клавиш для символов, нужных Swift. Дисплей знает только глифы из штатной символьной ROM, поэтому строчные буквы и фигурные скобки нарисовать не может. А файл на диске всё равно должен быть нормальным ASCII-исходником SwiftII, чтобы один и тот же .swift открывался на любом Apple II. Поэтому SwiftII хранит канонический исходник, а перевод делает на краях — при вводе и при выводе:
Вы набираете (на II Plus) | Видите на экране | Хранится на диске |
PRINT (обычное видео) | print (строчными) | |
'INT | инверсная I, затем nt | Int |
<: :> | [ ] | [ ] |
??/ | \ | \ |
<% | <% | { (один байт 0x7B) |
Обычный Swift — это в основном строчный ASCII и пунктуация, а клавиатура II/II+ не умеет ни строчных, ни \, ни { } [ ], ни , ни обратной кавычки. Поэтому между клавиатурой и файлом стоит слой перевода ввода: он автоматически переводит буквы в строчные, использует ' как одноразовый маркер заглавной ('' — на целое слово), назначает Ctrl-W на и заимствует диграфы и триграфы из стандарта C для недостающей пунктуации.

Апостроф-маркер важен, потому что Swift мешает регистры внутри идентификаторов. Тип Int набирается как 'INT, String — как 'STRING, а camelCase тоже обрабатывается: readLine набирается READ'LINE, а двойной '' помечает сразу целый прогон заглавных. На IIe с нормальной клавиатурой всё набирается как есть, но .swift-файл на диске получается одинаковым независимо от того, какая машина его создала.
С выводом отдельная история. Если и есть в проекте часть, где усилий вложено куда больше, чем видит пользователь, — это вывод символов на экран. Пришлось держать в голове четыре разных пути вывода. На дисплее до-IIe (40 колонок) строчные буквы показываются обычными заглавными, заглавные — инверсными заглавными, а недостающие глифы вроде { рисуются диграфами вроде <%. Videx на 80 колонок, а также IIe в режимах 40 и 80 колонок умеют показывать все символы напрямую. Способы работы с этими четырьмя типами экрана различаются даже среди тех, что показывают всё, — но это уже слишком большая тема для статьи.
Редактор и файловый браузер
Чтобы среда была полезной, мало написать интерпретатор — нужны средства работы с файлами и их редактирования. В приличном для меня виде на этой машине их не было, так что пришлось написать самому. Оба инструмента живут внутри того же бинарника лаунчера — так они получают доступ к файлам и возврат в меню без перезагрузки.

Файловый браузер — небольшой трёхпанельный менеджер. Слева — родительский каталог для контекста, справа — текущий каталог с курсором, строка деталей показывает тип ProDOS и размер каждой записи. Нижняя панель — превью: если задержать курсор на записи хотя бы на 1,5 секунды, появляется прокручиваемый предпросмотр файла (задержка снижает лишние обращения к диску). .swift-исходник рендерится ровно с теми же правилами регистра и диграфов, что и в редакторе, так что программа выглядит одинаково и в превью, и при редактировании. Поскольку у клавиатуры II Plus нет стрелок вверх/вниз, навигация идёт по I и M.
Редактор нужен потому, что REPL принимает по одной строке — этого хватает, чтобы попробовать короткое выражение, но не написать настоящую многострочную программу. У редактора два режима: «cooked» с поддержкой диграфов и маркеров регистра и «raw». .swift-файл открывается в cooked, обычный текстовый — в raw, потому что модель ввода в стиле Swift только испортила бы обычный README. Ctrl-G переключает режимы, перечитывая файл, чтобы то, что видно, и то, что хранится, оставались согласованными; тег [DGR] или [RAW] в строке статуса подсказывает текущий режим.
Курсор аккуратно относится к расхождению отображения: { — это один байт в буфере, но показывается как <%, шириной в две колонки на II (Plus). Перемещение по нему сдвигает курсор на один байт в памяти, даже когда визуально он перепрыгивает две колонки, — за этим следит вычисление ширины с учётом диграфов. Раскладка клавиш намеренно повторяет Apple Pascal: стрелки влево/вправо для перемещения, Ctrl-O/Ctrl-L — вверх/вниз (стандарт UCSD), Ctrl-S — сохранить, Ctrl-R — сохранить и запустить, Ctrl-Q — назад в браузер.
Кстати, о возврате без перезагрузки. Если внимательно посмотреть демо, видно, что при выходе из REPL система проходит через перезагрузку, прежде чем вернуться в меню. Выглядит топорно, но это вынужденно: чтобы корректно вернуться в лаунчер, REPL должен был бы прочитать с диска его SYS-файл и прыгнуть в него, а чтение файла требует MLI ProDOS — который REPL уже затёр своим кодом. Нужной процедуры загрузки в памяти больше нет. А вот редактор скомпилирован прямо в лаунчер, который работает только в основной памяти и держит MLI живым, — поэтому редактор открывает и сохраняет .swift напрямую, а переход браузер ↔ редактор происходит быстро, без перезагрузки.
Боль с переключением банков памяти
Переключение банков доставило немало хлопот. Apple II не считает дополнительную память единым плоским адресным пространством. 6502 адресует лишь 64 КБ, поэтому лишняя память появляется, замещая диапазон $D000–$FFFF другой физической подложкой. В этом окне может оказаться материнская ROM, код MLI ProDOS, код языковой карты SwiftII или выбранный банк Saturn.
Это делает банкинг одновременно полезным и опасным: он даёт SwiftII место под код, который не влезает в основную ОЗУ, но при этом какая-нибудь процедура может случайно скрыть код ROM или MLI, который сама же собирается вызвать.
REPL семейства A заметно больше того, что может оставаться резидентным в основной памяти, поэтому часть «холодного» кода приходится банковать. Память ниже $C000 остаётся видимой, а вот код в окне $D000–$FFFF зависит от текущего состояния банка, так что каждый вход в это окно и выход из него требует аккуратного переключения. Что держать в основной памяти, а что банковать, я делил по ожидаемой частоте выполнения: холодным считаю графику, peek/poke и т. п. — их и отправляю в Saturn 128K или в 64 КБ aux-памяти IIe.
Две карты используются по-разному: код в банкованном окне Saturn исполняется прямо с карты, а код в aux-памяти IIe исполняться на месте не может — его сначала надо скопировать в буфер-плацдарм в основной памяти и запускать уже оттуда. Компилятор и раннер семейства B используют ту же запасную память иначе — под хранение байткода: раннер по мере исполнения проходит через небольшое окно в основной памяти, а не исполняет всё на месте. Для программиста на SwiftII всё это прозрачно.
Почему проект — это девять дискет
Из всех ограничений вытекает одно следствие: SwiftII — не единый файл, а девять образов дисков. REPL семейства A переопределяет функции ввода-вывода MLI ProDOS, а компилятор-раннер семейства B, наоборот, в них нуждается для чтения и записи байткода на диск. Одним бинарником им не быть: один вытесняет ProDOS, другой без него не работает.
Итого релиз — это четыре дискеты REPL (семейство A), четыре дискеты компилятора (семейство B) и одна общая дискета с данными. Внутри каждого семейства дискеты различаются набором фич и объёмом программы, который тянет машина: базовая (lite) для любого II/II+, версии с Saturn и с aux-памятью IIe, а также варианты с нативными строчными буквами и 80 колонками на IIe. Компиляторы семейства B вдобавок к возможностям семейства A умеют файловый ввод-вывод (readFile, writeFile, appendFile, listDirectory), switch, for-in по массивам, random(in:), звук tone, тайминг wait, числовые функции abs/sgn и строковые hasPrefix/hasSuffix. Девятая дискета — data с примерами программ и тестовым набором, монтируется во второй дисковод рядом с любой из остальных.
Была мысль автоопределять машину при загрузке и подстраиваться под любую конфигурацию, но я сознательно от неё отказался. Во-первых, на дворе не 1970-е: выложить девять .po-файлов ничего не стоит, тогда как лишняя физическая дискета в ту эпоху обходилась дорого, — старая причина всё впихивать в один диск исчезла. Во-вторых, большинством этих машин я не владею, а значит, адаптивный путь исполнения был бы кодом, который я никогда не смог бы полностью проверить.
Тестирование зоопарка машин
Всё вышесказанное объясняет, почему тестирование стало отдельным большим проектом: каждый из восьми образов целится в чуть иную машину — от оригинального Apple II (Plus) с 16K-языковой картой до IIe с 64 КБ aux-памяти в расширенной 80-колоночной карте.
Одна команда make ci прогоняет обычные C-юнит-тесты, собирает девять образов дисков и проверяет каждый бинарник по бюджету размера. Но C-юнит-тесты — это логические тесты на моём современном Mac, а софт всё равно надо гонять на настоящих или эмулируемых системах 6502 с ProDOS. Вместо того чтобы запускать эмулятор вручную для каждой конфигурации железа и дискеты и проверять всё глазами, я сделал автоматический приёмочный стенд. Он гоняет весь UI-поток под headless-сборкой эмулятора izapple2 (портативный эмулятор Apple II Plus/IIe на Go): одна команда проходит по матрице железа, загружает нужную эмулируемую машину, подаёт нажатия клавиш, считывает экран и выдаёт pass/fail. Полный прогон по всем конфигурациям занимает около получаса.
Как это собиралось с ИИ
SwiftII собран с активной помощью ИИ — в основном Claude Code (Opus 4.8) и Codex (GPT-5.5). Проект я делал в свободное время около двух месяцев, на начальных тарифах Claude Pro и ChatGPT Plus, а не на более дорогих. Без ИИ как хобби-проект это было бы для меня нереально — руками на сопоставимом уровне ушло бы, по моей оценке, года два-три работы по вечерам, если не больше.
Базовые правила. Несколько правил я задал в начале и потом донастроил:
● Бюджет — это всерьёз. Код, который работает, но вдвое больше необходимого, всё равно провалил задачу. Ориентир всегда — 64-килобайтный II Plus, и каждая фича считается в байтах.
● Сначала проектный документ. Любой неочевидный выбор — раскладка нулевой страницы, представление значений, формат байткода, схема банкинга — сначала описывается с альтернативами и ценой, и только потом код.
● Приноси мне решение с вариантами, а не открытый вопрос. «Как мне это сделать?» — это не то.
● Попробуй фичу, измерь её реальную цену в памяти и выкинь, если не окупается.
Решения, которые оставались за мной. Самыми весомыми моими вмешательствами были архитектура и объём работ. ИИ построил схему автоопределения машины — я её зарубил, потому что не могу проверить такой механизм на железе, которого у меня нет. ИИ реально завёл плавающую точку — я её вырезал, потому что она ещё сильнее урезала и без того ограниченную кучу ради фичи, нужной единицам демок. Я вручную решал, что остаётся резидентным в основной памяти, а что уходит в банк. Я поднял размер кучи на уровнях с Saturn и aux-памятью — раз у этих машин есть запас, пусть он идёт программам, а не простаивает. Когда ИИ начинал выкручивать lite-REPL под то, что лучше делает компилятор, я переносил фичу в нужное семейство, а не впихивал её силой. И я вернул ИИ к привычным раскладкам клавиш в стиле Apple Pascal после того, как он изобрёл собственную, слишком неудобную на практике.
Claude и Codex делали разную работу. Claude Code был сильнее на старте — в проектах с нуля, начальной архитектуре и каркасе, но и лимиты у него кончались быстрее; иногда он спотыкался на реализации и не всегда синхронно обновлял все проектные документы. Ещё замечу, что Claude охотно спорит с решениями, которые считает неоптимальными. Codex был быстрее, буквальнее следовал инструкциям и хорошо работал проверяющим по уже сделанному, а ещё лучше и лаконичнее вёл документацию. Сложившееся разделение: Claude — под начальную структуру и трудные куски с нуля, Codex — под быстрое исполнение, проверку и наведение порядка в документации.
Почему автономный режим тут буксовал. Полностью автономная работа и попытки «сделать проект за один заход» здесь шли плохо. Подозреваю, дело в том, что архитектурные решения крайне нетипичны: данных по разработке под винтажные машины — тем более под почти полувековой Apple II — в обучающих выборках заметно меньше, чем по современной веб- и мобильной разработке. Так что ИИ я вёл вручную: сначала план, потом одобрение или корректировка, потом правки. Двигаться он мог быстро, но приходилось постоянно возвращать его к ограничениям и к тому, чего я на самом деле хочу.
Документация как память ИИ. Отдельно важным оказался вопрос ограниченного контекстного окна и границ сессий. Модель держит в контексте лишь ограниченный объём, а с концом сессии забывает всё — следующая стартует «на холодную», без памяти о том, почему было принято то или иное решение. Решением стало сделать долговременной памятью саму проектную документацию. SwiftII строился как 18 пронумерованных фаз (с 0 по 17), каждая — самодостаточная цель с письменным следом того, зачем она и что в итоге вышло. Вокруг фаз — около двадцати пронумерованных проектных документов, каждый фиксирует одно неочевидное решение: что выбрано, какие были альтернативы и как это сделано. Я относился к ним как к документам передачи дел: когда стартует новая сессия или я переключаю инструмент, она читает дорожную карту, нужные проектные документы и извлечённые уроки — так свежее контекстное окно наследует накопленный опыт, а не повторяет старые ошибки. По мере роста кодовой базы и документов на ориентацию уходило всё больше токенов, и бюджет токенов стал реальным ограничением рабочего процесса; документация помогала и мне, и ИИ держать в контексте только самое важное для текущей задачи.
Заключение
Проще всего попробовать SwiftII в эмуляторе — тулчейн не нужен. Скачайте образы дисков (.po) со страницы релизов на GitHub и откройте в эмуляторе Apple II: Mariani на macOS, кроссплатформенный izapple2, AppleWin на Windows или онлайновый Apple2TS. Для максимальной совместимости начинайте с swiftii-iip-lite-repl-vX.X.X.po.
Если у вас есть настоящее железо, каждая дискета — это стандартный 140-КБ ProDOS-образ 5,25″. Запишите нужную для вашей системы дискету через ADTPro или запустите с эмулятора дисковода вроде BMOW Floppy Emu. Оригинальный Apple II 1977 года тоже потянет — при условии апгрейда до 48 КБ ОЗУ и 16-килобайтной языковой карты.
В итоге всё это — работа, сделанная с любовью, и поклон наследию Apple II и Apple Pascal. Почти полвека назад Apple Pascal доказал, что высокоуровневый язык можно гонять на крохотном железе, компилируя в байткод; SwiftII — просто попытка применить ровно ту же философию к языку в духе Swift. Огромная доля архитектурной заслуги — Роберту Нюстрому: реализация ВМ во многом адаптирована из его прекрасной книги Crafting Interpreters. Не менее важен проект cc65, чей C-кросс-компилятор под 6502 сделал всю затею технически возможной. И, конечно, спасибо Стиву Возняку за Apple II: спроектировав машину с хорошо задокументированной, открытой и расширяемой архитектурой, он построил вещь, которая почти полвека спустя всё ещё учит нас работе с железом на самом низком уровне.
Есть что-то поэтичное в том, чтобы взять язык, созданный современным Apple, и вернуть его к 8-битному железу, с которого компания начиналась. Меня поражает, чего сегодня можно добиться для винтажных систем с ИИ-инструментами и упорным человеком: хобби-проект такого размера в одиночку был просто нереален ещё пару лет — да что там, пару месяцев назад. Полная документация лежит в каталоге docs репозитория SwiftII. Надеюсь, читать это было так же интересно, как мне — собирать.

