Меня зовут Степан, я C# профессионал уже более 7 лет на рынке и рассказываю об этом в Telegram каналe StepOne. Иногда мне скучно на работе, потому что перекладывать JSON это слишком просто, даже если микросервисы.

Я отучился на системного программиста-компиляторщика и столкнулся с отсутствием спроса рынка на такие навыки. Но выбрал быть счастливым и написал язык программирования hydrascript, чтобы JSON гонялся даже в докере на макбуке. Решение под катом вас точно удивит!

Дисклеймер

Интерпретатор языка программирования hydrascript создан исключительно с помощью Vanilla C#. Никаких lex, flex, yacc, bison, antlr, llvm и других компиляторных инструментов не было использовано и не планируется

Лор hydrascript

Проект стартовал, как диплом бакалавра "Расширенное Подмножество JavaScript". Я взял небольшую часть стандарта ECMA-262 и дополнил её своим авторским видением. Далее успешно закончил кафедру ИУ-9 Бауманки и решил продолжить проект под кодовым именем "HydraScript".

После починки одного бага появляется несколько других!

Четыре года разработки по вечерам пролетели незаметно. Проект усилил мои компетенции в архитектуре и платформе .NET. Я даже изобрёл новую реализацию паттерна Visitor.

Также, open source разработка в репозитории hydrascript обрела 4 цели:

  1. Частичная реализация JavaScript с объектами и сильной структурной статической типизацией без таких ключевых слов, как: constructorclassinterface

  2. Публичный реверс-инжиниринг современного статического анализа (вывод типов, forward ссылки, runtime ошибки на этапе компиляции и так далее)

  3. Демистификация и фасилитация компиляторного домена через исходный код HydraScript

  4. Сбор понятных решений стандартных компиляторных задач (лексер, парсер, CFG, SSA, DCE и так далее)

Установка

Теперь всё серьёзно. Это больше не просто диплом студента, а полноценный open source - CI/CD на GitHub Actions, семантическое версионирование, стандарты разработки, шаблоны PR, беклог, автоматические релизы и не только.

В рамках релиза бинарник интерпретатора собирается под три платформы в режиме Native AOT, чтобы не требовать зависимостей:

  1. Windows (x64)

  2. macOS (arm64 Apple Silicon)

  3. Linux (x64)

Последний релиз доступен на GitHub по этой ссылке. В качестве альтернативы можно поставить себе HydraScript, как утилиту .NET:

dotnet tool update --global hydrascript

Ссылка на NuGet: https://www.nuget.org/packages/hydrascript

"Киллер" фичи

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

Так что сейчас выделю самое главное. В основном это достижения статического анализа.

Проверка доступа к переменной до инициализации

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

let x = f()
function f() {
    console.log(x)
    return 5
}

Всегда есть значение по умолчанию

Переменные в C# требуют присваивания значения при объявлении. Если я напишу такой код, то получу ошибку Local variable ‘x’ might not be initialized before accessing

int x;
Console.WriteLine(x);

Однако, в HydraScript переменной сразу будет присвоено значение по умолчанию. Для этого интерпретатор должен вывести тип. Если тип не будет выведен, то появится ошибка статического анализа Cannot define type

let x: number
>>> x // 0

let xArr: number[]
>>> xArr // []

let s: string
>>> ~s // 0

Непересекающиеся идентификаторы символов

Символ для компилятора - это сущность в программе, которую можно использовать в исходном коде. Переменная, тип, функция, класс - и так далее. Несмотря на то, что символы могут быть разных типов, ограничения по именованию будут присутствовать. Например, такой код в C# не скомпилируется, хотя очевидно, что идентификатор x имеет разные значения:

x x = new();

x x(x x)
{
    Console.WriteLine(x);
    return x;
}

class x
{
    x x(x x)
    {
        Console.WriteLine(x);
        return x;
    }
}

IDE покажет не одну, а сразу ТРИ ошибки:

Вкладка Problems в JetBrains Rider IDE
Вкладка Problems в JetBrains Rider IDE

Сейчас в HydraScript три типа символов:

  1. VariableSymbol - переменная или объект

  2. TypeSymbol - тип

  3. FunctionSymbol - функция или метод

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

type x = number
let x:x
function x(x:x) {
    >>>x
    x = x + 1
}
x(x)

Перекладываю JSON в Docker на MacBook через HydraScript и CGI интеграцию

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

Довести до ума язык, чтобы на нём можно было создать веб-сервер? Пока что за рамками моих временных ресурсов. Сделать язык CGI-скриптинг совместимым? Проще пареной репы - нужно иметь в языке:

  • API для работы со строками

  • Вывод в stdout

  • Чтение переменных среды ($ENV)

  • Поддержку shebang комментариев, они же решётка строки (#)

Что такое CGI-скриптинг

CGI-скриптинг — это набор команд, которые превращают обычную «неподвижную книжку» сайта в живую страницу: когда сервер получает твой запрос:

  • он собирает нужные данные в переменные окружения env

  • запускает скрипт через интерпретатор по пути в shebang строке. Например, #!/usr/bin/bash

  • скрипт записывает ответ вstdout. Например, Console.WriteLine в C#

  • сервер подхватывает этот поток, превращает его в веб-страницу и сразу показывает тебе на экране

Это если простыми словами. Более сложно, есть официальный стандарт RFC 3875

Или вот статья на хабре, первая в гугле по запросу "habr cgi"

Всё это я добавил в рамках релиза 2.6.0. Далее надо было достать веб-сервер с поддержкой CGI, который можно развернуть в Docker за пару команд. Оказалось, что есть образ httpd, где установлен Apache 2. Осталось докинуть в образ бинарник интерпретатора, который я решил поставить как dotnet tool:

FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine as sdk-build
RUN dotnet tool update -g hydrascript
ENV PATH="/root/.dotnet/tools:${PATH}"

FROM httpd:2.4-alpine
COPY --from=sdk-build /root/.dotnet/tools/ /usr/bin
RUN apk add dotnet10-runtime

Далее собрал образ через docker-compose, докинув конфиг Apache и CGI скрипты. В качестве демонстрации я решил написать программу, которая разбирает QueryString в массив объектов имя-значение. Например, для строки a=1&b=bla&c=false сервер должен отдать JSON вида:

{
  "result": [
    {
      "name": "a",
      "value": "1"
    },
    {
      "name": "b",
      "value": "bla"
    },
    {
      "name": "c",
      "value": "false"
    }
  ]
}

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

uri_parse.cgi
#!/usr/bin/hydrascript

type QueryStringParseResultItem = {
    name: string;
    value: string;
}

type QueryStringParseResult = {
    result: QueryStringParseResultItem[];
}

type QueryStringParser = {
    input: string;
}

function parse(parser: QueryStringParser): QueryStringParseResult {
    const qsLen = ~parser.input
    let i = 0
    let items: QueryStringParseResultItem[]

    let currentName: string, currentValue: string
    let isName = true
    while (i < qsLen) {
        const currentChar = parser.input[i]
        if (currentChar == "&" || i == qsLen - 1) {
            let addittion = i == qsLen - 1 && currentChar != "&" ? currentChar : ""
            items = items ++ [{name: currentName; value: currentValue + addittion;}]
            currentName = ""
            currentValue = ""
            isName = true
        } else if (currentChar == "=") {
            isName = false
        } else {
            if (isName) {
                currentName = currentName + currentChar
            } else {
                currentValue = currentValue + currentChar
            }
        }
        i = i + 1
    }

    return {
        result: items;
    }
}

let parser: QueryStringParser = {
    input: $QUERY_STRING;
}

>>> "Content-Type: application/json\n\n"
>>>  parser.parse()
Проверил через Bruno ответ от сервера
Проверил через Bruno ответ от сервера

Итоги

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

Если вы тоже устали от корпоративного булшита и недостатка rocket science, то поставьте звезду репозиторию GitHub hydrascript:

Ещё я веду Telegram канал StepOne, куда выкладываю много интересного контента о программировании на C#, даю карьерные советы, рассказываю истории из личного опыта и раскрываю все тайны IT-индустрии!