15-го марта я опубликовал пост о скетч-программировании и пообещал написать статью. Даже в этой статье было упоминание. К понедельнику, но к какому — не уточнил :) Я честно попытался, и даже черновик есть, но, учитывая мою лень — здравствуйте. Сейчас я не помню, как запустить локально проект разработки расширения под VS Code. Думаю, самое время написать статью, пока я вообще обо всём этом не забыл.

Началось всё с очередной бредовой идеи попробовать написать свой язык программирования, или компилятор, или транспайлер… Так бывает, когда надоедают формочки и кнопочки.

Писать компилятор с нуля довольно скучно. Я как будто бы уже знаю итог, и передо мной простая рутинная работа в виде парсинга, построения AST-дерева и так далее. Быстро это всё мне надоело.
Я начинаю вспоминать про Babel, немного помню про его внутрянку, мне знаком парсер Babylon. Теперь это уже @babel/parser.
Грубо говоря, эта штука парсит код и выдаёт AST. Мы можем расширять JS фичами из нового стандарта, которые ещё не поддерживаются всеми браузерами. Кто-то называет его компилятором, кто-то транспайлером — тут уже, такое ощущение, дело вкуса.

Приблизительно в это время, на внутренних митапах я рассказываю про "Новые интересные фрейворки", про свой стек Adonis, Edge, Alpine, про сигналы, Svelte, Solid, HTMX, и про то почему React бездарно устаревший фреймворк, который начиная с 2013 не добился первоначальных целей и обещаний (да это я накинул, уже какой раз обещаю еще одну статью на эту тему). Не смотря, на то, что React это не тот фреймворк на котором бы я хотел писать, мне приходится это делать как минимум в пет проектах. А так же понятно, на нем написано огромное количества кода — я с ним буду сталкиваться еще очень долго. В таких вещах где виртуал дом не просто работает с версткой, а выдает нам допустим нативные компоненты или просто рендерит во, что-то другое. Тут у него за счет схожести с веб разработкой есть ряд преймуществ.

Но так или иначе, объявлять две переменных ради одной реактивной используя такую-то функцию - мне порядком надоело. Для себя я бы хотел видоизменить способ написания этих компонентов. И как можно это сделать? Написать очередной плагин под Babel? Написать еще один CoffeScript? Я хочу свободный синтаксис, и простую реализацию.

LLM

Какой бы крутой парсер для AST ни был, всё сводится к тому, что синтаксисы многих языков ограничены этим самым синтаксисом. Вам, по сути, нужен алгоритм, который сможет чётко и безукоризненно прочитать исходный код, чтобы его нормально распарсить. Кавычки, фигурные скобки или отступы — и вот это всё. Самый шик — последняя строчка в функции по умолчанию return. Всё буквально декларативно, как конфиг, что опять приводит меня к довольно скучной теме с AST.

Но можно ли попросить LLM трансформировать один текст в другой по примерам? Даём ей описание нашего синтаксиса, а на выходе получаем, допустим, C++ или ассемблер. Который дальше уже можно спокойно исполнять.

Я хочу создать новый язык программирования.

Давай объявление функции будет выглядеть так
имяФукнции аргументы... тело
Вот пример кода
sumValues args
sum args

переведи в js

function sumValues(args: number[]): number {
  return args.reduce((acc, x) => acc + x, 0);
}

Появление LLM меняет разработку ПО примерно в той же степени, что и переход от ассемблера к первым языкам высокого уровня. Дальнейшее развитие языков и фреймворков повышало уровень абстракции и продуктивность, но не оказывало столь радикального влияния на саму природу программирования. LLM же оказывают именно такое влияние, с той разницей, что они не просто повышают уровень абстракции, но и заставляют нас переосмысливать, что значит программировать с использованием недетерминированных инструментов.

Высокоуровневые языки программирования ввели радикально новый уровень абстракции. Работая с ассемблером, мы думаем о наборе инструкций конкретной машины. Нам приходится разбираться, как выполнить даже простые действия, перемещая данные в нужные регистры, чтобы вызвать конкретные операции. Языки высокого уровня позволили нам мыслить в терминах последовательностей операторов, условных конструкций. Поначалу было довольно много ограничений. Например, только одно выражение в условном if. Постепенно языки развивались, давая нам всё больше абстракции. Появились нестрого типизированные языки, которым мы переизобрели типизацию, тратим человеко-часы и обсуждаем, какой нам нужно задать тип, и просто лепим any, потому что это уже какой-то бред.

Но всё это изменения и улучшения по сути — горизонтальный лифт. В каком-то языке больше сахара, в каком-то меньше. LLM же дают нам гигантский скачок в абстракции. Конечно, не без оговорок и минусов. Когда мы писали функцию, мы могли скомпилировать её сто раз — и результат всегда будет один и тот же, с теми же багами или без них. Большие языковые модели вводят недетерминированную абстракцию, так что мы не можем просто сохранить свои промпты в git и быть уверенными, что каждый раз получим одно и то же поведение.

Агент для фронтовых задач, который сам ходит в Jira, Figma за дизайном и открывает PR.
Агент для фронтовых задач, который сам ходит в Jira, Figma за дизайном и открывает PR.

Абстракция повышается, и теперь уже многие говорят об агентной абстракции, когда всё делается агентами: от создания проекта до его деплоя и двигания тасочек в Jira. Это отдельная большая тема. Сегодня я говорю о своего рода переходном подходе — концепте, который, вероятно, займёт своё место между обычным программированием и вайбкодингом (когда вы полностью отдаёте написание кода модели или используете её как автокомплит).

Пример с React

Как я уже и сказал, одна из вещей, которая мне не нравится в React, — это его костлявость по части работы с ним. Чтобы объявить реактивную переменную, вам нужно написать такое:

const [counter, setCounter] = useState<number>(0);

Мне бы хотелось, что бы это выглядело как-то так:

state counter number = 0;

Можно и сократить до:

counter = 0

Но для того, чтобы сохранить читаемость, чтобы нам не приходилось при беглом чтении гадать, какая именно переменная перед нами, лучше, конечно, первый вариант. LLM на самом деле поймёт из контекста JSX, что именно нам нужно. state counter больше нужен нам, кожаным мешкам. Но не исключается и уменьшение галлюцинаций у LLM за счёт большего контекста.

Простенький компонент целиком:

// @sketch:reactComponent
// @ext:tsx

Component Count

props add = 0
state count = 0

effect {
    console.log("Component mounted");
    
    cleanup {
        console.log("Cleanup");
    }
}

<div onclick="count += add"> Will add { add } </div>
<div>
    Current  count: { count }
</div>

Превращается в:

import React, { useState, useEffect } from 'react';

interface Props {
    add?: number;
}

const CountComponent: React.FC<Props> = ({ add = 0 }) => {
    const [count, setCount] = useState<number>(0);

    useEffect(() => {
        console.log("Component mounted");

        return () => {
            console.log("Cleanup");
        };
    }, []);

    const handleClick = () => {
        setCount((prev: number) => prev + add);
    };

    return (
        <div>
            <div onClick={handleClick}> Will add {add} </div>
            <div>
                Current count: {count}
            </div>
        </div>
    );
};

export default CountComponent;

Мы упростили определение компонента, стейта, пропсов, использование useEffect (с колбэком анмаунта), вёрстку в JSX (count += add — это буквально нативный JS) и сам модуль (импорт зависимостей, экспорт). При этом читаемость стала куда лучше. А самое главное — стучать по клавиатуре нужно меньше. Не говоря уже о том, что типизация на TS также присутствует.

Данный подход применим к любому стеку, языку, и на самом деле про LLM-транспайлеры уже есть научные работы. (например про кодинг под квантовый компьютер) Как я сказал ранее, один из минусов — недетерминированность и отсутствие гарантии, что сгенерится какой-то мусор. Но тут вы не просите его без дизайна сверстать сайт. Этот псевдокод, или скетч, как я его называю, по сути — хороший контекст. Пока я с этим всем игрался, каких-либо проблем замечено не было.

Из других минусов — это, конечно же, подсветка синтаксиса: нужно делать какие-то плагины под это. А также сам промпт с кодом занимает некоторое время. Поэтому это, конечно, больше концепт того, что может быть, когда LLM станет доступнее.
Неочевидные плюшки от автокомплита в том же VS Code в том, что он адаптируется к синтаксису и может даже подсказывать, и поверх этого можно вайбкодить ))

Плагин под VS Code

У плагина есть баги, например конфиг файл не перечитывается. Это опенсорс, welcome покомитить.

Для того чтобы всё это хоть как-то было полезно и имело место быть в нашем инструментарии, нам нужна эта функциональность прямо в редакторе. Не будем же мы ходить в чат с LLM и просить её преобразовать код на каждую правку.

Для этих целей пришлось написать плагин под VS Code (можно установить из стора), который при сохранении ходит в ChatGPT по API и возвращает валидный код. (Так забавно — сижу, читаю инструкцию к нему, потому что уже забыл, как им пользоваться.)

Создание плагина — довольно увлекательная штука, но по сути инструментарий под это довольно простой. Вы буквально запускаете инстанс VS Code с вашим плагином.

npm install --global yo generator-code

# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Initialize a git repository? Y
# ? Which bundler to use? unbundled
# ? Which package manager to use? npm

# ? Do you want to open the new folder with Visual Studio Code? Open with `code`

Детальная анатомия плагина и вот это всё выходит за рамки поста. По сути, самое главное — это инициализация проекта и получение валидного кода по Ctrl+S (сохранение).

Вы можете спокойно добавлять любые команды в свой плагин:

 "commands": [
      {
        "command": "sketch-programming--llm-transpiler.initialize",
        "title": "Sketch-programming: Initialize"
      },
      {
        "command": "sketch-programming--llm-transpiler.currentRoot",
        "title": "Sketch-programming: Show current root directory"
      },
      {
        "command": "sketch-programming--llm-transpiler.create",
        "title": "Sketch-programming: Create assistant and vector store"
      },
      {
        "command": "sketch-programming--llm-transpiler.upload",
        "title": "Sketch-programming: Upload all sketches"
      }
    ]

Как работает плагин

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

Первое — это инициализация проекта.

В результате мы получаем папку sketch, в которой есть файлы, описывающие наш синтаксис, и зеркальная структура в src, которая будет выгружаться в проект в виде валидного кода. На скриншоте — инициализация в пустой папке; более реальный юзкейс — это инициализация в каком-то проекте. Главное, что всё из папки src будет сохраняться в root/src воркспейса.

Сейчас плагин работает только с OpenAI и использует API. Для работы с ним нам необходим ключ. Используются ассистенты и векторное хранилище.

Для этого у вас должен быть проект и API-ключ.

Объявление ассистента по дефолту выглядит так (хардкод в плагине), это можно настроить в sketch.config.js (там же имя проекта, ключ):

{
        "name": name,
        "description": 'Sketch assistant for transpiling metacode to actual code.',
        "model": "gpt-4o",
        "instructions": "You are a transpiler, you get file with @sketch:sketch_name tag in the content.\nYou have files describing how to transpile metacode to actual code. \nAnswer the code in the json field \"transpiledCode\" without any additional text. Do not wrap code in any formatting.",
        "tools": [
            {
                "type": "code_interpreter"
            },
            {
                "type": "file_search",
                "file_search": {
                    "ranking_options": {
                        "ranker": "default_2024_08_21",
                        "score_threshold": 0
                    }
                }
            }
        ],
        "top_p": 1,
        "temperature": 1,
        "tool_resources": {
            "file_search": {
                "vector_store_ids": [
                    vectorStoreId
                ]
            },
            "code_interpreter": {
                "file_ids": []
            }
        },
        "response_format": {
            "type": "json_schema",
            "json_schema": {
                "name": "transpiled_code",
                "description": 'The response should be a JSON object with a single property "transpiled_code" of type string. This property should contain the transpiled code.',
                "schema": {
                    "type": "object",
                    "properties": {
                        "transpiled_code": {
                            "type": "string",
                            "description": "The code after transpilation process."
                        }
                    },
                    "required": [
                        "transpiled_code"
                    ],
                    "additionalProperties": false
                },
                "strict": true
            }
        }
    }
}
openAIApiKey: '[key]',
projectId: 'sketch',
assistanName: 'Morty',
vectorStoreName: 'buhgalteria',

Мы говорим, что создан тыпередать масло: транспайлер, твоя задача — преобразовать код. Для этого у тебя будет инпут, и, пожалуйста, используй code_interpreter и file_search для поиска описания. Верни в виде { transpiled_code: blabla }.

Вектор-стор — это своего рода хранилище, которое ассистент (LLM) может использовать в качестве контекста при выполнении промпта. Например, если у вас интернет-магазин, а вы строите чат-бота, который мог бы рекомендовать товары, вы загружаете файл (CSV с товарами) в базу: это дело превращается в вектор-стор, и ассистент может с этим работать.

В нашем же случае это будут MD-файлы, описывающие наш синтаксис (sketch:reactComponent):

sketch:reactComponent
sketch:reactComponent

С каждым запросом мы не будем отправлять и файл, и описание того, как с ним работать. Описание у ассистента уже будет.

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

// @sketch:reactComponent - имя скетча согласно которого будет преобразование

С помощью Sketch-programming: Create assistant and vector store создаётся ассистент и вектор-стор. С помощью Sketch-programming: Upload all sketches загружаются скетчи.

В дашборде мы увидим следующую картину:

Ассистент с тулами
Ассистент с тулами
Вектор стор с файлом скетча
Вектор стор с файлом скетча

Чтобы немного оживить наш пример, давайте в текущей директории создадим React-проект:

npm create vite@latest .

Теперь мы можем писать код в sketch, и зеркально, при сохранении, будем получать файлы в корне проекта.

Пример до и после в редакторе
Пример до и после в редакторе

Можем добавить новый скетч — пусть это будет CSS. Можно придумывать самим, а можно попросить ChatGPT:

Что напридумывал ChatGPT:

Полный вывод ChatGPT

Вот набор гипотетических улучшений CSS в том же стиле — лаконичный синтаксис → эквивалент текущему CSS.


2. Nested Selectors (без & и лишнего шума)

Simplified Syntax

.card {
  color: black;

  .title {
    font-size: 20px;
  }
}

Equivalent CSS code

.card {
  color: black;
}

.card .title {
  font-size: 20px;
}

3. Inline Conditions (условия прямо в CSS)

Simplified Syntax

button {
  background: blue if hover;
}

Equivalent CSS code

button {
  background: blue;
}

button:hover {
  background: blue;
}

4. Media Queries как свойства

Simplified Syntax

.container {
  width: 1200px;
  width@max-768: 100%;
}

Equivalent CSS code

.container {
  width: 1200px;
}

@media (max-width: 768px) {
  .container {
    width: 100%;
  }
}

5. Логические отступы одной строкой

Simplified Syntax

.box {
  padding: 10px 20px vertical;
}

Equivalent CSS code

.box {
  padding-top: 10px;
  padding-bottom: 10px;
  padding-left: 20px;
  padding-right: 20px;
}

6. Авто-центровка как ключевое слово

Simplified Syntax

.modal {
  center: both;
}

Equivalent CSS code

.modal {
  display: flex;
  justify-content: center;
  align-items: center;
}

7. Цветовые модификаторы без функций

Simplified Syntax

button {
  background: blue +20% brightness;
}

Equivalent CSS code

button {
  background: color-mix(in srgb, blue 80%, white);
}

8. Анимации одной строкой

Simplified Syntax

.fade-in {
  animate: opacity 0→1 300ms ease-out;
}

Equivalent CSS code

.fade-in {
  animation: fade-in 300ms ease-out;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

9. Размеры с контекстом

Simplified Syntax

.sidebar {
  width: 30% of parent;
}

Equivalent CSS code

.sidebar {
  width: 30%;
}

(но явно указывает контекст — не viewport)


10. Состояния как вложенные блоки

Simplified Syntax

input {
  border: 1px solid gray;

  focus {
    border-color: blue;
  }

  disabled {
    opacity: 0.5;
  }
}

Equivalent CSS code

input {
  border: 1px solid gray;
}

input:focus {
  border-color: blue;
}

input:disabled {
  opacity: 0.5;
}

Если хочешь, могу:

  • придумать CSS 2.0 / CSS Next как цельный язык

  • сделать версию в духе Tailwind / utility-first

  • или оформить это как RFC / proposal для W3C

Честно, получается у него очень хорошо — вы только посмотрите на это:

Загружаем скетчи еще раз и пробуем. Мне самому интересно:

Мы видим, что Ассистент нашел необходимый файл с правилами и применил его.

Что дальше?

Пока такой LLM транспайлер отрабатывает в среднем за 5 секунд. Но как концепт работает замечательно. Сам плагин разумеется желательно допилить. Что же касается самого подхода, то он так же может развиваться дальше.

В скетче мы можем прописывать промты с просьбой реализации чего-то, например вместо JSX, написать словами, что мы хотим:

Обратите внимание, как автокомплит помогает писать код скетча.

Такой подход потребует отправлять ассистенту не только скетч, но и готовый код, если он уже есть, иначе есть шанс получать разный результат.

У меня немного флэшбэчит Cucumber — для написания человекопонятных тест-кейсов, которые мапились на код:

Скетч может быть самодокументированным кодом за счёт того, что он проще, будто псевдокод.

Ещё один из вариантов — использовать агента в среде разработки (Cursor, Tabnine, Qodo etc). Попросить его изучить файл и как-то запускать его на save. Либо то, что сейчас становится стандартом: https://agentskills.io/. То есть, так же по умолчанию, работая с вашим проектом, агент будет смотреть на какие-то особенности в этих файлах. А также локальный MCP на базе такого агента. Вариантов, в общем, куча. Но по времени это будет то же самое.

Вывод

Sketch-programming / LLM-транспайлер — это не попытка «убить компиляторы» и не замена классическому программированию. Это переосмысление точки входа в код и эксперименты с новым уровнем абстракции, который стал возможен благодаря LLM.

Если упростить, идея такая:

  • мы перестаём жёстко привязываться к формальному синтаксису языка;

  • вместо этого описываем намерение, структуру и паттерны;

  • а рутинную трансформацию в валидный код делегируем модели.

В отличие от вайбкодинга, где код «появляется из воздуха», sketch-programming остаётся детерминированным по смыслу:

  • у нас есть исходный текст;

  • есть правила трансформации (sketch-описания);

  • есть предсказуемый выходной формат.

Да, LLM недетерминированы. Да, это медленнее, чем локальный транспайлер. Да, нужно решать вопросы кеширования, повторяемости, подсветки синтаксиса и tooling’а. Но всё это инженерные проблемы, а не концептуальные тупики.

Главное, что даёт этот подход:

  • меньше бойлерплейта;

  • меньше механической работы;

  • больше фокуса на идее, а не на ритуалах фреймворка;

  • возможность быстро создавать свои диалекты под конкретный стек и команду.

По сути, это переходный слой между классическим кодом и агентным будущим, где:

  • LLM уже не просто автокомплит,

  • но ещё и не полностью автономный разработчик.

Будет ли это мейнстримом — неизвестно.
Займёт ли нишу — почти наверняка: это я буду использовать, а вы присоединяйтесь.

А самое важное — такие эксперименты двигают нас от вопроса «на каком языке писать?» к вопросу «как мы вообще хотим выражать мысли в коде?».

Репа подхода

Репа плагина

Плагин

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


  1. Sadler
    22.12.2025 00:55

    Component Count

    Отдельной строкой -- плохо, в файле может быть несколько компонентов и подкомпонентов, не все экспортируемые. Концепция блоков export component Count { ... } выглядит для JS куда более нативной.

    Чем больше расширение языка будет совместимо с оригиналом, тем меньше придётся переписывать существующего кода при боевом внедрении. Те же сторонние библиотеки должны продолжать подключаться и работать даже с этим транспайлером.

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

    state counter number = 0;

    Двоеточие, отделяющее имя типа, лучше оставить, читаемость трёх слов через пробел весьма низкая.

    reducer'ы и dispatch со строковыми литералами внутри -- сразу на выброс как невыразительный синтаксис, порождающий ошибки.

    Вложенность в CSS и так вполне неплохо и, главное, привычно работает. Не нужно городить альтернативный синтаксис там, где он не приносит пользы.

    Состояния в CSS как вложенные блоки приводят к неоднозначности, лучше всё-таки иметь какой-то префикс, хоть тот же ":" . Когнитивная нагрузка от этого околонулевая, зато сразу видно, где состояние, а где -- элемент.

    Логично было бы продумать удобную привязку CSS к react-компоненту. Желательно минуя css-in-js, при этом сохраняя гибкость.

    В целом, gpt4o здесь не нужен, локальные модели могут вполне шустро это обрабатывать, особенно если взять миниатюрную модель и натаскать на конкретно эту задачу, как это работает с моделями для автокомплита.


  1. tapeline
    22.12.2025 00:55

    Почему Вам кажется, что недетерминированность и "неповторяемость" — инженерные, а не концептуальные проблемы? Интересно узнать Ваше мнение.