Swift — это современный язык программирования, лежащий в основе многих приложений, работающих на платформах Apple наших дней. Мне показалось, что было бы неплохо немного вернуться к истокам Apple, к Apple II. Это была первая массовая серия компьютеров Apple, первоначально выпущенная в 1977 году с процессором MOS 6502 с частотой 1 МГц.

Я создал SwiftII, мини-среду разработки на Swift для оригинального Apple II, а также для IIe и более поздних моделей.

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

Одно важное замечание: это не современный Swift и никогда им не будет. Полная стандартная библиотека Swift не поместится на этой машине. SwiftII — это намеренно созданное подмножество, гораздо ближе по духу к Embedded Swift, чем SDK, предназначенный для современных платформ, таких как iOS или macOS.

Моя цель заключалась в том, чтобы максимально адаптировать Swift для Apple II, сохранив при этом читаемость кода как у оригинала. Если вы знаете Swift, вы должны уметь читать программы, совместимые со SwiftII, и сразу понимать их.

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

Приложение

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

На оригинальной клавиатуре II Plus нет строчных букв и клавиши \, поэтому программа набирается диграфами. ??/ становится \, что SwiftII интерпретирует как канонический Swift. Подробнее об этой особенности позже.

Вот несколько примеров скриншотов:

Меню запуска при загрузке, полноэкранный редактор в 80 столбцов, файловый браузер с предварительным просмотром кода в реальном времени, игра xsnake, все 16 цветов низкого разрешения через gr / color / vlin, xgrdemo, демонстрация графики из пяти сцен с низким разрешением и цветом
Меню запуска при загрузке, полноэкранный редактор в 80 столбцов, файловый браузер с предварительным просмотром кода в реальном времени, игра xsnake, все 16 цветов низкого разрешения через gr / color / vlin, xgrdemo, демонстрация графики из пяти сцен с низким разрешением и цветом

Если вы слишком увлечены чтением и хотите погрузиться в код и образы дисков, вот репозиторий GitHub: https://github.com/yeokm1/swiftii.

Мотивация

Недавно я восстановил Apple II Plus, который мне любезно подарили. Мне стало интересно, какие современные возможности я могу ему предоставить.

Несколько лет назад я написал клиент ChatGPT для DOS и клиент Slack для Windows 3.1. Эти проекты были посвящены внедрению современного сетевого сервиса в устаревшую машину. На этот раз я хотел узнать, можно ли сделать то же самое для современного языка программирования, да ещё и от самой Apple!

Вдохновением для этого приложения послужил Apple Pascal. В 1979 году Apple Pascal перенёс p-System из Калифорнийского университета в Сан-Диего на Apple II. Вместо компиляции Pascal в машинный код для 6502 он компилировался в байт-код, который выполнялся на виртуальной машине, подобно тому, как это делает Java.

Я хотел сделать то же самое для Swift. Компилятор генерирует байт-код, а виртуальная машина (ВМ) его выполняет. С разницей почти в полвека оба языка достигли уровня 6502, избежав генерации нативного кода для 6502 через ВМ.

Я также хотел иметь инструмент REPL (Read-eval-print loop), чтобы пользователи могли тестировать свой код в интерактивном режиме. Современный SDK Swift от Apple также поддерживает эту функцию REPL, поэтому я посчитал разумным добавить и её.

Целевое оборудование

В качестве базовой целевой платформы используется оригинальный Apple II 1977 года с соответствующими аппаратными улучшениями. Если SwiftII работает на нём, он будет работать практически на любом будущем Apple II.

Apple II Plus 1979 года на фотографии выше — это усовершенствованная версия оригинального Apple II с такими функциями, как Applesoft BASIC, больший объём оперативной памяти и автозапуск с диска. В остальном, это обычный Apple II.

Мой Apple II Plus имеет следующие основные характеристики:

  • процессор MOS 6502 1 МГц;

  • 48 КБ оперативной памяти;

  • клон карты памяти Saturn на 128 КБ (обратно совместима как языковая карта на 16 КБ);

  • клон карты Videx Videoterm на 80 колонок;

  • универсальная карта контроллера дисков Yellowstone (может эмулировать оригинальный режим Disk II);

  • эмулятор дискет для эмуляции дисковода Disk II.

Более подробная информация о моей собственной настройке Apple II Plus приведена здесь: https://github.com/yeokm1/retro-configs-apple/tree/main/apple-ii-plus.

Оригинальный Apple II 1977 года поддерживается при условии, что он был модернизирован до 48 КБ оперативной памяти плюс языковая карта на 16 КБ, что даёт те же 64 КБ в сумме. На машине без автозапуска ПЗУ запуск Disk II осуществляется вручную с помощью команд C600G или PR#6.

Apple IIe — более удобный в использовании компьютер, поскольку он поддерживает нижний регистр, имеет улучшенную память дисплея и полноценную ASCII-клавиатуру. На IIe гораздо естественнее набирать исходный код SwiftII, вместо того чтобы полагаться на граф Apple II Plus и слой преобразования ввода, о которых я расскажу подробнее позже.

Я использовал ProDOS 2.4.3, потому что это современная, до сих пор поддерживаемая версия ProDOS 8, хорошо документированная и работающая на оригинальном Apple II. Использование единого эталонного образа ProDOS также значительно упростило сборку диска и тестирование по сравнению с поддержкой нескольких вариантов DOS/ProDOS. Для ProDOS требуется языковая карта объёмом не менее 16 КБ.

6502 — это 8-битный процессор. Его регистры A, X и Y имеют ширину 8 бит, и нет универсального 16-битного регистра. Нет блока обработки чисел с плавающей запятой и даже аппаратной инструкции умножения или деления. Аппаратный стек занимает всего 256 байт, что ограничивает глубину вызовов функций.

Что касается памяти, 64 КБ — это очень мало по сегодняшним меркам, больше похоже на микроконтроллер.

Язык

Вот пример того, как выглядит SwiftII. Если вы знаете Swift, вам это покажется очень знакомым.

Возможности SwiftII

Ядро (облегчённые диски) читается точно так же, как Swift:

  • let и var с выводом типов;

  • if / else if / else, while и for-in по диапазонам;

  • сокращённые вычисления && / || и префикс !;

  • функции верхнего уровня с return;

  • необязательные операторы if let, ?? и принудительная распаковка !;

  • массивы с функциями append, count, isEmpty и индексацией;

  • строки с конкатенацией +, интерполяцией \(...) и String(n).

Кроме того, дополнительные возможности зависят от того, с какого диска вы загружаетесь:

  • диски extras (sat/aux) добавляют разбор целых чисел (Int(s)), вспомогательные функции для работы с байтами/строками (asc, chr), больше методов для работы с массивами (removeLast, removeAll, contains), peek/poke, управление курсором/текстом и графику низкого разрешения (gr, color, plot, hlin, vlin);

  • диски компилятора Family B идут дальше, предлагая switch, for-in непосредственно над массивами, random(in:), tone и файловый ввод-вывод.

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

В чём отличие типов от Swift

Типы ведут себя иначе, чем в обычном Swift:

  • Int имеет знаковый 16-битный тип из-за ограничения на кросс-компиляторы. Диапазон значений — от −32768 до 32767. (В современных системах обычный Swift — 64-битный). Это также единственный числовой тип, нет Double и Float. Подробнее об этом я расскажу в разделе ниже.

  • String — это просто байты, а не Unicode. Современный Swift рассматривает строку как коллекцию объектов Character. Строки SwiftII — это последовательности байтов ASCII, ближе к строковым массивам в стиле C.

  • Элементы в массивах должны быть одного типа.

  • Идентификаторы ограничены 11 символами для экономии места. Более длинное имя приводит к ошибке компиляции, а не к усечению, поэтому два длинных имени никогда не могут конфликтовать по общему префиксу.

Также отсутствуют сокращённые вычисления, словари, обработка ошибок или throws, параллелизм (async/await), макросы и метки аргументов в месте вызова. Каждый из этих вариантов потребует памяти или сложности компиляции за один проход, чего машина, очевидно, не может себе позволить.

Основное ограничение: 40 704 байта

Чтобы осознать бюджет памяти, сначала нужно понять карту памяти Apple II. 6502 может адресовать 65536 байт с помощью своей 16-битной адресной шины. Бинарные файлы SwiftII работают под ProDOS как SYS-бинарные файлы, которые начинаются с $2000.

Поскольку они не оставляют места для BASIC.SYSTEM выше этого значения, практический потолок загрузки образа — это непрерывный диапазон от $2000 до глобальной страницы ProDOS в $BF00 -> $2000-$BEFF.

Я использовал ИИ для создания этой инфографики.

Схема переключения банков памяти Apple II, показывающая окно D000 в FFFF, банки Saturn и копирование в вспомогательную ОЗУ
Схема переключения банков памяти Apple II, показывающая окно D000 в FFFF, банки Saturn и копирование в вспомогательную ОЗУ

Этот регион составляет ровно 40 704 байта. Это вся основная ОЗУ, которую может использовать мой бинарный файл.

Память свыше 64 КБ

64 КБ достаточно для работы основных функций SwiftII, но недостаточно для большего их количества или более крупных программ. Однако память свыше 64 КБ не является напрямую адресуемой.

Обновления памяти того времени, такие как карта Saturn 128K или 64 КБ вспомогательной оперативной памяти IIe, не расширяют адресное пространство, как это было в случае с современной линейной памятью. Она находится за общим окном, где желаемый банк выбирается с помощью программных переключателей. Затем ваша программа записывает данные в этот конкретный выбранный банк. Если вы знакомы с миром DOS на ПК, то концептуально это похоже на расширенную спецификацию памяти (EMS).

На Apple II это особенно неудобно, потому что в окне языковой карты также находится ПЗУ, код интерфейса машинного языка ProDOS (MLI). В этот момент там может быть виден только один из них.

Я вернусь к этой теме позже.

Настройка разработки

Весь проект написан в основном на C90 и компилируется двумя способами:

  • clang на современном Mac для модульных тестов;

  • cc65 для Apple II, где он фактически поставляется. cc65 — это кросс-компилятор C для систем 6502.

Почему C, а не ассемблер? Заставить ИИ писать на ассемблере или напрямую в двоичный код, вероятно, позволило бы добиться большего повышения эффективности. Но проект, содержащий ассемблер 6502, было бы гораздо сложнее для меня в плане проверки и внесения изменений.

C поддерживает проект на таком уровне, что, если инструменты ИИ неверны/недоступны или мне приходится самостоятельно отлаживать последний этап, я все равно могу понять и изменить код приемлемым образом. Думаю, компромисс оправдан.

Как работает конвейер компиляции

Программа SwiftII проходит через традиционный конвейер:

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

Вместо этого он использует однопроходный парсер Пратта, который генерирует байт-код непосредственно по мере чтения исходного кода. Промежуточного представления нет. Это вдохновлено книгой Роберта Нистрома «Создание интерпретаторов».

(Учитывая сложность этого раздела и для обеспечения точности, я поручил ИИ значительно улучшить и проверить этот и последующие подразделы).

От исходного текста к байт-коду

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

Возьмем эту строку:

let x = 1 + 2

Лексический анализатор сначала преобразует символы в поток токенов:

LET IDENT(x) EQUAL INT(1) PLUS INT(2) EOF

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

В парсере выражений используется парсинг Пратта. Каждый оператор имеет приоритет. Когда он видит 1 + 2, он генерирует «push 1», когда видит +, замечает, что + связывается со следующим значением, генерирует «push 2», а затем генерирует «add». В памяти нет объекта BinaryExpr(left: 1, op: +, right: 2). Выходные байты генерируются по мере работы парсера:

Виртуальная машина — это стековая машина. Таким образом, этот байт-код помещает операнды в стек, выполняет код операции, который извлекает их, помещает результат в стек, а затем сохраняет результат в глобальный слот 0.

Функции верхнего уровня также являются просто диапазонами байт-кода. Когда компилятор видит func greet(...), он записывает начальное смещение функции, количество параметров и флаг возврата в небольшую таблицу функций, а затем выводит тело функции в область байт-кода. Последующий вызов не передаёт указатель на функцию, он передаёт индекс в таблице функций.

Строки проходят несколько иной путь. Компилятор копирует каждый литерал в кучу констант и выводит OP_STR <offset>. Поэтому файл .swb должен содержать как байт-код, так и пул констант. Инструкции гласят: «загрузить строку по смещению в куче N», и исполнитель должен воссоздать ту же структуру кучи констант перед выполнением программы.

Как на самом деле выглядит байт-код

Каждая инструкция состоит из 1 байта кода операции, за которым следуют от 0 до 2 байтов встроенного операнда. Многобайтовых кодов операций или префиксов нет.

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

Вот пример: let x = 1 + 2, за которым следует print(x), предполагая, что x — глобальная переменная #0, а print — встроенная #0, компилируется в двенадцать байтов:

Поскольку однопроходный компилятор не отслеживает типы операндов глубоко, некоторые коды операций являются полиморфными во время выполнения. OP_ADD выполняет целочисленное сложение, когда оба операнда являются целыми числами, но при использовании строк происходит конкатенация строк в куче, поэтому язык получает "a" + "b" без второй инструкции конкатенации.

Интерполяция строк работает аналогично: один код операции проверяет тег значения и преобразует текст в Int, Bool или nil.

Полностью отсутствует плавающая точка

В SwiftII нет чисел с плавающей запятой типа Double, Float и десятичных чисел. Int — это 16-битное знаковое целое число, и это единственный доступный числовой тип.

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

К слову, собственный язык Apple Embedded Swift, упрощённый диалект, предназначенный для микроконтроллеров, также бережно относится к числам с плавающей запятой. Долгое время даже вывести число типа Double было невозможно, потому что для этого не существовало стандартной библиотечной процедуры. Эта проблема была решена только в Swift 6.3, где была реализована полностью Swift-версия.

Если даже ограниченный Swift рассматривает числа с плавающей запятой как нечто, требующее осторожной обработки, то Apple II 1970-х годов столкнется с большими трудностями.

Applesoft BASIC, поставляемый с II Plus, поддерживает числа с плавающей запятой через свои процедуры обработки. Однако это реализовали путём хранения данных непосредственно в ПЗУ, чего я сделать не могу. Я также понимаю, что производительность при работе с числами с плавающей запятой на Apple II не очень хороша.

Два семейства бинарных файлов

Один и тот же исходный код поставляется как два семейства бинарных файлов.

Причина в конструкции и использовании языковой карты Apple II объёмом 16 КБ, расположенной в диапазоне $D000-$FFFF. ProDOS хранит свой MLI в том же диапазоне.

  • Семейство A содержит REPL. Чтобы поместиться, интерпретатор выгружает «холодный» код в языковую карту. Это перезаписывает интерфейс командной строки ProDOS, поэтому семейство A не может выполнять общий ввод-вывод файлов после запуска интерпретатора. Оно может запускать программы, подготовленные программой запуска, но не может свободно открывать и читать произвольные файлы.

  • Семейство B содержит компилятор и средство запуска. Это инструменты только для MAIN-компилятора, поэтому они оставляют языковую карту свободной, и интерфейс командной строки ProDOS остается активным.

Оба диска семейства содержат средство запуска, которое объединяет файловый браузер и редактор.

Компилятор семейства B считывает исходный файл .swift и записывает на диск байт-код .swb для выполнения средством запуска.

Файл .swb содержит 12-байтовый заголовок (трёхсимвольная магическая строка SWB, однобайтовая версия формата, счётчик входных программ и длины секций), за которым следует байт-код, пул констант и 4-байтовая запись для каждой функции.

Сама система семейства B поставляется на четырёх дисках, разделённых на три уровня в зависимости от объёма свободной оперативной памяти машины.

На стандартной машине вы получаете 1834 байта байт-кода. Для увеличения объёма используется дополнительная карта оперативной памяти Saturn 128K или IIe 64K (например, карта расширенного текста на 80 столбцов), которая выгружает завершённые тела байт-кода функций из основной памяти в свободную оперативную память.

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

Почему выход из REPL приводит к перезагрузке компьютера

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

Это выглядит неестественно, но это вынужденная мера. Для корректного возврата в меню запуска REPL должна была бы прочитать системный файл лаунчера с диска и перейти к нему. Чтение файла требует наличия ProDOS MLI, но REPL перезаписала MLI своим собственным кодом. Подпрограмма MLI, необходимая для загрузки следующей программы, больше не существует в памяти.

Полноэкранный редактор скомпилирован в лаунчер, а не является отдельной программой. Лаунчер работает только с Main и поддерживает ProDOS MLI в рабочем состоянии, поэтому редактор может открывать и сохранять файлы .swift напрямую.

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

  • Ctrl-Q возвращает в файловый менеджер без перезагрузки.

  • Ctrl-R сохраняет ваш файл, подготавливает исходный код, запускает интерпретатор.

«Что вы набираете» vs «что вы видите» vs «что сохраняется»

На современных компьютерах обычно три вещи совпадают:

  • что вы набираете на клавиатуре,

  • что вы видите на экране,

  • что сохраняется в файле.

Вы нажимаете [, на дисплее появляется [, и байт для [ попадает в файл. На оригинальных Apple II и II Plus эти три пути расходятся.

Клавиатура II/II+ воспроизводит только заглавные буквы и не имеет клавиш для символов, необходимых Swift. Дисплей имеет только оригинальные символы ПЗУ, поэтому он не может напрямую отображать строчные буквы или фигурные скобки. Однако файл на диске по-прежнему должен быть обычным ASCII-кодом SwiftII, поэтому один и тот же файл .swift работает независимо от того, какой Apple II его написал.

Таким образом, SwiftII сохраняет каноничность хранения и выполняет трансляцию по краям дисплея и клавиатуры.

Следующие два раздела описывают входную и выходную стороны этого преобразования.

Набор текста на Swift на клавиатуре 1977 года для II и II+

Обычный Swift состоит в основном из строчных букв ASCII и знаков препинания. Клавиатура II/II+ не может набирать строчные буквы, обратную косую черту, { } [ ], _ или обратную кавычку.

Поэтому SwiftII помещает слой преобразования ввода между клавиатурой и сохранённым файлом. Он автоматически переводит буквы в нижний регистр, использует ' в качестве одноразового маркера верхнего регистра ('' для целого слова), сопоставляет Ctrl-W с _ и заимствует стандартные для C диграфы и триграфы для отсутствующих знаков препинания.

На скриншоте 'INT' в памяти становится Int, а инверсное видео обозначает заглавные буквы там, где Apple II Plus не может отобразить строчные. Это же правило применяется и к camelCase. readLine вводится как READ'LINE, а двойная '' обозначает сразу весь ряд заглавных букв.

Этот апостроф важен, потому что Swift смешивает верхний и нижний регистры внутри идентификаторов. Тип, например Int, вводится как 'INT', а String — как 'STRING'.

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

Отображение текста на Apple II

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

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

Для 40-колоночного экрана Pre-IIe необходимо выполнить перевод из канонического Swift в верхний регистр, инвертированное преобразование цвета и диграфы.

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

Разработка Swift на самом компьютере

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

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

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

Файловый менеджер

Он представляет собой небольшой трёхпанельный файловый менеджер. Левая панель отображает родительский каталог для контекста, правая панель — текущий каталог с выделением курсором. В строке с подробной информацией отображается тип ProDOS и размер каждой записи.

Нижняя панель — это предварительный просмотр. Если пользователь удерживает курсор не менее 1,5 секунд, панель отображает прокручиваемый предварительный просмотр файла. Задержка уменьшает количество ненужных операций чтения с диска. Панель отображает исходный код .swift с точно такими же правилами регистра и диграфа, которые использует редактор, поэтому программа выглядит идентично как при предварительном просмотре, так и при редактировании.

Поскольку на клавиатуре II Plus нет клавиш со стрелками вверх/вниз, навигация осуществляется с помощью клавиш I и M для перемещения вверх и вниз.

Редактор

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

Он имеет режим «готового» и «сырого» кода. Файл .swift открывается в «готовом» режиме с поддержкой диграфов и регистровых маркеров. Обычный текстовый файл открывается в «сыром» режиме, потому что модель типизации — это соглашение исходного кода Swift, которое только исказило бы обычный файл README.

Ctrl-G переключает между ними, перезагружая файл, чтобы то, что вы видите, и то, что хранится, оставались согласованными. В верхней строке состояния отображается тег [DGR] или [RAW], указывающий на текущий режим.

Курсор учитывает несоответствие отображения, о котором говорилось ранее. Символ { в буфере представляет собой один байт, но на II (Plus) отображается как <%, шириной в два столбца. Перемещение курсора стрелкой по нему перемещает по одному байту в памяти, даже если курсор заметно сдвигается на два столбца, а расчёт ширины столбцов учитывает особенности графа.

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

На IIe без подобных ограничений всё может отображаться как есть.

Проблема с банкингом памяти

Этот способ банкинга памяти доставлял немало хлопот.

Apple II не рассматривает дополнительную оперативную память как одно большое плоское адресное пространство. 6502 может адресовать только 64 КБ, поэтому дополнительная память появляется за счёт замены $D000-$FFFF другим физическим резервом. Она может вмещать:

  • ПЗУ материнской платы;

  • код MLI ProD;

  • код языковой карты SwiftII;

  • выбранный банк Saturn.

Это делает банкинг одновременно полезным и опасным. Он даёт SwiftII место для размещения кода, который не помещается в основную оперативную память, но также означает, что подпрограмма может случайно скрыть код ПЗУ или MLI ProDOS, который она собирается вызвать.

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

Основная оперативная память ниже $C000 остаётся видимой, но код в окне языковой карты $D000-$FFFF зависит от текущего состояния банка. Поэтому каждый вызов в это окно или из него требует тщательно управляемого переключения банка.

Разделение кода на основной и резервный (банки) области памяти определяется моей ожидаемой частотой выполнения. Холодный код я классифицирую как графический, peek/poke и т.д. Он размещается в памяти карты Saturn 128K или во вспомогательной памяти IIe объёмом 64K.

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

  • выбор банка памяти на Saturn позволяет коду в резервном окне выполняться непосредственно с карты;

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

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

Всё это ясно программистам SwiftII.

Почему этот проект поставляется на девяти дисках

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

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

REPL семейства A переопределяет функции ввода-вывода MLI ProDOS, но компилятору-исполнителю семейства B они необходимы для чтения/записи байт-кода на диск. Поэтому оба варианта не могут быть одним и тем же бинарным файлом, поскольку один вытесняет ProDOS, а другой нуждается в нём.

Таким образом, релиз включает 4 диска REPL семейства A, 4 диска компилятора семейства B и 1 диск с общими данными.

Семейство B: Компилятор и исполнитель

Все 4 диска компилируют файл .swift в файл байт-кода .swb, а затем запускают его, и они обладают самым большим набором функций в проекте. В дополнение ко всему, что есть в семействе A, они имеют:

  • файловый ввод/вывод: readFile, writeFile, appendFile, listDirectory;

  • switch;

  • for-in по массивам;

  • random(in:);

  • звук: tone;

  • тайминг: wait;

  • числовые функции: abs / sgn;

  • строковые функции: hasPrefix / hasSuffix.

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

Диски IIe поддерживают 80-колоночный режим, если установлена ​​соответствующая 80-колоночная карта.

Девятый диск — это данные. Он содержит примеры программ и набор тестов, смонтированных на диске 2 вместе с любым из вышеперечисленных файлов.

Я подумывал об автоматическом определении машины при загрузке и адаптации к каждой конфигурации. Я намеренно этого не сделал по двум причинам.

  1. Сейчас уже не 1970-е годы. Доставка восьми файлов .po ничего не стоит, тогда как дополнительная физическая дискета в ту эпоху была бы дорогостоящей. Теперь причина принудительного размещения всего на одном диске неактуальна.

  2. У меня нет большинства этих машин. Поэтому адаптивный путь выполнения был бы кодом, который я никогда не смог бы полностью проверить.

Тестирование слишком большого количества машин

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

  • оригинальной Apple II (Plus) с языковой картой на 16 КБ;

  • II Plus с Saturn;

  • II Plus с Saturn и 80-колоночным Videx;

  • IIe;

  • IIe с дополнительной ОЗУ в 64 КБ, расширенной 80-колоночной текстовой картой.

Один запуск make ci выполняет модульные тесты на чистом C, собирает все восемь образов дисков и проверяет каждый бинарный файл на соответствие его размеру.

Модульные тесты на чистом C — это логические тесты, которые выполняются на моём современном хост-компьютере Mac, но нам всё ещё необходимо запускать программное обеспечение на реальных или эмулированных системах 6502 с ProDOS для проверки.

Вместо того чтобы запускать эмулятор по одному с каждой конфигурацией оборудования и диска и проверять его вручную, я решил, что необходима автоматизированная система приемки. Она управляет всем потоком пользовательского интерфейса в безголовой сборке izapple2, портативного эмулятора Apple II Plus/IIe, написанного на Go.

Одна команда сканирует аппаратную матрицу, загружает нужную эмулируемую машину, внедряет нажатия клавиш, сканирует экран и сообщает о прохождении/непрохождении.

Разработка на эмулированных конфигурациях Apple II заняла около получаса.

Как в этом помог ИИ

SwiftII был разработан с активной помощью ИИ, в основном с использованием Claude Code (Opus 4.8) и Codex (GPT 5.5).

Для контекста, я работал над проектом в свободное время в течение 2 месяцев, используя базовые планы Claude Pro и ChatGPT Plus, а не более дорогие.

Без ИИ этот проект был бы для меня невозможен как хобби.

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

Следующие правила и вызовы были сгенерированы ИИ и извлечены из истории моего репозитория.

Основные правила

Я установил несколько начальных правил с помощью ИИ и доработал их:

  • бюджет реальный. Код, который работает, но вдвое больше, чем нужно, всё равно не работает. Целевая платформа всегда 64 КБ II Plus, и каждая функция оценивается в байтах;

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

  • предложите мне решение с вариантами, а не ответ на открытый вопрос. «Как мне это сделать?» — это неприемлемо;

  • протестируйте функцию, измерьте её реальную стоимость памяти и откажитесь от неё, если она того не стоит.

Мои собственные решения 

Моменты моей личной работы обычно касались архитектуры или масштаба:

  • поставлять один образ диска на машину, а не тестовую среду во время выполнения. ИИ разработал схему определения машины. Я отказался от этого подхода, потому что не могу проверить тестовую среду на оборудовании, которого у меня нет. Я не уверен, насколько точны эмуляторы в этом отношении. В 2026 году поставка большего количества файлов .po ничего не стоит;

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

  • горячий код в основной ОЗУ, холодный код в банке памяти. Я контролировал, что остаётся в основной ОЗУ, а что выводится в банк памяти;

  • увеличение объёма кучи на Saturn и вспомогательных уровнях. На этих машинах есть свободная оперативная память, поэтому используйте её для выделения большего пространства реальным программам, а не оставляйте её без дела;

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

  • привязки клавиш Apple Pascal. Я вернул ИИ к использованию знакомых соглашений UCSD и IJKM после того, как ИИ придумал свою собственную схему сочетаний клавиш, которая оказалась слишком неудобной для использования на практике;

  • тестирование на нескольких эмуляторах: я тестировал на эмуляторах Mariani и izApple2, чтобы не перенастраивать поведение моей программы под один эмулятор, на случай, если есть ошибки или особенности;

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

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

Claude и Codex справлялись с разными задачами

Я использовал два инструмента ИИ, и они оказались полезными по-разному.

Claude Code был сильнее в работе с нуля, начальной архитектуре и создании каркаса. Обратная сторона медали заключается в том, что он быстрее исчерпывал свой ресурс. Иногда он давал сбои во время реализации и не всегда синхронизировал все соответствующие проектные документы. Я обнаружил, что Claude готов оспаривать решения или проектные задачи, которые он считает неоптимальными.

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

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

Почему автономный режим постоянно давал сбои?

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

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

Документация как память ИИ

Ещё одна деталь рабочего процесса имела большое значение — ограниченные контекстные окна и границы сессий ИИ.

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

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

Ключевые этапы:

  • Этапы 0–1 — создание каркаса, затем настоящий лексер, однопроходный компилятор и виртуальная машина байт-кода, при этом 1 + 2 выводят 3 от начала до конца как на хосте, так и на Apple II.

  • Этапы 2–3 — интерактивная REPL, затем строки и управление потоком выполнения, что также определило модель клавиатуры и дисплея до Apple IIe.

  • Этап 4 — функции, опционалы и массивы: момент, когда начинает ощущаться, что это Swift.

  • Этап 8 — определение возможностей оборудования и матрица многобинарной сборки, корень разделения на облегчённую и расширенную версии.

  • Этапы 11–13 — бинарный файл aux extras для Apple IIe, 80-колоночный текст и запуск программ .swift непосредственно с диска.

  • Этапы 14–15 — редактор в лаунчере и диск с данными, затем компилятор и исполнитель Family B на диске для программ, слишком больших для REPL.

  • Этапы 16–17 — расширенные функции (switchrandomtone, страничный байт-код, 80-колоночный Videx) и доработка и выпуск версии 1.0.

Вокруг этапов расположено около 20 пронумерованных проектных документов, каждый из которых описывает одно неочевидное решение. Наиболее полезные из них:

  • 003 — модель набора текста Apple II Plus (автоматический ввод строчных букв, маркеры регистра, диграфы).

  • 011 — перенос дополнительных функций на карту Saturn или вспомогательную оперативную память IIe (XLC-транзистор и копирование вниз).

  • 013 — 80-колоночный текст в прошивке IIe и Videx для II Plus.

  • 015 и 016 — инструментарий компилятора/запуска семейства B и окно потоковой передачи исходного кода.

  • 018 — целевая, самонастраивающаяся тестовая среда.

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

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

Попробуйте!

Самый простой способ попробовать SwiftII — в эмуляторе, инструментарий не требуется. Загрузите образ диска (.po) из репозитория GitHub Releases и откройте его в эмуляторе Apple II: Mariani на macOS, AppleWin на Windows или izapple2, который является кроссплатформенным. Для максимальной совместимости начните с swiftii-iip-lite-repl-vX.X.X.po.

Если у вас есть реальное оборудование, каждый диск представляет собой стандартный образ ProDOS 5,25" размером 140 КБ. Запишите соответствующий диск для вашей системы на дискету с помощью ADTPro или запустите его из эмулятора дискет, например, BMOW Floppy Emu. Оригинальный Apple II 1977 года тоже его запускает, если он был модернизирован до 48 КБ ОЗУ и языковой карты на 16 КБ.

Если вам нужен набор инструментов, репозиторий собирает весь набор дисков из исходного кода с помощью cc65. Вы также можете кросс-компилировать файл .swift в файл .swb на своем Mac и запустить его на Apple II.

Заключение

В конечном итоге, это была дань любви и уважения наследию Apple II и Apple Pascal. Почти 50 лет назад Apple Pascal доказал, что можно запускать язык высокого уровня на миниатюрном оборудовании, компилируя его в байт-код. SwiftII — это всего лишь моя попытка применить ту же самую философию к языку, подобному Swift. 

Огромная часть архитектурной заслуги принадлежит Роберту Нистрому, реализация виртуальной машины в значительной степени адаптирована из его феноменальной книги «Создание интерпретаторов». Не менее важен проект cc65, чей кросс-компилятор C для 6502 сделал всю эту затею технически осуществимой.

Огромная благодарность Стиву Возняку за Apple II. Разработав его с хорошо документированной, открытой расширяемой архитектурой, он не только внёс огромный вклад в революцию домашних компьютеров, но и создал вневременную машину, которая продолжает учить нас основам вычислительной техники на «голом железе» почти полвека спустя.

Есть что-то глубоко поэтичное в том, чтобы взять язык, созданный современной Apple, и замкнуть круг, вернувшись к 8-битному оборудованию, с которого началась компания. Команда Swift действительно преуспела, разработав язык, достаточно элегантный, чтобы сделать этот эксперимент хотя бы отдалённо осуществимым на таком старом оборудовании.

Я хотел бы поблагодарить создателей эмуляторов Apple II izapple2 и Mariani, которые позволили мне протестировать проект на современном хосте Mac. Без них разработка проекта не была бы лёгкой.

Приятный бонус в том, что ProDOS позволяет использовать 15-символьные имена файлов. Вам не нужно, например, всё сжимать в архаичный формат DOS 8.3, то есть вы можете использовать настоящее расширение .swift. Это небольшая деталь, но её стоит отметить.

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

Надеюсь, вам понравилось читать это так же, как мне понравилось создавать этот проект. Я посвящаю этот проект Стиву Возняку и команде Swift в Apple.

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

Комментарии (1)


  1. voodoo144
    30.06.2026 01:23

    Очень интересный проект - но перевод статьи просто кровь из глаз…