#brainfuck — Интерпретатор Brain Fuck на C#


BrainFuck

Материал из Lurkmore

В эту статью нужно добавить как можно больше разных эзотерических символов и слов.
Также сюда можно добавить интересные факты, картинки и прочие кошерные вещи.
«

In Soviet Russia brain fucks YOU

BrainFuck (не путать с Mindfuck) — один из эзотерических языков программирования, язык-головоломка. Тем не менее, на нём вполне успешно пишут программы. Целью его создателя был тьюринг-полный язык с компилятором минимального размера.

Содержание

[править] Синтаксис

Синтаксис брейнфака состоит из команд сдвига исполнительной каретки влево-вправо, уменьшения-увеличения текущего значения на единицу, чтения-записи и организации while-цикла. Всего — восемь команд.

[править] Хелловорлд

Хелловорлд на брейнфаке:

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

[править] Обфускация

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

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

Согласитесь, из непонятного «шоыэтазанахуй», код становится вполне читабельным и доступным для понимания. А если учесть, что в Brainfuck используется только 8 символов-команд — можно еще комментариев налепить, в любой удобной для вас форме:

[править] Употребление

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

  • Нечитаемости программ
  • Неудобства или непродуманности синтаксиса


Перл — это тот же brainfuck, только автор его, почему-то, воспринимает серьезно. Может, конечно, это он так издевается, и тащится от того, что мир подхватил его версию brainfuck и строчит на нем да фанатеет.

ЛОР
  • Неудобства инструмента разработки

— Нормальному программисту не важно на чем и под какую платформу писать.
— Брейнфак под дос.

—. если вы настоящий мазохист-извращенец, если вам нравится преодолевать непреодолимое и выполнять невыполнимое, то ваш выбор — это BAT-скрипты.
— С машиной Тьюринга (или с BrainFuck, что почти то же самое) не сравнить.

ЖЖ
  • Примера, чем чревато излишнее упрощение

— В российских школах будут преподавать программирование на Delphi и C++Builder.
— . слава богу, хоть не лисп.
— Надо вообще на COBOL’е или на FORTRAN’е учить. Хотя, самый простой, конечно Brainfuck.

ЛОР
  • Мозгокрутных логических задач, не имеющих практической ценности

Brainfuck. Задача для детей дошкольного возраста. Забудь то, чему тебя учили в школе. Детишки дошколята решают ее в среднем за 3 минуты. 8809=6; 7111=0; 2172=0; 6666=4; 1111=0; 3213=0; 7662=2; 9312=1; 0000=4; 2222=0; 3333=0; 5555=0; 8193=3; 8096=5; 7777=0; 9999=4; 7756=1; 6855=3; 9881=5; 5531=0.
2581=? (спойлер: 2. Ответ — это число замкнутых кружочков и овальчиков в начертании числа )

forum.bobr.by
  • Насмешки при сравнении языков программирования

— Пых-пых что-то достал, хочется чего-то нового, с большими возможностями. …
— Ну автору сабжа же нужно «что-то новое, с большими возможностями». Вот brainfuck ему как раз и подойдет. Утрамбует его моск немножко. Научиться яснее выражать свои мысли, и мы ему быстрее сможем помочь.

#brainfuck — Интерпретатор Brain Fuck на C#

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

case ‘.’:
SetWindowText (hEditC, . );
break;

у меня pointer объявлен, как char *pointer, я не использовал std::map.
поэтому вторым аргументом я писал pointer, но при этом после результата в hEditC дописывался оставшийся код из первого Edit’a.

обработка нажатия кнопки «интерпретировать»


Object-oriented Brainfuck interpreter

Inspired by this question, I decided to give it a try and implemented a brainfuck interpreter myself. It includes various improvements:

  • It’s object-oriented and modular
  • Unlimited tape size
  • It includes a call stack (no seeking for opening parenthesis)
  • It allows stepping and debugging
  • It throws exceptions on errors

Interpreter

Main routine (for testing)

Takes the program directly (not a filename) as first command line argument. Prints the tape’s contents to stderr after each step.

How to Write a Brainfuck Interpreter in C#

Oct 13 ・5 min read

I’ve always been somewhat interested in how programming languages are written. Last month I bought Writing An Interpreter In Go, and am about 40% of the way through that book (though my version is in C#). I’ve hand built a tokenizer and am starting to write my own parser.

It’s gotten me curious about programming languages, and I wanted to try my hand at building a brainfuck interpreter. Lets dive in.

Brainfuck

Brainfuck is an esoteric programming langauge. It’s designed to be confusing, hence the name. The language only consists of 8 characters, > + — . , [ ] . With an internal structure defined by wikipedia as:

The brainfuck language uses a simple machine model consisting of the program and instruction pointer, as well as an array of at least 30,000 byte cells initialized to zero; a movable data pointer (initialized to point to the leftmost byte of the array); and two streams of bytes for input and output (most often connected to a keyboard and a monitor respectively, and using the ASCII character encoding).

Each character in the language is defined as:

: increment the data pointer (to point to the next cell to the right)
> : decrement the data pointer (to point to the next cell to the left)
+ : increment (increase by one) the byte at the data pointer
— : decrement (decrease by one) the byte at the data pointer.
. : output the byte at the data pointer
, : accept on byte of input, storing its value in the byte at the data pointer
[ : if the byte at the data pointer is zero, then instead of moving the instruction pointer forward to the next command, jump it forward to the command after the matching ] command.
] : if the byte at the data pointer is nonzero, then instead of moving the instruction pointer forward to the next command, jump it back to the command after the matching [ command.

Here is a program that prints out “Hello World”:

If this is your first esoteric programming language, this definitely throws you for a loop. Since this article is about documenting how to build an interpreter, here’s a great video describing brainfuck to anyone to wants a more visual representation.


So lets see some basic code examples.

Examples

If I wanted to write a single exclamation point, here is a program that would do that:

It uses the + character to add 1 to the current data pointer 33 times, 33 being the ASCII representation for an exclamation point. Then uses the . character to print that single character.

Here are three different programs that print out !? :

The first version generates and prints the exclamation pointer as before, then moves to the next cell and increments data pointer to 63, the ASCII representation for ?, and then prints it.

The second version reuses the cell of the exclamation mark, so it only needs to increment that cell 30 more times. 33 (!) + 30 = 63 (?)

The third version uses the [ and ] brackets to repeat sections of code resulting in smaller programs.

The Interpreter

I’ve named my interpreter Dizzy, because programming in brainfuck can make you dizzy.

Well that aside, there’s not a whole lot of complexity to the interpreter.

Lets start off by creating a class called Interpreter that will be interpreting our program.

It has three instance variables: 1. A byte array to hold any data we might need, 2. A pointer telling us where we are in that array, 3. The input converted into a char array.

Then lets create a scaffold of our Run method, which is a switch statement for each of the 8 valid characters in our language.

In brainfuck, any other characters are just ignored, so anything not in this set of 8 will just be skipped over.

Given our definition above, for > and , we need to increment and decrement the pointer.

Then for + and — , we need to increment and decrement the current value in our tape referenced by the pointer.

Then our IO characters are also pretty simple:

Finally we need to implement our brackets. Brackets help us repeat or skip sections of a brainfuck program, kind of like a goto statement. That’s why the third program for writing !? was so much shorter, brackets allowed the same section code to be run multiple times.


The only complication here is nested brackets. For a program like [+[+]+]>+ , which skips from the first open bracket to the last close bracket, we need to keep track which open/close brackets belong together.

We introduce an int called unmatchedBracketCounter , (defined outside the for loop) which tracks how many pairs of brackets we’ve encountered. If we’re looking for a ] and encounter another [ , we know we will have to now pass two ] before we can stop.

Combining all these elements we end up with a single class that can interpret a brainfuck program.

Testing

Now for testing. As someone who codes for a living, I feel very comfortable writing programs in normal programming languages. Brainfuck programs are another matter.

Fortunately I found this great website which has brainfuck programs, can run brainfuck programs, and can generate brainfuck programs as well.

Here are some fun programs that I found on that website.

CALCULATE PI

This calculates Pi to the 15 decimal place. When run the output is 3.14070455282885 , which the first 15 digits of PI are 3.14159265358979 , which I am using an 8 bit cell, so it seems like I’m experiencing some overflows as called out in the program.

CALCULATE SQUARES

When run this programs outputs all squares up to 10000, 0, 1, 4, 9 16, …, 9801, 1000.

99 BOTTLES OF BEER

When run this prints out the 99 bottles of beer chanty, from 99 all the way to 0.

And finally a program that takes advantage of user input. Most brainfuck programs that I found didn’t use the input character ,, I wrote one with the help of the text generator mentioned earlier.

Closing Thoughts

There you have it. A interpreter that will run brainfuck programs.

I definitely enjoyed writing this interpreter. The first one that I’ve written from scratch. It was a fun and pretty different programming challenge from what I’m used to.

I hope you enjoyed it and continue to experiment with things that you’re interested in.


Настоящий Brainfuck №1

Brainfuck, вероятно, это самый безумный язык
программирования, который встречается на
свете. Сам язык состоит всего из 8
операторов, которыми можно написать
практически любую программу, которую вы
хотите. Для работы вам понадобится:
компилятор или интерпретатор (приводятся в
конце статьи — компилятор на ассемблере,
интерпретатор Brainfuck-C); ASCII таблица; вероятно
калькулятор :).

Основы

Главная идея Brainfuck — манипулирование
памятью. Вам выдается 30.000 массив 1 байтовых
блоков, хотя на самом деле размер массива
зависит от компилятора или интерпретатора,
но стандартный размер — 30.000. Внутри этого
массива вы можете увеличивать указатель,
значение в ячейке и так далее. Для работы,
как я уже говорил, используется 8
операторов:

> = увеличение указателя памяти или
смещение право на 1 блок
[-]

Что делается? По идее открывается цикл,
уменьшается значение текущей ячейки на 1 и
цикл продолжается до тех пор, пока значение
не достигнет 0. Однако так как изначально
все значения в памяти и есть 0, цикл никогда
не начнется.

Напишем другую программу:

На С это аналогично такой программе:

В этой программе мы увеличили значение по
указателю на 5, потом открыли цикл, который
уменьшал значение до 0.

На выходе мы получим такой вид памяти.

Очевидно, что сначала мы увеличиваем
указатель на 4 и затем два раза увеличиваем
значение на 1 два раза. Разнообразим?

Опять же понятно, что сначала 2
записывается в четвертую ячейку, потом 1 во
вторую и затем еще раз 1 прибавляется к 4.

Давайте напишем программу, которая будет
уже выводить нечто в консоль, например
любимый всеми нами «Hello world». Выглядит
она так:

Круто? Настоящий Brainfuck. Разберем что
происходит. Надо помнить, что мы имеем дело
с кодами ASCII для вывода, так что надо
оперировать именно такими кодами.

Сдвигаемся вправо, оставляя первую ячейку
пустой, записываем туда 9, открываем цикл. В
цикле смещаемся влево, записываем туда 8,
смещаемся вправо и уменьшаем значение на единицу.

Цикл идет до тех пор, пока во второй ячейке
не станет 0. Не слишком трудные вычисления
покажут, что в конце работы программы в
первой ячейки памяти будет 72, во второй 0.
Это код буквы Н, поэтому выводим ее в
консоль. Продолжаем для каждого символа и в
конце концов получаем заветные Hello world!
Занятно, не правда ли? ��

AlterVision Антон Резниченко

Компилятор (интерпретатор) Brainfuck

Порой преподаватели университета дают весьма интересные задания. На этот раз идеей многоуважаемого Эдуарда Эмильевича Александрова, уже натолкнувшего меня на создание менеджера памяти MC Heappie и OpenGL-генератора Landscape Winter, было создание компилятора …
Одно «но» — разумеется, он по началу не уточнил, какого именно)
Мой хороший товарищ Тим в своё время говорил о замечательном языке программирования, который «трахает мозг» … Полистав свою любимую Википедию, я наткнулся на крайне интересный язык программирования — Brainfuck!


Brainfuck (англ. brain мозг + fuck) — один из известнейших эзотерических языков программирования, придуман Урбаном Мюллером (Urban Muller) в 1993 году для забавы. Язык имеет восемь команд, каждая из которых записывается одним символом. Исходный код программы на Brainfuck представляет собой последовательность этих символов без какого-либо дополнительного синтаксиса.

Одним из мотивов Урбана Мюллера было создание языка с как можно меньшим компилятором. Отчасти он был вдохновлен языком FALSE, для которого существовал компилятор размера 1024 байта. Существуют компиляторы языка Brainfuck размера меньше 200 байт. Программы на языке Brainfuck писать сложно, за что его иногда называют языком для мазохистов. Но при этом важно отметить, что Brainfuck является вполне естественным, полным и простым языком и может использоваться при определении понятия вычислимости.

Машина, которой управляют команды Brainfuck, состоит из упорядоченного набора ячеек и указателя текущей ячейки, напоминая ленту и головку машины Тьюринга. Кроме того, подразумевается устройство общения с внешним миром (см. команды . и ,) через поток ввода и поток вывода.

Подробнее о самом языке Вы можете легко почитать на соответственной страничке — Википедия — Brainfuck (ru).

Главное, что нужно знать для написания компилятора — это интерпретация команд языка.

Команда Эквивалент C Описание команды
> ++p; перейти к следующей ячейке
» двигают текущий указатель по пространству ячеек машины Тьюринга. При этом, если указатель пытается переместиться в отрицательную область или выходит за пределы пространства, он обнуляется.
Команды изменения значения «+» и «-» отвечают за инкремент и декремент значения в ячейке, на которой находится указатель.
Команда вывода информации «.» печатает содержимое текущей ячейки на экране. Так как ячейки являются байтами, то содержимое легко печатается как символ, соответствующий данному значению ASCII.
Команда ввода «,» считывает один символ из основного ввода — то есть пользователю необходимо нажать на какую-либо клавишу.
Команда начала цикла «[» записывает текущий указатель команды в стек и начинает выполнение цикла, если значение текущей ячейки больше нуля.
Команда окончания цикла «]» считывает из стека последний указатель команды и возвращается на него.

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

А теперь — самое главное — «Hello World!» на BrainFuck!!

Сохраняем это в файл hw.bf, и запускаем его как run hw.bf из командной строки. Что мы видим? Hello World! Значит, компилятор удался!!

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

Выполнено на Microsoft Visual Studio 2008, лицензия МГУ им.Н.П.Огарёва, ФЭТ.
Автор кода: © Антон /AlterVision/ Резниченко

#brainfuck — Интерпретатор Brain Fuck на C#

200?’200px’:»+(this.scrollHeight+5)+’px’);»> using namespace std;

string filename;
cout > filename;
fstream source_file(filename.c_str(), ios_base::in);
vector source;

while (!source_file.eof())
<
char ch = 0;
source_file.get(ch);
source.push_back(ch);
>

Что ж, вот и всё! Интерпретатор написан. Основная трудность, как вы заметили, может возникнуть лишь при обработке циклов. А так — всё элементарно
Надеюсь, что урок Вам понравился!

200?’200px’:»+(this.scrollHeight+5)+’px’);»> #include
#include
#include
#include
#define CELLS_NUM 30000

int main()
<
unsigned char arr[CELLS_NUM] = <>;
unsigned int current_cell = 0;

BrainFuck Interpreter in Java


Brainfuck consists of only eight simple commands and an instruction pointer. While it is fully Turing-complete, it is not intended for practical use, but to challenge and amuse programmers.
BrainFuck consists of 8 character commands only which makes its use very challenging even for simple tasks –

  • The > command increments the data pointer (to point to the next cell to the right).
  • The command decrements the data pointer (to point to the next cell to the left).
  • The + command increments (increase by one) the byte at the data pointer.
  • The — command decrements (decrease by one) the byte at the data pointer.
  • The . command outputs the byte at the data pointer.
  • The , command accept one byte of input, storing its value in the byte at the data pointer.
  • [ – if the byte at the data pointer is zero, then instead of moving the instruction pointer forward to the next command, jump it forward to the command after the matching ] command.
  • ] – if the byte at the data pointer is nonzero, then instead of moving the instruction pointer forward to the next command, jump it back to the command after the matching [ command.
  • (Alternatively, the ] command may instead be translated as an unconditional jump to the corresponding [ command, or vice versa; programs will behave the same but will run more slowly, due to unnecessary double searching.)
  • [ and ] match as parentheses usually do: each [ matches exactly one ] and vice versa, the [ comes first, and there can be no unmatched [ or ] between the two.

Since BrainFuck consists of only these 8 commands, building an interpreter for BrainFuck is quite simple. In this article, we will build a simple program which takes a BrainFuck code as input and produces the desired output. We will simply accept the BrainFuck code as a String and produce the output by parsing the String and checking every character for its actual functionality. The memory is represented by an array of byte type simulating memory of max 65535 bits from 0 to 65534(65535 is the highest number which can be represented by an unsigned 16-bit binary number). The variable ptr refers to the current index of the memory array.

In this article, we won’t be discussing the details of writing programs in BrainFuck.For more details on writing BrainFuck programs, refer to the following links:

Java Implementation of the BrainFuck Interpreter-

BF interpreter written in C#

I have recently written a Brainfuck interpreter in C#. I tested it with the examples given on EsoLang website. It does not handle errors right now.

  1. Even that it can run only one program at once, should I maybe put the variables into the >BF method?
  2. Should I split all of the code from the cases and create separate methods for them, accessing them from the switch ?

Of course, I am also looking for all other suggestions on how I can improve this code.

Недавно я написал интерпретатор Brainfuck на C#. Я тестировал его с примерами, приведенными на веб-сайте EsoLang. Он не обрабатывает ошибки прямо сейчас.

  1. Даже то, что она может работать только одна программа сразу же, если я, возможно, поместить переменные в класс (создать новый класс), или просто оставить их внутри метода BF ?
  2. Должен ли я разбить весь код из корпусов и создать для них отдельные методы, получив их от switch ?

Конечно, я также ищу все другие предложения о том, как я могу улучшить этот код.

Создан 20 окт. 16 2020-10-20 21:11:37 107MP

Hey, welcome to CodeReview! Hope you get some nice reviews. – Graipher 20 окт. 16 2020-10-20 21:38:13

You can stack switch conditions – Paparazzi 20 окт. 16 2020-10-20 23:04:15

2 ответа


Looks like you’re throwing away memory[0] here; I’d expect the pointer to initialize at the very start of the «tape».

The == true part is redundant, and the fully-qualified File.Exists feels. crowded. I’d remove System.IO. and add using System.IO; at the top of the file.

Arguably if no argument is specified, you should be throwing some ArgumentException instead of just saying «specify the source file» and exiting with a code-0, which would be interpreted as a successful run. Such a guard clause would help reduce nesting in the Main procedure:

Notice the consistency with brace positioning; your code alternates between C#-style and Java-style braces, depending on whether we’re looking at a member or its body — it really doesn’t matter which one you prefer, but pick a style, and stick to it.

«BF» might perhaps possibly make a [bad] >BF is a method, and methods do something, they’re verbs. Interpret or even Run would be a better choice.

BF is a simple language; you don’t need to go out of your way and implement a lexer to tokenize input and a parser to make sense out of it — basically every single character is an instruction, so it makes sense to process it one character at a time.

But is it ideal to read an entire file’s contents, iterate every single character to create a character array, and then pass that array to a method that will iterate it one item at a time? Seems rather inefficient, considering you could be streaming the file content into the interpreter, and finish interpreting the BF program as you finish reading the last character in the file, having iterated the contents exactly once.

I think I’d make a separate dedicated >pointer and memory[] as instance fields; I’d make a Dictionary to map each BF token to a dedicated method, too.

Создан 20 окт. 16 2020-10-20 22:11:34 Mat’ s Mug

Thanks! I d > – 107MP 20 окт. 16 2020-10-20 22:27:01

A counter argument against streaming the code is that BF programs can have loops, so you have to keep most of the code in memory anyway. – Roland Illig 20 окт. 16 2020-10-20 22:27:28

@RolandIllig wouldn’t only the loop body need to stick around though? I’ve never written a BF interpreter, . your comment makes me want to give it a shot :-)Mat’ s Mug 20 окт. 16 2020-10-20 23:01:18

The code from the question is the most straight-forward way to write a BF interpreter. When you want to discard code from memory, you can only do if after proving that it is dead code. This will make the interpreter so much more complicated that it is usually not done. You would need the typical data flow analysis algorithms for this task. – Roland Illig 20 окт. 16 2020-10-20 23:04:11

@Mat’sMug: You should really write your own BF interpreter, it’s a matter of a few hours only. When you are finished, you could review your answer here and check whether using a ‘Dictionary’ and separate methods really make the code clearer. I doubt so, since almost every simple BF interpreter is just the basic ‘while switch’ loop. This coding style makes stepping through the code really simple, unlike having to deal with separate methods (unless, maybe, for the ‘[‘ and ‘]’ commands). – Roland Illig 20 окт. 16 2020-10-20 23:07:48

Look at you. It’s almost like you know a few things about compilers or something. – RubberDuck 21 окт. 16 2020-10-21 00:28:33

@RubberDuck lol, I started making one in C# 6, got the lexer done, hoping to finish the parser and the interpreter tonight. Not the «simple BF interpreter», but the work will be useful for RD’s 2.x syntax trees :-)Mat’ s Mug 21 окт. 16 2020-10-21 14:04:50

@RolandIllig how about an [instruction per class](http://codereview.stackexchange.com/q/145060/23788)? I have to say it was pretty fun to implement. – Mat’ s Mug 24 окт. 16 2020-10-24 13:22:03

(Functionality) Since the above is an error message, it should be written using Console.Error.WriteLine . Additionally, you should call Environment.Exit(1); to tell the calling program about the usage error.

(Style) You can omit the == true .


(Style) Why do you have two empty lines between the above closing braces? Usually, one empty line is enough, and in this case, I would prefer to have zero empty lines.

(Functionality) You should initialize pointer = 0 , since the first val >0 .

(Style) Instead of pointer += 1 , it is more common to write pointer++ .

(Style) This one should be pointer— .

(Style) As long as you don’t declare any local variables, the braces in each of the case s are not necessary. A case statement takes quite a lot of screen space because of all the break statements, so enclosing them in braces wastes even more space.

(Functionality) Using three different types for the memory cells makes the Brainfuck programs hard to debug. You use int for the loops, char when writing a character and byte when reading a character. This is very confusing. Choose one of the types and use it in all places.

(Functionality) It is simpler to just say memory[pointer] = (byte) Console.Read() . Or just leave out the type cast and simply say memory[pointer] = Console.Read() .

(Style) The closing brace is indented at the same level as the statements above it. This looks wrong.

(Style) Why d >s ? I would understand level , depth or some similar name, but s is usually a name for a string .

(Functionality) If the brainfuck program has unbalanced loop brackets, your code will throw an exception because ptr will be outs >memory index. You have to choose whether you want to add error handling code here.

(Style) The many empty lines look unorganized. Just remove them, unless you want to organize your code into paragraphs for human readers. Then use one empty line for that.

Overall: The code is easy to read and nicely structured. There are only some small stylistic things and some bits around error handling that could be improved.

Создан 20 окт. 16 2020-10-20 22:25:13 Roland Illig

#brainfuck — Интерпретатор Brain Fuck на C#

A Brainfuck editor & optimizing interpreter, written in JavaScript. The code is converted to JavaScript code and then run in a web worker, which speeds up execution at lot (try mandelbrot.b for example).

Have fun! :-)
You can add special chars to the input field:
Decimal: \65 (same as «A»)
Hexadecimal: \x7E (same as «

«)
Control characters: \r \n \t

For any kind of feedback, toss me a mail to

Cell size (Bits): 8 16 32
Dynamic (infinite) Memory:
Memory size:
Memory overflow behaviour:
undefined (fast) wrap abort
End of input: no change char:
Dump Memory at char:
Count instructions

Цукерберг рекомендует:  Обучение - Какую программу выбрать для считывания текстов с мобильного
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих