open-bsl — интерпретатор встроенного языка 1С:Предприятия (BSL),
написанный на Rust. Исходный текст проходит обычный для интерпретатора
конвейер: разбор, семантический анализ, компиляцию в регистровый байт-код и
исполнение в виртуальной машине.
Проект находится на ранней стадии разработки. Основные конструкции языка уже работают, однако совместимость с 1С пока неполная, а публичный API крейтов может меняться.
Для сборки нужен Rust с поддержкой редакции 2024.
cargo build --workspace
cargo run -p bsl-cli -- path/to/script.bslБез имени файла запускается интерактивная оболочка:
cargo run -p bsl-cliВсё, что идёт после имени скрипта, доступно из кода массивом строк
АргументыКоманднойСтроки (синоним — CommandLineArguments; скобки
необязательны, как в OneScript):
cargo run -p bsl-cli -- path/to/script.bsl арг1 "арг 2"Список параметров командной строки:
cargo run -p bsl-cli -- --helpПеред отправкой изменений рекомендуется выполнить все проверки:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depsКод разделён на крейты. Конвейер интерпретатора и базовый рантайм:
| крейт | назначение |
|---|---|
bsl-syntax |
лексер, парсер, AST и синтаксические диагностики |
bsl-sema |
разрешение имён и семантическое представление программы |
bsl-bytecode |
представление и текстовый формат байт-кода |
bsl-compiler |
кодоген из семантического представления, включая динамические фрагменты |
bsl-vm |
виртуальная машина, исполняющая байт-код интерпретатором |
bsl-rt |
значения BSL, коллекции, встроенные функции и реестр компонентов |
bsl-number |
десятичная арифметика |
bsl-format |
правила преобразования значений в строки |
Платформенные подсистемы вынесены в runtime-компоненты — статически
подключаемые библиотеки над bsl-rt; байт-код записывает, какие из них
нужны программе, и не исполняется без них:
| крейт | подсистема |
|---|---|
bsl-binbuf |
БуферДвоичныхДанных и битовые функции |
bsl-regexp |
регулярные выражения |
bsl-textdoc |
ТекстовыйДокумент |
bsl-json |
ЧтениеJSON/ЗаписьJSON и ПрочитатьJSON/ЗаписатьJSON |
bsl-stream |
потоки и ЧтениеДанных/ЗаписьДанных |
bsl-http |
HTTPСоединение, HTTPЗапрос, HTTPОтвет, прокси и TLS |
bsl-crypto |
ХешированиеДанных и ХешСумма |
bsl-zip |
архивы ZIP |
bsl-pdf |
запись и чтение PDF |
bsl-xml |
ЧтениеXML/ЗаписьXML, DOM, XPath, XSD и XDTO |
bsl-spreadsheet |
ТабличныйДокумент, MXL и XLSX |
Верхний слой — фасад встраивания и то, что построено на нём:
| крейт | назначение |
|---|---|
open-bsl |
фасад встраивания: движок, модуль, состояние, выбор компонентов фичами |
bsl-cli |
запуск файлов, REPL и conformance-тесты |
open-bsl-consumer |
проба замкнутости фасада, в поставку не входит |
Замкнутость проверяется сборкой: open-bsl-consumer зависит только от
open-bsl и пишет свой компонент, разбирает ошибку по фазам и значение по
видам исключительно именами фасада — если для этого понадобится имя из
внутреннего крейта, он перестанет собираться.
Граница между исполнением и фронтендом явная: bsl-vm зависит от
представления bsl-bytecode, но не от парсера и семантики. Для
Выполнить/Вычислить VM передаёт исходник компилятору хоста через обратный вызов
и получает готовый фрагмент байт-кода; штатная реализация и её кэш принадлежат
состоянию open-bsl.
Внешних зависимостей немного: bsl-number использует num-bigint и
num-traits, а базовый bsl-rt кроме bsl-number внешних зависимостей не
имеет. bsl-zip и bsl-spreadsheet используют zip, а bsl-zip и
bsl-pdf — flate2; bsl-regexp использует fancy-regex для исполнения
шаблонов (разбор диалекта и измеренные края остаются своими), bsl-http —
reqwest и tokio, bsl-crypto — крейты семейства digest, а rustyline
нужен только командной строке. Транспорт и его runtime заперты внутри
bsl-http: ни ядро, ни виртуальная машина, ни фасад о них не знают, а
фоновый пул заданий работает на обычных потоках std::thread.
Интерактивная оболочка сохраняет переменные между введёнными фрагментами.
Доступны подсветка синтаксиса и дополнение по Tab. Набор подсказок зависит от
контекста: после точки предлагаются методы, после Новый — типы, в остальных
случаях — ключевые слова, встроенные функции и переменные текущей сессии.
Поиск не зависит от регистра: например, стрн дополняется до СтрНайти.
Повторное нажатие Tab выводит все подходящие варианты.
Цвет можно отключить переменной окружения NO_COLOR=1. Если терминал не
поддерживает сырой режим, REPL переходит к обычному построчному вводу.
Каталог функций, конструкторов, объектов, методов и свойств стандартной сборки генерируется из runtime-дескрипторов:
cargo run -p bsl-cli -- --emit-api-reference
cargo run -p bsl-cli -- --emit-api-reference docs/reference/bsl-api/api.mdПроверяемый снимок и правила для поясняющих страниц находятся в
docs/reference/bsl-api/.
Скомпилированную программу можно вывести в текстовом виде и затем исполнить:
cargo run -p bsl-cli -- --emit-bytecode script.bsl
cargo run -p bsl-cli -- --emit-bytecode script.bsl out.bslc
cargo run -p bsl-cli -- --run-bytecode out.bslcВывод --emit-bytecode — не отладочный отчёт, а входной формат для
--run-bytecode. Его можно изучать и править вручную; всё после ; считается
комментарием.
.handlers 1
0 3 11 12 ; Попытка 3..11 -> обработчик 12
.code 14
0000 LoadConst dst=1 k=0 ; = 5
0002 NewStructure dst=0 shape=0 base=1 count=2 ; поля: цена, количество
0004 GetProp dst=4 obj=5 name=0 ; .цена
Формат удобен при разборе работы компилятора: по нему видно короткое замыкание логических операторов, распределение регистров и выбранные варианты инструкций. Стабильность формата между версиями не гарантируется; номер версии проверяется при загрузке.
Текстовый образ считается недоверенным входом. До первой инструкции VM проверяет цели переходов и обработчики исключений, геометрию кадров и вызовов, согласованность таблиц имён, функций и форм, а также требования к компонентам. Испорченный листинг отвергается как негодный байт-код, а не исполняется частично.
Скрипт можно разложить по нескольким файлам. Директива в начале файла подключает соседний файл общим модулем и даёт ему имя:
//@используй(modules/служба.bsl как Служба)
Сообщить(Служба.Удвоить(Служба.Счётчик));Экспортные функции и переменные модуля видны только с квалификацией по имени, поэтому подключение чужого файла не может незаметно переопределить символ главного скрипта. Тела модулей исполняются до главного скрипта в порядке обхода графа, а переменные модуля живут, пока живёт сессия: изменение, сделанное одной функцией, видно последующим чтениям.
Платформа 1С такой директивы не знает — это расширение open-bsl, поэтому
её семантика задана планами и закреплена фикстурами, а не замером.
Тот же граф собирается из приложения, и тогда точка входа компилируется отдельно от модулей:
let engine = open_bsl::Engine::builder()
.common_module("Служба", "Функция Удвоить(Знач х) Экспорт\n Возврат х * 2;\nКонецФункции")
.build()?;
let module = engine.compile_entry("Возврат Служба.Удвоить(21);")?;Конфигурация целиком сериализуется в текстовый байт-код и загружается обратно
теми же --emit-bytecode и --run-bytecode: образ хранит модули, их
экспортные таблицы и связи между ними.
Поверх каталога общих модулей работает пул фоновых заданий — подмножество серверного механизма 1С, снятое замерами на платформе 8.3.27. Целью задания может быть экспортная процедура или функция общего модуля:
Задание = ФоновыеЗадания.Выполнить("Служба.Посчитать", Параметры, "ключ-1", "Отчёт");
ФоновыеЗадания.ОжидатьЗавершенияВыполнения(Задание, 30);
Если Задание.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно Тогда
Сообщить(Задание.ИнформацияОбОшибке.Описание);
КонецЕсли;Каждое задание получает изолированное состояние на потоке пула. Значения между
сессиями не разделяются: параметры копируются графом, сохраняющим ссылочную
структуру и циклы, а результат возвращается через временное хранилище
(ПоместитьВоВременноеХранилище и ПолучитьИзВременногоХранилища).
Сообщить внутри задания не печатается в общий вывод, а копится в его
истории и читается через ПолучитьСообщенияПользователю.
Пул ленивый: пока заданий нет, потоки не создаются. Занятый пул ставит задание в очередь, а не отказывает; явные лимиты (число заданий, объём параметров, размер истории) проверяются до приёма. Ожидание не занимает поток впустую — задание, ждущее синхронного HTTP-запроса, освобождает свой поток другим заданиям, а вложенное ожидание ребёнка не блокирует пул.
Из приложения пул настраивается при сборке движка и доступен напрямую:
let engine = open_bsl::Engine::builder()
.common_module("Служба", src)
.background_jobs(open_bsl::jobs::BackgroundJobConfig::default())
.build()?;
let runtime = engine.job_runtime()?;Регламентные задания, планировщик, права и пользователи в эту версию не
входят. Устройство механизма описано в
docs/archive/plans/background-jobs.md,
а топология и владение — в
docs/architecture/component-architecture.md.
bsl-cli — один из клиентов интерпретатора, а не обязательная его часть.
Точка входа для приложений — фасад open-bsl. Крейты пока не опубликованы
на crates.io, поэтому подключать их следует по пути или через git:
[dependencies]
open-bsl = { path = "../open-bsl/crates/open-bsl" }По умолчанию фасад включает все runtime-компоненты. Набор можно сузить фичами; скомпилированный модуль объявляет нужные ему компоненты в заголовке и без них честно откажется исполняться:
open-bsl = { path = "../open-bsl/crates/open-bsl",
default-features = false, features = ["json", "xml"] }Минимальный запуск: движок компилирует модуль, состояние его исполняет.
fn run_script(src: &str) -> Result<open_bsl::Value, open_bsl::Error> {
let engine = open_bsl::Engine::builder().build()?;
let module = engine.compile(src)?;
engine.new_state().run(&module)
}run возвращает значение верхнеуровневого оператора Возврат, а при его
отсутствии — Неопределено. У каждой фазы свой вариант open_bsl::Error,
так что приложение может сообщить пользователю, где именно возникла
проблема. Пользовательский вывод следует формировать через
open_bsl::format_value: реализация Display предназначена для отладки
и не воспроизводит форматирование 1С.
Engine хранит неизменяемый реестр компонентов, а изменяемые возможности
конкретного прогона принадлежат State. Построитель состояния принимает
потоки вывода, аргументы запуска, часы, источник случайности, часовой пояс,
файловую систему и сетевой транспорт; два состояния одного движка
настраиваются независимо.
Сообщить и сообщения об ошибках уходят в переданные писатели (любой
Write + 'static; чтобы прочитать перехваченное, передайте обёртку над
разделяемым буфером):
let mut state = engine
.state_builder()
.stdout(std::io::stdout())
.arguments(vec!["аргумент".to_string()])
.build();По умолчанию состояние использует часы, случайность, часовой пояс и файловую
систему процесса. Через StateBuilder::clock, random, zone и files хост
может сделать прогон воспроизводимым или ограничить доступ к окружению.
FileSystem охватывает не только чтение файла целиком, но и метаданные,
каталоги и долгоживущие FileHandle; тем же объектом пользуются ядро и
компоненты JSON, XML, потоков, архивов и документов.
Для одноразовых фрагментов у состояния есть exec и eval; каждый вызов
компилируется отдельным модулем, локальные переменные между вызовами не
сохраняются:
let mut state = engine.new_state();
assert_eq!(state.eval("2 + 2")?.to_string(), "4");Языковые Выполнить и Вычислить, напротив, компилируются относительно
текущего кадра. VM сама фронтенд не вызывает: она обращается к компилятору
состояния, а его кэш повторно использует одинаковые фрагменты в одной области
модуля и не разделяется с другими State.
Скомпилированный модуль сериализуется в переносимый текстовый байт-код и загружается обратно; совместимость записанных в нём компонентов и внутренние инварианты образа проверяются при запуске, до первой инструкции:
let text = module.bytecode()?;
let restored = engine.load_bytecode(&text)?;REPL-подобные сценарии с накоплением локальных переменных между фрагментами
собираются из низкоуровневых частей конвейера
(bsl_sema::resolve_repl_stmts_with_registry →
bsl_compiler::compile_snippet_with_requirements →
bsl_vm::run_repl_chunk_with_registry). Компилятор возвращает единый
SnippetUnit, связывающий чанк с его таблицами имён и форм; рабочий пример —
crates/bsl-cli/src/repl.rs.
У встраивания сейчас есть несколько существенных ограничений:
ValueиModuleиспользуютRcиRefCell, поэтому не реализуютSendиSync. Именно поэтому фоновое задание получает не ссылку на значения вызывающего, а их копию: параметры переносятся между потоками отдельным графом, а обратно результат идёт через временное хранилище;- лимитов времени, памяти и числа инструкций у переднего плана нет; свои бюджеты есть только у пула фоновых заданий;
- файловая система по умолчанию открывает пути процесса; ограничивающий
FileSystemнужно передать явно; - публичные интерфейсы не считаются стабильными.
Недоверенные программы лучше запускать в отдельном процессе с ограничениями по времени, памяти и доступу к файловой системе.
Приложение может добавить движку собственные глобальные функции, конструкторы
и типы объектов — тем же механизмом, каким подключены штатные компоненты.
Компонент описывается статическими таблицами дескрипторов и регистрируется
при сборке движка; после этого исходный код видит его имена наравне со
встроенными, а модуль записывает зависимость от компонента в заголовок
байт-кода. Автору компонента достаточно зависеть от open-bsl: фасад
реэкспортирует типы значений, ошибок, дескрипторов и окружения, которые
встречаются в его публичных контрактах.
Полный пример — тип «Счётчик» с конструктором, методом и свойством:
crates/open-bsl/examples/counter.rs.
Тест counter_example собирает и запускает его, поэтому пример всегда
компилируется и печатает ожидаемый результат; вручную:
cargo run -p open-bsl --example counter.
Правила, которых держатся и штатные компоненты:
- коды конструкторов и функций — стабильные и плотные, с единицы в каждой таблице; по ним связывается байт-код. У методов и свойств кода нет: вызов метода и доступ к свойству связываются по паре «адрес статической таблицы типа, номер имени», а не по коду;
- арность конструкторов и глобальных функций объявляется в дескрипторе и
проверяется на этапе семантики;
FunctionKind::Procedureзапрещает вызов в выражении, как у платформенных процедур. Арность метода хранится вMethodDescriptorи проверяется в рантайме после определения типа получателя, но до вызова обработчика; - имена всюду регистронезависимы, русское написание принято перечислять первым;
- функции, конструкторы, методы и свойства получают
CallContext. Он не раскрывает стек VM, но даёт форматирование, таблицы форм и доступные на этом пути возможности состояния: вывод, часовой пояс, файловую систему, источник случайности, сеть, фоновые задания, временное хранилище и обратный вызов функции модуля. Отсутствующая возможность возвращаетRtError::CapabilityMissing, а не подставляет молчаливую заглушку; - обработчик метода получает сам объект-получатель (
&dyn ObjectProtocol) и возвращается к своему типу черезdowncast_ref; строковые вызовы (ЗаписатьСтроку, загруженный байт-код) обслуживает та же таблица —call_methodпо умолчанию, писать его не нужно; - метод, которому нужно дождаться внешней операции, объявляется
MethodDescriptor::suspendingи возвращаетCallOutcome::Pendingс описанием операции вместо того, чтобы блокировать поток: виртуальная машина паркует исполнение, а поток тем временем занят другой работой. Так устроены синхронные методыHTTPСоединение. Обычные методы объявляютсяMethodDescriptor::newи по-прежнему просто возвращают значение — оборачивает его сам дескриптор. Приостановка пока доступна только встроенным компонентам: перечень операций закрытый, а типыCallOutcome,PendingHostCallиSuspendingMethodCallживут вbsl-rtи фасадом не реэкспортируются — подключаемый черезopen-bslкомпонент такой метод объявить не может; - типы, которые компонент вводит в язык, добавляются через
LibraryDescriptor::with_types: без этогоТипЗнч(объект)назовёт тип по дескриптору, ноТип("Имя")его не найдёт; - имена типа живут в его дескрипторе:
name— имя значения и конструктора,type_display— как тип печатается (Строка(ТипЗнч(...))),type_names— остальные написания, по которым он ищется. У платформы эти три часто расходятся («ЧтениеXML» против «Чтение XML»), поэтому каждое написание измеряется, а не выводится из соседнего; - при неоднозначном написании владелец объявляется через
with_type_aliases. Реестр строит и замораживает проверенный каталог типов: конфликт канонических имён, затенение типа ядра или неразрешённый псевдоним не зависят от порядка регистрации и останавливают сборку движка; - зависимости компонента от других библиотек объявляются через
with_dependenciesс точными версиями и проверяются при сборкеEngine. Сам байт-код отдельно хранит требования к использованным компонентам; они сверяются при связывании до первой инструкции.
Целевая реализация для проекта — платформа 1С, а не OneScript. Там, где поведение языка нельзя надёжно вывести из документации, оно проверяется на реальной платформе.
Непроверенное предположение отмечается в трёх местах:
- комментарием
// НЕ ИЗМЕРЕНО(ОБЛАСТЬ.ВОПРОС)рядом с реализацией; - записью в
crates/bsl-rt/src/open_questions.rs; - примером в
tests/conformance/measure/measure-all.bsl.
Согласованность этих списков проверяет тест
open_questions_registry_matches_source_markers.
Замеры на установленной платформе запускаются так:
./tests/conformance/measure/1c/run-on-1c.sh
cargo run -p bsl-cli -- \
--ingest-measurements tests/conformance/measure/platform.tsvПервый скрипт создаёт временную файловую базу, загружает пробу в модуль формы
конфигурации и запускает её штатным обработчиком старта. Внешний файл через
/Execute не открывается, поэтому предупреждение о небезопасном действии не
требуется обходить. Нужен графический сеанс; пути к платформе и базе и тайм-аут
настраиваются переменными, описанными в комментариях к
run-on-1c.sh.
Команда --ingest-measurements сохраняет результат и выводит найденные
расхождения, но не меняет реализацию автоматически. Эталонные файлы также не
создаются из вывода самого open-bsl.
Фикстуры conformance находятся в tests/conformance/fixtures/. Файл без
соответствующего .expected считается ещё не измеренным и пропускается.
Получить сводку можно командой:
cargo test -p bsl-cli -- --nocaptureЭти результаты получены на платформе 1С и используются как эталоны:
1/3 -> 0,333333333333333333333333333
2/3 -> 0,666666666666666666666666667
10/3 -> 3,333333333333333333333333333
1/268435456 -> 0,000000003725290298461914063
Sqrt(2) -> 1,4142135623731
1.10 * 1.00 -> 1,1
Pow(10, 30) -> 1 000 000 000 000 000 000 000 000 000 000
СтрДлина(Строка(1/3)) -> 29
Строка(1000.5) -> 1 000,5
КодСимвола(разделитель) -> 160
Строка(Истина) -> Да
Строка(Новый Массив) -> Массив
При делении ограничивается число знаков после запятой, а не общее количество значащих цифр. Округление точной половины выполняется вверх. Умножение остаётся точным и может быстро порождать очень большие числа; внутренний предел масштаба нужен для защиты от исчерпания памяти.
Открытые вопросы, включая поведение Sqrt на малых аргументах, перечислены в
open_questions.rs.
Сценарии находятся в каталоге benchmarks. Каждый написан на
BSL, у большинства есть двойник на Python, у части — на Lua. Скрипт измеряет
только время своей основной работы, не включая запуск процесса, и последней
строкой печатает результат в миллисекундах.
./benchmarks/run.sh # все сценарии, медиана пяти запусков
./benchmarks/run.sh "" 7 # все сценарии, семь запусков
./benchmarks/run.sh str_find 9Сравниваются семь колонок: bsl-cli, тот же бинарник с --optimize, Lua, LuaJIT,
Python, OneScript и сама платформа 1С. Отсутствующего в системе исполнителя
скрипт пропускает, а не подставляет за него числа. Колонка 1С меряется не на
месте: платформа поднимается десятки секунд, поэтому её результаты читаются из
снятого отдельным прогоном
benchmarks/1c/combined.platform.txt.
Результаты зависят от компьютера и версий исполнителей, поэтому приведённые в
репозитории числа не стоит воспринимать как универсальный рейтинг. Кроме того,
сравниваемые среды заметно различаются: в BSL используется точная десятичная
арифметика и строки UTF-16, Lua обычно работает с double и байтовыми строками,
а LuaJIT компилирует программу; --optimize применяет проходы к байт-коду до
его интерпретации. Сравнивать таблицу с другой редакцией
кода можно только чередующимся A/B двух бинарников в одном сеансе: между
сеансами машина уходит на десять-пятнадцать процентов, и разница редакций в
таком дрейфе тонет. Подробное описание сценариев и актуальные результаты
находятся в benchmarks/README.md.
Код проекта распространяется на условиях лицензии MIT. Полный текст приведён в файле COPYING. Сторонние компоненты сохраняют собственные лицензии и уведомления об авторстве.
1С:Предприятие — продукт фирмы «1С». Проект open-bsl не связан с фирмой
«1С».