В эпоху восьмибитных домашних компьютеров команда LIST на компьютерах Commodore была одним из основных средств вывода текста программ на экран. Несмотря на простоту её назначения — показать пользователю исходный текст программы, — реализация этой команды в Commodore BASIC оказалась далеко не безупречной и породила множество неожиданных проблем и курьезов. В частности, необычайные приключения с REM-операторами и использованием специальных PETSCII-символов не раз уводили пользователей в плен загадочных ошибок и загадок, связанных с внутренним устройством интерпретатора BASIC. Рассмотрим особенности поведения команды LIST и причины, по которым она может показать далеко не тот текст, который ожидался, а также возможные способы решения и обхода этих недочётов. В основе команды LIST лежит модель представления программы как связанного списка строк, каждая из которых хранится в памяти с указателем на следующую.
При вызове LIST происходит итерация по этим ссылкам и вывод текста строк с развёрнутыми ключевыми словами. При этом все ключевые слова в программе хранятся в виде токенов — специальных байтов с установленным старшим битом, заменяющих длинные слова на один байт для экономии памяти и ускорения обработки. При выводе происходит обратная операция — UN-CRUNCH — замена токена на символьное слово. Несмотря на кажущуюся простоту обратного преобразования, реализация команды LIST в старых ROM-листингах имеет существенные недостатки. Одной из главных проблем является некорректная обработка комментариев REM.
В то время как токенизатор (CRUNCH) воспринимает REM как начало комментария и оставляет все последующие символы строки без изменений, команда LIST по факту продолжает попытки распознать эти символы как потенциальные токены. Это особенно заметно при использовании в комментариях PETSCII-символов с установленным старшим битом — так называемых «shifted» символов (например, SHIFT-L). Вместо того чтобы просто отобразить эти символы, LIST пытается интерпретировать их как ключевые слова BASIC и подставлять их текстовые расшифровки. Из-за особенностей хранения токенов, один из PETSCII-кодов (SHIFT-L или 0xCC) фактически соответствует коду, используемому как маркер конца списка ключевых слов. Когда LIST доходит до такого символа при чтении токенизированного списка, он сталкивается с неожиданной ситуацией: вместо корректного завершения происходит обращение к адресу с нулём, и попытка вывести пустой символ.
Это вызывает нарушение логики программы и приводит к появлению «SYNTAX ERROR» даже при полном отсутствии синтаксических ошибок в самом коде. Интересно, что смежные символы, имеющие PETSCII-коды SHIFT-J (0xCA), SHIFT-K (0xCB), SHIFT-M (0xCD) и SHIFT-N (0xCE), взаимосвязаны с отдельными ключевыми словами — MID$, GO, FOR и NEXT соответственно. Поэтому LIST их успешно интерпретирует и выводит корректно, что создаёт иллюзию осмысленной работы, в то время как SHIFT-L остаётся «каменным камнем преткновения». На самом деле ZERO-BYTE, который символизирует конец списка ключевых слов в памяти, совпадает именно с кодом, соответствующим SHIFT-L, и этот факт побуждает LIST ошибочно считать курсор выхода за пределы массива со всеми вытекающими последствиями. Решить проблему оказалось непросто, учитывая очень ограниченный объём памяти и необходимость поддерживать обратную совместимость с существующими программами.
В версиях BASIC старше 1.0 появились расширенные списки ключевых слов, но при этом подход к возврату из токенов в текст остался практически таким же, вызывая схожие проблемы. Более новые версии, например BASIC 4.0, расширяют таблицу ключевых слов вплоть до предела адресного пространства, и при попытках анализа выходят за её границы, сталкиваясь с таблицей сообщений об ошибках в ROM, что приводит к выводу новых загадочных названий и фраз вместо ожидаемого текста. Появление в C64 и VIC-20 возможности переопределять функцию UN-CRUNCH косвенно дало шанс исправить этот баг — к примеру, дополнительной проверкой на токен REM (0x8F), при распознании которого LIST мог бы сразу вывести все следующие символы без попытки токенизации.
Патч для такой ситуации включал бы прямой вывод букв R, E, M с последующим «проходом» по строке до её конца. В теоретической модели это позволило бы избежать попыток обращения к маркеру конца списка ключевых слов и, как следствие, получить корректное отображение комментариев. Однако реализация такого патча осложнялась несколькими факторами. В первую очередь, адреса LIST и UN-CRUNCH изменялись в разных ревизиях ROM, что требовало поддерживать разные версии исправлений под разные конфигурации. Во-вторых, базовая архитектура интерпретатора ожидала, что токены всегда находятся в пределах определённых таблиц, и дальнейшая логика программы ориентировалась на это, не имея встроенных средств для мягкого выхода из ошибки.
В итоге, коррекция требовала более глубокой переработки ROM, которую в массовом порядке осуществить не удалось до появления эмуляторов и пользовательских расширений. Еще одной интересной особенностью LIST является его работа с внутренними указателями на строки программного кода. Строки запоминаются в памяти с указателями на адреса следующей строки, и именно эти ссылки LIST использует для перемещения по программе. При этом его логика список строчек трактует строго как связанный список. В то же время сам интерпретатор BASIC, исполняющий код, опирается на линейную последовательность с некоторыми оптимизациями, не всегда учитывающими эти ссылки.
Это противоречие можно использовать как трюк — «скрыть» определённые строки от вывода LIST, изменив указатели так, чтобы LIST пропускал определённые части программы, однако при этом исполнятся они будут как обычно. Задав указатель ссылки одной строки напрямую на строку с номером, следующей за скрываемой, можно, фактически, убрать скрытый кусок кода из вывода LIST, но не из потока исполнения. Это использовалось в качестве своеобразного средства защиты от просмотра уязвимой части программного кода, однако в момент повторной загрузки или правки код обычно автоматически перестраивает указатели и программа снова становится полностью читаемой. Таким образом, команда LIST в Commodore BASIC демонстрирует сразу несколько занимательных технических деталей и исторических особенностей эры восьмибитных компьютеров — действительно, её приключения достойны звания REM-арного анекдота в мире программирования. С одной стороны, несовершенство данной команды порой вызывает неприятные ошибки и сбои, с другой — она даёт возможность заглянуть во внутреннее устройство интерпретатора, понять самую суть работы токенизации и раскодирования в BASIC, а также получить пищу для творческого подхода к модификации и расширению функционала.
Сегодня, когда Commodore 64 и подобные ему компьютеры существуют в основном в виде эмуляторов, описанные проблемы LIST позволяют разработчикам и энтузиастам глубже понять работу классики, создавать патчи и дополнительные инструменты для корректного отображения и обработки кода BASIC. Более того, постановка вопроса о возможности и невозможности сделать LIST по-настоящему обратимой функцией CRUNCH наталкивает нас на размышления о компромиссах между эффективностью хранения, простотой реализации и удобством пользователя — дилеммах, которые остаются актуальными и для современных программных средств. Команда LIST по-прежнему остаётся живой историей из мира компьютерного программирования, которая учит нас внимательности к деталям, уважению к ранним разработчикам и вдохновляет к поиску умных решений там, где на первый взгляд кажется, что выхода нет.