Skip to content
dobpilotPublic

About

Интерпретатор BSL (встроенного языка «1С:Предприятия») на Rust с регистровой виртуальной машиной.

Resources

Contributing

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

open-bsl

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.

REPL

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

Поиск не зависит от регистра: например, стрн дополняется до СтрНайти. Повторное нажатие Tab выводит все подходящие варианты.

Цвет можно отключить переменной окружения NO_COLOR=1. Если терминал не поддерживает сырой режим, REPL переходит к обычному построчному вводу.

Справочник BSL API

Каталог функций, конструкторов, объектов, методов и свойств стандартной сборки генерируется из 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С

Целевая реализация для проекта — платформа 1С, а не OneScript. Там, где поведение языка нельзя надёжно вывести из документации, оно проверяется на реальной платформе.

Непроверенное предположение отмечается в трёх местах:

  1. комментарием // НЕ ИЗМЕРЕНО(ОБЛАСТЬ.ВОПРОС) рядом с реализацией;
  2. записью в crates/bsl-rt/src/open_questions.rs;
  3. примером в 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С».

About

Интерпретатор BSL (встроенного языка «1С:Предприятия») на Rust с регистровой виртуальной машиной.

Resources

Contributing

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages