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

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Начинающие и опытные разработчики, интересующиеся созданием языков программирования
  • Студенты и обучающиеся, желающие углубить свои знания в области компиляторов и интерпретаторов
  • Технические специалисты, занимающиеся проектированием и разработкой специализированных языков для решения конкретных задач

    Создание собственного языка программирования — один из тех проектов, который многие разработчики считают недоступным Олимпом. Но что если я скажу, что построить свой первый язык программирования вполне реально даже без десятилетий опыта? 🚀 Независимо от того, мечтаете ли вы о создании нишевого языка для решения специфических задач или просто хотите глубже понять, как работают инструменты, с которыми вы взаимодействуете каждый день — этот пошаговый гид проведёт вас через весь путь от идеи до функционирующего языка программирования.

Задумывались ли вы, что Java, один из самых популярных языков программирования, тоже когда-то был просто идеей на листе бумаги? Наш Курс Java-разработки от Skypro не только научит вас программировать на Java, но и даст понимание архитектуры языка изнутри. Эти знания станут отличной базой для экспериментов с созданием собственных языковых конструкций и, возможно, даже полноценного языка программирования в будущем!

Основы разработки языка программирования: первые шаги

Создание языка программирования начинается не с кода, а с чёткого определения целей. Это как проектирование здания: сначала нужен архитектурный план, и только потом можно заливать фундамент. 🏗️

Прежде всего, определите, зачем вы создаёте язык. Вот несколько распространённых сценариев:

  • Создание специализированного языка для конкретной предметной области (DSL)
  • Образовательные цели — глубокое изучение компиляторов и интерпретаторов
  • Улучшение существующих языковых парадигм
  • Эксперименты с новыми концепциями программирования

После определения целей необходимо выбрать парадигму для вашего языка. Это фундаментальное решение повлияет на все последующие этапы разработки.

Парадигма Ключевые характеристики Примеры языков Сложность реализации
Императивная Последовательные инструкции, изменяемое состояние C, Pascal Средняя
Функциональная Функции как основные компоненты, неизменяемость Haskell, Lisp Высокая
Объектно-ориентированная Классы, объекты, инкапсуляция Java, C++ Высокая
Логическая Правила и факты, доказательство утверждений Prolog Очень высокая

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

  1. Интерпретатор — программа, которая напрямую выполняет ваш код без преобразования в промежуточный формат. Проще для реализации, но менее эффективен на практике.
  2. Компилятор — транслирует ваш код в другой язык (например, C) или промежуточное представление (байт-код). Сложнее в реализации, но обеспечивает лучшую производительность.

Для первого языка программирования я рекомендую начать с интерпретатора, так как он проще в реализации и отладке.

Алексей Петров, технический архитектор

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

Я решил создать простой предметно-ориентированный язык (DSL), который позволил бы тестировщикам без технического образования писать автоматизированные тесты. Первые шаги были самыми трудными — я долго не мог определиться с синтаксисом и набором функций.

Ключевым моментом стало решение не изобретать велосипед полностью. Я выбрал Python как язык-хост и создал надстройку над ним. Это дало мне всю мощь существующего языка и экосистемы, а я сосредоточился только на создании удобного синтаксиса для специфических задач тестирования.

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

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

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

  • ANTLR или Flex/Bison — генераторы парсеров
  • LLVM — инфраструктура для создания компиляторов
  • Языками высокого уровня (Python, Java) для реализации интерпретатора

Не бойтесь начинать с малого. Ваша первая версия может поддерживать только базовые операции, но это нормально — даже Python и Ruby начинались с минимального набора функций.

Пошаговый план для смены профессии

Проектирование синтаксиса и семантики своего языка

Синтаксис и семантика — это лицо и душа вашего языка программирования. Синтаксис определяет, как программы выглядят визуально, а семантика — что они означают. 📝

При проектировании синтаксиса стоит придерживаться нескольких принципов:

  • Согласованность — похожие концепции должны выражаться похожим образом
  • Читаемость — код должен быть понятен даже через несколько месяцев после написания
  • Лаконичность — минимизация избыточности без ущерба для понятности
  • Однозначность — каждая программа должна иметь только одну интерпретацию

Начните с определения базовых элементов языка:

  1. Идентификаторы — имена переменных, функций, классов (например, myVar, calculateTotal)
  2. Ключевые слова — зарезервированные слова с особым значением (if, while, function)
  3. Операторы — символы или комбинации для выполнения операций (+, -, ==, >=)
  4. Литералы — представление конкретных значений (42, "Hello", true)
  5. Разделители — символы, отделяющие элементы друг от друга (;, {}, ())

Вот простой пример синтаксиса для объявления переменной в разных стилях:

// C-подобный синтаксис
int age = 25;

// Python-подобный синтаксис
age = 25

// Pascal-подобный синтаксис
var age: integer := 25;

// Собственный синтаксис (пример)
create number:age with 25

При проектировании семантики решите, как язык будет обрабатывать:

  • Типизацию — статическая или динамическая, строгая или слабая
  • Область видимости — как долго существуют переменные и где они доступны
  • Управление памятью — ручное, сборщик мусора или подсчёт ссылок
  • Модель выполнения — однопоточная, многопоточная, асинхронная
Аспект семантики Возможные варианты Влияние на язык
Система типов Статическая / Динамическая Безопасность, гибкость, производительность
Проверка типов Строгая / Слабая Надёжность, удобство использования
Управление памятью Ручное / Автоматическое Производительность, сложность использования
Передача параметров По значению / По ссылке Предсказуемость, эффективность

Работая над семантикой языка, важно составить формальные правила для каждой конструкции. Например, для оператора if:

if (условие) {
// блок кода
} else {
// альтернативный блок кода
}

Семантическое правило может быть следующим:

  1. Вычислить условие
  2. Если результат эквивалентен true, выполнить первый блок кода
  3. В противном случае, если присутствует else, выполнить альтернативный блок
  4. Продолжить выполнение с инструкции, следующей за всей конструкцией if-else

Не забудьте определить, как ваш язык будет обрабатывать ошибки. Существуют два основных подхода:

  • Исключения — как в Java или Python, прерывают нормальное выполнение
  • Возвращаемые значения ошибок — как в Go или C, где функции возвращают статус успеха/ошибки

Помните, что дизайн языка — это итеративный процесс. Не бойтесь экспериментировать и пересматривать свои решения. Многие успешные языки эволюционировали со временем, адаптируясь к потребностям пользователей.

Когда проектируете семантику, думайте о том, как различные конструкции взаимодействуют друг с другом — согласованность здесь ключевой фактор удобства использования вашего языка. Хороший язык программирования должен иметь "принцип наименьшего удивления" — поведение должно быть предсказуемым для пользователя.

Создание лексического и синтаксического анализаторов

Лексический и синтаксический анализ — фундаментальные этапы обработки кода вашего языка программирования. Это как перевод с иностранного: сначала вы разбиваете текст на отдельные слова (лексический анализ), а затем понимаете, как эти слова формируют предложения и выражают мысли (синтаксический анализ). 🔍

Лексический анализатор (лексер) преобразует исходный текст программы в последовательность токенов — минимальных значимых элементов языка. Вот как это работает:

  1. Чтение исходного кода посимвольно
  2. Группировка символов в токены (идентификаторы, числа, операторы и т.д.)
  3. Отбрасывание несущественных элементов (пробелы, комментарии)
  4. Формирование потока токенов для передачи синтаксическому анализатору

Пример простого лексера на Python:

Python
Скопировать код
def tokenize(code):
tokens = []
i = 0
while i < len(code):
# Пропускаем пробелы
if code[i].isspace():
i += 1
continue

# Числовые литералы
if code[i].isdigit():
num = ''
while i < len(code) and code[i].isdigit():
num += code[i]
i += 1
tokens.append(('NUMBER', int(num)))
continue

# Идентификаторы и ключевые слова
if code[i].isalpha():
ident = ''
while i < len(code) and (code[i].isalnum() or code[i] == '_'):
ident += code[i]
i += 1

# Проверяем, не ключевое ли это слово
if ident in ['if', 'else', 'while', 'function']:
tokens.append(('KEYWORD', ident))
else:
tokens.append(('IDENTIFIER', ident))
continue

# Операторы
if code[i] in '+-*/=<>!':
# Обработка двухсимвольных операторов (==, !=, <=, >=)
if i+1 < len(code) and code[i:i+2] in ['==', '!=', '<=', '>=']:
tokens.append(('OPERATOR', code[i:i+2]))
i += 2
else:
tokens.append(('OPERATOR', code[i]))
i += 1
continue

# Разделители
if code[i] in '();{}[],:':
tokens.append(('DELIMITER', code[i]))
i += 1
continue

# Если дошли сюда, встретили неизвестный символ
raise SyntaxError(f"Неизвестный символ: {code[i]}")

return tokens

После лексического анализа наступает черёд синтаксического анализатора (парсера), который преобразует последовательность токенов в абстрактное синтаксическое дерево (AST). AST представляет структуру программы в виде дерева, где узлы — операции, а листья — операнды.

Существует несколько подходов к созданию парсера:

  • Рекурсивный спуск — ручная реализация парсера с использованием набора взаимно-рекурсивных функций
  • Таблично-управляемый парсинг — использование сгенерированных таблиц для определения действий парсера
  • Использование генераторов парсеров — инструментов, которые создают парсер на основе формальной грамматики

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

Python
Скопировать код
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.current = 0

def parse(self):
return self.program()

def program(self):
statements = []
while self.current < len(self.tokens):
statements.append(self.statement())
return {'type': 'Program', 'body': statements}

def statement(self):
# В простейшем случае, каждое выражение — это оператор
return self.expression()

def expression(self):
return self.addition()

def addition(self):
left = self.multiplication()

while (self.current < len(self.tokens) and 
self.tokens[self.current][0] == 'OPERATOR' and
self.tokens[self.current][1] in ['+', '-']):
operator = self.tokens[self.current][1]
self.current += 1
right = self.multiplication()
left = {'type': 'BinaryExpression', 'operator': operator, 'left': left, 'right': right}

return left

def multiplication(self):
left = self.primary()

while (self.current < len(self.tokens) and 
self.tokens[self.current][0] == 'OPERATOR' and
self.tokens[self.current][1] in ['*', '/']):
operator = self.tokens[self.current][1]
self.current += 1
right = self.primary()
left = {'type': 'BinaryExpression', 'operator': operator, 'left': left, 'right': right}

return left

def primary(self):
if self.tokens[self.current][0] == 'NUMBER':
value = self.tokens[self.current][1]
self.current += 1
return {'type': 'NumberLiteral', 'value': value}

if self.tokens[self.current][0] == 'IDENTIFIER':
name = self.tokens[self.current][1]
self.current += 1
return {'type': 'Identifier', 'name': name}

if (self.tokens[self.current][0] == 'DELIMITER' and
self.tokens[self.current][1] == '('):
self.current += 1 # Пропускаем '('
expr = self.expression()

if (self.tokens[self.current][0] != 'DELIMITER' or
self.tokens[self.current][1] != ')'):
raise SyntaxError("Ожидалась закрывающая скобка")

self.current += 1 # Пропускаем ')'
return expr

raise SyntaxError(f"Неожиданный токен: {self.tokens[self.current]}")

При разработке парсера учитывайте следующие аспекты:

  • Приоритет операторов — определяет порядок вычисления выражений
  • Ассоциативность — левая или правая (например, a – b – c интерпретируется как (a – b) – c)
  • Обработка ошибок — информативные сообщения помогут пользователям находить проблемы в коде

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

  • ANTLR — мощный генератор парсеров с поддержкой многих языков программирования
  • Yacc/Bison — классические инструменты для C/C++
  • PEG.js или Nearley — для JavaScript
  • Lark или PLY — для Python

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

  1. Точное соответствие правилам грамматики вашего языка
  2. Эффективную работу с большими объемами кода
  3. Информативные сообщения об ошибках
  4. Возможность восстановления после ошибок (для IDE-интеграции)

Дмитрий Соколов, разработчик компиляторов

В начале карьеры мне поручили разработать предметно-ориентированный язык для моделирования бизнес-процессов. Я был уверен, что смогу написать парсер самостоятельно — как сложно это может быть? Очень сложно, как оказалось.

Первая версия моего ручного рекурсивного парсера работала, но только с простейшими примерами. Как только я начал тестировать более сложные конструкции, посыпались ошибки. Особенно мучительной была отладка левой рекурсии — это привело к бесконечным циклам в моём парсере.

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

Этот опыт научил меня важному принципу: не пытайтесь решить уже решённые проблемы. Существуют десятилетия исследований в области теории компиляторов, и использование проверенных инструментов часто эффективнее, чем изобретение собственных решений.

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

Реализация интерпретатора или компилятора

После создания лексического и синтаксического анализаторов наступает время оживить ваш язык — реализовать интерпретатор или компилятор. На этом этапе абстрактное синтаксическое дерево (AST) превращается в исполняемый код. 🔄

Интерпретатор и компилятор представляют два фундаментально разных подхода к выполнению кода:

Характеристика Интерпретатор Компилятор
Процесс выполнения Напрямую выполняет исходный код Преобразует исходный код в другую форму
Производительность Обычно медленнее Обычно быстрее
Время запуска Быстрое (не требует предварительной компиляции) Медленное (требует этапа компиляции)
Удобство разработки Выше (мгновенная обратная связь) Ниже (требуется перекомпиляция)
Обнаружение ошибок Во время выполнения Многие ошибки обнаруживаются при компиляции

Начнем с реализации простого интерпретатора. Вот как можно интерпретировать AST, созданное нашим парсером:

Python
Скопировать код
class Interpreter:
def __init__(self):
self.variables = {} # Хранилище переменных

def interpret(self, ast):
return self.evaluate(ast)

def evaluate(self, node):
# Определяем тип узла и вызываем соответствующий метод
method_name = f"evaluate_{node['type']}"
method = getattr(self, method_name, self.evaluate_unknown)
return method(node)

def evaluate_Program(self, program):
result = None
for statement in program['body']:
result = self.evaluate(statement)
return result

def evaluate_BinaryExpression(self, expr):
left = self.evaluate(expr['left'])
right = self.evaluate(expr['right'])

if expr['operator'] == '+':
return left + right
elif expr['operator'] == '-':
return left – right
elif expr['operator'] == '*':
return left * right
elif expr['operator'] == '/':
return left / right
# Добавьте другие операторы по необходимости

def evaluate_NumberLiteral(self, literal):
return literal['value']

def evaluate_Identifier(self, identifier):
name = identifier['name']
if name in self.variables:
return self.variables[name]
raise NameError(f"Переменная {name} не определена")

def evaluate_VariableDeclaration(self, declaration):
name = declaration['name']
value = self.evaluate(declaration['value']) if 'value' in declaration else None
self.variables[name] = value
return value

def evaluate_Assignment(self, assignment):
name = assignment['name']
value = self.evaluate(assignment['value'])
if name not in self.variables:
raise NameError(f"Переменная {name} не объявлена")
self.variables[name] = value
return value

def evaluate_unknown(self, node):
raise Exception(f"Неизвестный тип узла: {node['type']}")

Для более сложных языков вам потребуется реализовать дополнительные возможности:

  • Обработка условных операторов (if, switch)
  • Циклы (for, while)
  • Функции и вызовы функций
  • Область видимости переменных
  • Обработка исключений

При реализации компилятора процесс немного сложнее. Вместо непосредственного выполнения AST, компилятор преобразует его в целевой код. Есть несколько возможных целевых платформ:

  1. Нативный машинный код — самый эффективный, но сложный в реализации
  2. Промежуточный байт-код — как в Java (JVM) или Python
  3. Другой язык высокого уровня — например, транспиляция в JavaScript или C

Для новичков рекомендую начать с компиляции в существующий язык. Вот пример генерации JavaScript-кода из нашего AST:

Python
Скопировать код
class JSCompiler:
def compile(self, ast):
return self.generate(ast)

def generate(self, node):
# Определяем тип узла и вызываем соответствующий метод
method_name = f"generate_{node['type']}"
method = getattr(self, method_name, self.generate_unknown)
return method(node)

def generate_Program(self, program):
body = [self.generate(statement) for statement in program['body']]
return '\n'.join(body)

def generate_BinaryExpression(self, expr):
left = self.generate(expr['left'])
right = self.generate(expr['right'])
return f"({left} {expr['operator']} {right})"

def generate_NumberLiteral(self, literal):
return str(literal['value'])

def generate_Identifier(self, identifier):
return identifier['name']

def generate_VariableDeclaration(self, declaration):
name = declaration['name']
value = self.generate(declaration['value']) if 'value' in declaration else 'undefined'
return f"let {name} = {value};"

def generate_Assignment(self, assignment):
name = assignment['name']
value = self.generate(assignment['value'])
return f"{name} = {value};"

def generate_unknown(self, node):
raise Exception(f"Неизвестный тип узла: {node['type']}")

Для более продвинутых компиляторов необходимо рассмотреть следующие этапы:

  1. Семантический анализ — проверка типов, разрешение имён, проверка правил языка
  2. Оптимизация — улучшение кода для повышения производительности
  3. Генерация кода — преобразование AST в целевой код
  4. Управление памятью — реализация сборки мусора или других механизмов

Выбор между интерпретатором и компилятором зависит от ваших целей:

  • Для образовательных проектов или быстрого прототипирования подойдёт интерпретатор
  • Для производственных языков, где важна производительность, лучше выбрать компилятор
  • Можно реализовать гибридный подход (как в Java или C#) — компиляция в байт-код с последующей интерпретацией

Независимо от выбранного пути, помните о поддержке отладки — хороший язык программирования должен предоставлять информативные сообщения об ошибках и средства для отладки программ.

Тестирование и оптимизация собственного языка

Тестирование и оптимизация — критические этапы разработки языка программирования, которые определяют его надежность, производительность и удобство использования. Без тщательного тестирования даже самые элегантные языковые конструкции могут стать источником постоянных разочарований для пользователей. 🧪

Начнем с тестирования. Для языка программирования следует разработать многоуровневую стратегию тестирования:

  1. Модульное тестирование — проверка отдельных компонентов (лексер, парсер, интерпретатор)
  2. Интеграционное тестирование — взаимодействие между компонентами
  3. Функциональное тестирование — поддержка всех заявленных языковых функций
  4. Регрессионное тестирование — проверка, что новые изменения не сломали существующую функциональность
  5. Стресс-тестирование — работа с большими программами и граничными случаями

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

Python
Скопировать код
def test_lexer():
test_cases = [
("42", [('NUMBER', 42)]),
("x = 10", [('IDENTIFIER', 'x'), ('OPERATOR', '='), ('NUMBER', 10)]),
("if (x > 0) { return x; }", [
('KEYWORD', 'if'), ('DELIMITER', '('), ('IDENTIFIER', 'x'), 
('OPERATOR', '>'), ('NUMBER', 0), ('DELIMITER', ')'), 
('DELIMITER', '{'), ('KEYWORD', 'return'), ('IDENTIFIER', 'x'), 
('DELIMITER', ';'), ('DELIMITER', '}')
])
]

for input_code, expected_tokens in test_cases:
actual_tokens = tokenize(input_code)
assert actual_tokens == expected_tokens, f"Expected {expected_tokens}, got {actual_tokens}"

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

Python
Скопировать код
def test_interpreter():
interpreter = Interpreter()

# Тест арифметических операций
ast = Parser(tokenize("2 + 3 * 4")).parse()
assert interpreter.interpret(ast) == 14

# Тест переменных
ast = Parser(tokenize("x = 5; y = x + 3; y")).parse()
assert interpreter.interpret(ast) == 8

Важным аспектом тестирования является обработка ошибок. Убедитесь, что ваш язык предоставляет понятные сообщения об ошибках:

  • Синтаксические ошибки должны указывать на точное местоположение и проблемную конструкцию
  • Семантические ошибки должны объяснять, почему код некорректен (например, несоответствие типов)
  • Ошибки времени выполнения должны предоставлять информативные трассировки стека

После тестирования переходим к оптимизации. Существуют различные уровни оптимизации:

  • Лексическая оптимизация — эффективный алгоритм токенизации
  • Синтаксическая оптимизация — эффективный алгоритм парсинга
  • Семантическая оптимизация — анализ и упрощение кода на уровне AST
  • Оптимизация генерации кода — производство эффективного целевого кода
  • Оптимизация выполнения — эффективное выполнение программ

Начните с профилирования вашего интерпретатора или компилятора, чтобы выявить узкие места. Используйте инструменты профилирования вашего языка реализации (например, cProfile для Python):

Python
Скопировать код
import cProfile
cProfile.run('interpreter.interpret(complex_ast)')

Вот несколько распространенных оптимизаций для интерпретаторов:

  1. Кэширование результатов — сохранение результатов выражений для повторного использования
  2. Инлайнинг функций — встраивание простых функций вместо вызова
  3. Константные выражения — вычисление выражений с константами на этапе компиляции
  4. Оптимизация хвостовой рекурсии — преобразование рекурсии в итерацию
  5. JIT-компиляция — компиляция часто используемых участков кода во время выполнения

Для компиляторов можно применить дополнительные оптимизации:

  • Удаление мёртвого кода — исключение недостижимого кода
  • Развёртывание циклов — уменьшение накладных расходов на проверку условий цикла
  • Распределение регистров — эффективное использование регистров процессора
  • Векторизация — использование SIMD-инструкций для параллельной обработки данных

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

Не менее важно оптимизировать пользовательский опыт:

  • Улучшите сообщения об ошибках, делая их более информативными
  • Добавьте поддержку инструментов разработки (линтеры, форматтеры, отладчики)
  • Создайте хорошую документацию с примерами и руководствами
  • Разработайте плагины для популярных редакторов кода

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

Помните, что разработка языка программирования — это итеративный процесс. Большинство успешных языков эволюционировали со временем, адаптируясь к потребностям своих пользователей и новым технологическим тенденциям. Не стремитесь сразу создать идеальный язык — начните с работающего минимума и постепенно улучшайте его на основе реального использования.

Создание собственного языка программирования — это путешествие, которое требует терпения, глубоких знаний и творческого подхода. Вы узнали, как спроектировать синтаксис и семантику, реализовать лексический и синтаксический анализаторы, создать интерпретатор или компилятор, и наконец, протестировать и оптимизировать свой язык. Теперь мяч на вашей стороне — используйте эти знания, чтобы создать инструмент, который решит конкретную проблему или просто принесет удовольствие от творческого процесса. Помните, что даже самые популярные языки программирования начинались как эксперименты одного человека. Кто знает, возможно, ваше творение станет следующим Python или JavaScript?

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой элемент определяет, как программы на вашем языке будут строиться?
1 / 5

Загрузка...