В 2002 году Алекс А. Степанов проводит лекцию в адоби: STL and Its Design Principles - где упоминает кейворд concept (там прям целый слайд про концепты). В 2009 году в свет выходит книга Elements of programming (Stepanov, McJones) и где по-моему нет ни одного алгоритма без концептов. В 2011 новый стандарт языка с++11, где в отложенных (прям очень жаль) фичах фигурируют концепты. В 2014 мир видит творенье Страуструпа - Tour of C++, где глава 5.4 названа Concepts and Generic Programming, хотя язык не поддерживает кейворд concept. Годом ранее, в 2013, Андрю Саттон публикует бумагу Concepts Lite. В стандарте с++14 появляется новая фича digit separators, но нет концептов. В 2017 на реддите обсуждают c++17 и предлагают отдохнуть еще три года.

Через три года сочувствующий html разработчик захочет попробовать язык с чистого листа, заюзав и концепты, и модули. Модули привносят важное свойство - уменьшение времени компиляции. Идея в шаг за шагом написании небольших компонентов и комбинировании их после в нечто осознаное, например, линтер.

Первым делом добавим функцию print:

// guard for non-printable types
template<typename T, typename... Ts>
void print(const T& x, const Ts&...) {
  auto msg = "\n[error] not supported print operation for type ";
  std::cout << msg << typeid(x).name() << "\n";
}

// print first arg and continue with rest
template<Printable T, typename... Ts>
void print(const T& x, const Ts&... rest) { ... }

Компонент для матчинга строки нашего линтера (javascript кода) будет выглядить как-то так:

// checking for pair of quotes, while skipping whats inside them
template<InputIterator I>
constexpr result_t lint(...) {
    auto pred = [](char ch){ return ch == '\'' || ch == '"'; };
    ...
}

Так как алгоритм выше constexpr, запускаем тесты на каждую! компиляцию модуля:

constexpr auto test(const std::string_view str) {
   auto p = std::make_pair(std::begin(str), std::end(str));
   return !lint(..., p) && p.first == p.second;
}

static_assert(test("'foo'"), "single quoted string test");
static_assert(test("'\\' quoted'"), "string with escaped single quote test");

У линтера, кажется, вырисовывается интерфейс:

result_t lint(..., /* pairs of iterators */) { }

Тут идея вполне очевидная: много компонентов с одинаковым интерфейсом. Можем решить ее с помощью наследования, а можем позаимствовать идею из видео Better Code: Runtime Polymorphism (Sean Parent) -- в двух словах, тот же самый полиморфизм, но удобнее (e.g. extension of built-in types). Продолжим:

struct keyword_t { std::size_t id; };
template<InputIterator I>
constexpr result_t lint(const keyword_t& x, matcher<I>& m) { ... }

// models forall relation
// if at least one fails -> all fail
template <InputIterator I>
constexpr result_t lint(const std::vector<linter>& xs, matcher<I>& m) { ... }

Первый аргумент отвечает за тип компонента, второй -- обертка над парой итераторов. Обертка исключительно для читаемости:

template <InputIterator I> 
struct matcher {
    I first; 
    I last;
    constexpr matcher(I first, I last) : first{first}, last{last} {}

    // advance 'first' while predicate is hold
    template <Predicate<value_t> Op>
    constexpr void skip(Op pred) {
        first = std::find_if_not(first, last, std::move(pred));
    }
    ...

Есть несколько вариантов для типа возвращаемого значения result_t. У линтера на самом деле простая задача и, следовательно, простой результат действия: или мы нашли несоответствие, или все ок. По идее, выбор падает на bool, но на практике интересен линтер чуть-чуть поумнее, например std::optional\<std::size_t\>. Если все ок, возвращаем пустой опшионал, в противном случае - количество проделанных шагов до несоответствия.

Если линтер нашел недействительный токен или не нашел искомый, кидаем эксепшн:

struct panic_t { };
template <InputIterator I>
constexpr result_t lint(const panic_t&, matcher<I>& m) {
    return 0
        ? result_t{}
        : throw std::domain_error("\n err near \n-> " + m.debug_info());
}

struct iff_t { linter a; linter b; };
// models relation between pair of linters
// if linter 'a' not fails, linter 'b' must not fail
template <InputIterator I>
constexpr result_t lint(const iff_t& x, matcher<I>& m) {
    auto lhs = lint(x.a, m);
    if (lhs.has_value()) return lhs;
    auto rhs = lint(x.b, m);
    return rhs.has_value() ? lint(panic_t{}, m) : result_t{};
}

Компоненты нашего линтера захочется композировать (или компоновать), поэтому пару/тройку связующих:

struct or_t { linter a; linter b; };
template <InputIterator I>
constexpr result_t lint(const or_t& x, matcher<I>& m) {
    return lint(x.a, m) ? lint(x.b, m) : result_t{};
}

struct optional_t { linter a; };
template <InputIterator I>
constexpr result_t lint(const optional_t& x, matcher<I>& m) {
    lint(x.a, m);
    return {};
}

// models forall relation
// if at least one fails -> all fail
template <InputIterator I>
constexpr result_t lint(const std::vector<linter>& xs, matcher<I>& m) {
    for (const auto& x : xs) {
        auto r = lint(x, m);
        if (r.has_value()) return r;
    }
    return {};
}

Теперь собственно к разборке ("линтингу") javascript кода. Хедеры (или импорты) javascript файлов выглядят примерно так:

import * as foo from "../foo"
import bar, { boo, kung as fu, mu } from "../bar";

Грамматика хедеров тут: https://262.ecma-international.org/6.0/#sec-imports .
Компонент для хедеров import_t сигнатурой будет очень похож на все предыдушие компоненты:

struct import_t{};
template<InputIterator I>
result_t lint(const import_t&, matcher<I>& m) { ... }

Внутри хотя будет интереснее, так как грамматику можно описать композицией компонентов (тут прям нужно добавить, что описание декларативное без всяких while и for-loop :)

// match: * as foo
auto namespace_import = iff_t{
    char_t{context::Chars::Star},
    std::vector<linter>{
        keyword_t{context::Keywords::As},
        identifier_t{}
    }
};

// match: { foo, bar as boo, ... }
auto named_import = iff_t{
    char_t{context::Chars::LeftCurlyBracket},
    std::vector<linter>{
        optional_t{import_var_list_t{}},
        char_t{context::Chars::RightCurlyBracket},
    }
};

// match: foo | foo, * as bar | foo, { bar... }
auto imported_default_binding = std::vector<linter>{
    identifier_t{},
    optional_t {
        iff_t {
            char_t{context::Chars::Comma},
            or_t{
                namespace_import,
                named_import
            }
        }
    }
};

// and combining all togather 
auto rule = iff_t{
    keyword_t{ context::Keywords::Import },
    or_t{
        std::vector<linter>{
            string_t{},
            optional_t{char_t{context::Chars::Semicolon}},
        },
        std::vector<linter>{
            or3_t {
                namespace_import,
                named_import,
                imported_default_binding,
            },
            keyword_t{context::Keywords::From},
            string_t{},
            optional_t{char_t{context::Chars::Semicolon}},
        }
    }
};

Описание import_t моделирует граф. Если в графах грамматики встречается цикл, уносим его в алгоритм, другими словами, для удобства добиваемся direct acyclic graph. Примером цикла есть токен import_var_list_t выше, который матчит { foo, bar, ...}, алгоритм которого будет петля, которую тоже можно тестировать на каждую компиляцию.

Как-то не хочется заключение писать, так как линтер еще ой как далек от завершения. Хочется вспомнить книгу JavaScript: The Good Parts (Douglas Crockford, 8 May 2008), которая, как видно из названия, предлагает юзать только "хороший" сабсет грамматики из "яваскрипта". На сегодняшний день хороший сабсет будет еще меньше (например, for-loop, while -- нежелательны; var, let запрещены), то есть задача создания хорошего и быстрого линтера кажется не такая уж непосильная.

Комментарии (6)


  1. Kelbon
    25.06.2022 15:23
    +6

    А причем тут концепты и модули-то?


    1. xyli0o Автор
      25.06.2022 15:49
      -1

      Без них было бы совсем печально :(


  1. vadimr
    25.06.2022 15:33

    .


  1. 1vanK
    25.06.2022 16:03
    +9

    Как связаны заголовок и содержимое статьи?


  1. art1z
    27.06.2022 20:25

    C var понятно, ему место в истории, а let то чем не угодил? Все переводить на тру функциональщину и immutable.js? Так все равно останутся безнаказанно мутируемые аргументы фукций


    1. xyli0o Автор
      27.06.2022 21:17

      :) Мутации легко добиться и с "конст": const d = {k: 1}; d.k = 2 но это в локальном скоупе функцию. А вот аргументы функции лучше не мутировать -- этот скоуп на уровень выше.

      Кейворд let он как бы режет глаза :) Наиболее частое использование let на моей памяти - let x; try { x = guessNumber() } catch() { x = 42 } ... -- тут как бы не юзать let и вынести try в функцию.