Сама идея
Сижу я, значит, и думаю о Lisp-подобных языках. А тут как понял что не так много Lispов имеют поддержку АТД, а это реально грустно, учитывая то как Я люблю АТД. Они позволяют легко представлять почти любые данные(это всё на самом деле были emunы), строить AST, хранить состяние игр и т.д. Например, этот код
#[derive(Debug)]
enum IpAddr {
V4(u8, u8, u8, u8),
V6(u16, u16, u16, u16, u16, u16, u16, u16),
}
let v4 = IpAddr::V4(127, 0,0,1);
println!("{v4:?}");
// а потом на это можно делать pattern-matching и так далее
match v4 {
IpAddr::V4(127, x, y, z) => println!("локальный"),
IpAddr::V4(g, x, y, z) => println!("generic v4"),
_ => println!("что то другое"),
}
что даёт в результате
V4(127, 0, 0, 1)
локальный
Так что конечно, как любой нормальный человек, я просто решил написать свой собственный игрушечный Lisp с ADT - это было бы реально весёлое упражнение в области программирования в которой у меня нету опыта. О, и раз уж я, видимо, люблю страдать, я решил ограничится только стандартной библиотекой, ради минимализма. И я назвал его Blip, потому что, ну… не знаю, просто захотелось так назвать.
Начало
Но в этот раз мне не хотелось писать свой эксперимент на Rust, потому что у меня уже было 3 (этот сайт, hinoirisetr и shantiнеопубликован) активных проекта на нём. Я хотел попробовать что-то новое. Мой выбор упал на Odin, потому что он выглядел подходящим для задачи. У него ручное управление памятью и приятный синтаксис. В целом, я бы описал его как смесь Jai, Go и C. Да, он всё ещё не такой развитый как C или Rust или Zig, но вообще важна ли развитость для игрушечных личных проектов(нет). Так что я написал небольшую базовую реализацию на Odin, и она работала весьма хорошо пока я не… Не запустила её с valgrind
и увидела что она утекала память, и мой окисленный мозг сказал “О нет, это недопустимо!!” - и конечно вместо того чтобы просто исправить Odin версию я просто решила “а почему бы не переписать Blip на другом языке”. Немного подумав, я выбрала OCaml как язык для переписи, и решила “какая же прекрасная идея, OCaml это именно то, что мне было нужно, что то между Rust и Haskell(когда я пытался выучить Haskell мой мозг свернулся в трубочку)”, да и я хотел побаловаться с ФП побольше, так что перепись на OCaml.
OCaml
OCaml казался приятным… Но мой LSP сервер отказался работать без dune
, а это очень неприятно, поскольку я хотел использовать простой Makefile
(для минимализма). Хм, ну ладно, я предпочитаю удобство перед минимализмом, так что я перешёл на dune
. Увидел что есть папка для тестов, и решил написать несколько… Только чтобы понять что мне нужна библиотека, просто чтобы писать ТЕСТЫ?!? Ваша билд система создала директорию для тестов, так почему вы не представляете утилиты для них? У меня 0 оправданий за то, что мне пришлось писать это
let assert_equal name printer expected actual : bool =
let green = "\027[32m" in
let red = "\027[31m" in
let reset = "\027[0m" in
if expected = actual then (
Printf.printf "%s[PASS]%s %s\n" green reset name;
true)
else (
Printf.printf "%s[FAIL]%s %s: expected %s but got %s\n" red reset name
(printer expected) (printer actual);
false)
Да, возможно это моя вина за то что я не пользуюсь зависимостями, но обычно ожидаешь что современный язык будет предлагать утилиты для тестов… Ну или хотя-бы не пихать директорию tests/
в лицо. О, а я уже рассказала про загадочные ошибки компилятора? Ну, у OCaml и они есть. Компилятор зачастую показывает ошибку на линию выше или ниже чем она реально находится и говорит что-то о “invalid syntax”. Возможно я просто плохо знаю функциональное программирование и пишу плохой код, но почему мне надо оборачивать половину match
кейсов в блок ()
?… На этот момент, Blip уже был не просто Lispом - он был целым цирком переписей. Каждый язык на который я смотел выглядел как подходящая опция: Zig, D, C, Vale, Hare, исправить Odin версию(лол, нет), C3, Nim.
Zig
Zig казался естественным следующим шагом: Я слышала о нем уже весьма давно, и он определенно подходил для этого проекта с его ручным управлением памятью и всем таким. О, и, конечно, Zigуаны не умолкали, рассказывая о том, насколько крутым является comptime
. Но, по иронии судьбы, в итоге я использовал его всего 4 раза в ~860 строках кода, и все разы для реализации std.fmt.Format
. Так что, да, это не совсем та мега-макросистема, которую я ожидал. Но, тем не менее, мне очень понравился Zig. Его модель управления памятью упростила использование арен, в нем есть хороший паттерн-матчинг для тегированных объединений(tagged unionов). В целом, я была доволена. Но на полпути к переписыванию интерпретатора в Zig мне пришла в голову идея, что я должен протестировать и сравнить все реализации. Но тестировать только 2-3 имплементации не так весело. Поэтому я решил превратить Blip из проекта по созданию языка программирования в эксперимент: “реализовать один и тот же крошечный интерпретатор на множестве языков, а затем сравнить эргономику и производительность”. С новыми целями в голове, я объединил Zig и OCaml в одну ветку(коммит), доработал реализацию на Zig и начал работать над утилитой бенчмаркинга (по иронии судьбы, написанной на Rust).
С
Старый (я использовал C99), минималистичный, портативный. Так я бы описала C. А из-за его небольшого размера я снял ограничение “только stdlib” и использовал nob.h, в конце концов, я фанат Tsoding, лол. И я слишком нуб, чтобы писать свои собственные динамические массивы/арены и все такое (да, я ленивый, я знаю). На момент публикации я еще не закончил эту реализацию, но уже столкнулся с некоторыми проблемами:
Работа с аренами
В C нет встроенных утилит для использования аллокаторов арены, поэтому использование арены для хранения строк в дереве токенов было неудобным. Поскольку я использую арену для хранения всех строк и идентификаторов, мне также приходится передавать весь массив TokenArray
в функцию token_to_str
, даже если она форматирует только один Token
.
Отсутствие неймспейcинга
Это была первая проблема, которую я заметил. Теперь, вместо нормального неймспейсинга, мне пришлось называть все варианты енама TOKEN_LPAREN
вместо простого LPAREN
, потому что в противном случае я получал ошибку о коллизиях.
Необходимость использования .h
Да, это небольшая проблема, но она все же ухудшает впечатления от работы.
Настройки компилятора по умолчанию
Чтобы получить действительно полезные ошибки/предупреждения, мне нужно было использовать следующие параметры компилятора: -g -fsanitize=address,undefined -fno-omit-frame-pointer -Wshadow -Wall -Wextra
. Что это такое??? Я понимаю, что C — это СТАРЫЙ язык, но мне это действительно не нравится.
Запланированные следующие шаги
Написать остальные имплементации, после чего я оценю каждую из них на основе опыта разработчика, используя 5 критериев (шкала от 1 до 5, общее количество баллов ≥15/25 для прохождения):
- Ясность синтаксиса
- Билд система
- Опыт дебаггинга
- Стандартная библиотека
- Качество ошибок
Конечно, судьей буду я, так как это мой любительский проект :). Лично я считаю, что это самые важные вещи для языка, и я хочу быть абсолютно уверен, так как окончательным победителем станет язык, который я буду использовать для реализации остальных своих идей для Blip. После отбора языков я добавлю больше функций в каждый из интерпретаторов:
- Функции
- Переменные
- Разветвления
- Операции со списками
- Рекурсия
А ПОТОМ проведу тестирование с помощью утилиты bench, которую я написал (позже я также добавлю к ней тесты на соответствие спецификациям), поскольку, конечно, я предпочитаю, чтобы мой язык имел хорошую производительность. Все языки, которые я планирую использовать, перечислены в README.md, но я приведу более подробные обоснования в следующих частях.
На этом все, но ждите продолжения, где я расскажу о некоторых других языках (Nim, D и еще C) и о прочем.