Войти
ФлеймФорумПрограммирование

Революционные идеи в сфере языков программирования (3 стр)

Страницы: 1 2 3 4 526 Следующая »
#30
16:35, 3 окт. 2018

Итак, возможный метод решения проблемы.
Операторы функционируют как лямбды - они являются значениями, передаются в виде аргументов и обладают типом, составленным из параметров и результатов. Типы приводятся по правилам, описанным в #26 - тип параметра можно уточнить, тип результата можно обобщить. Если оператор берёт лямбды аргументами - то приведение рекурсивно распространяется и на них.
Внутри тела оператора складывается контекст из возможных операций - это операторы, наследуемые от контекста модуля, и операторы, переданные в виде аргументов (возможно, неявных).

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

behavior Ordered[T: Type]
    requires operator less[: T, : T]: Testable
end

parametric [T: Type] operator greater[a: T, b: T]: Testable
        -- requires-требования работают как дополнительные аргументы
        requires behavior Ordered[T]
    return less[b, a]
end

-- крутые слова нужны только затем, что термин "Value" уже задействован как имя самого общего типа в языке
parametric [Domain: Type, Codomain: Type] class OrderedMapNode
        -- requires-требования работают как дополнительные поля класса
        requires behavior Ordered[Domain]
    state key: Domain
    state image: Codomain
    state less_subnode: Optional[OrderedMapNode[Domain, Codomain]]
    state greater_subnode: Optional[OrderedMapNode[Domain, Codomain]]
end

parametric [Domain: Type, Codomain: Type] operator set_node_image[node: Optional[OrderedMapNode[Domain, Codomain]], key: Domain, image: Codomain]: OrderedMapNode[Domain, Codomain]
    -- behavior Ordered[Domain] уже есть в составе OrderedMapNode[Domain, Codomain]
    -- get[cell] и set[cell, value] передаётся в составе Optional[...]
    if empty[node] then
        return OrderedMapNode[Domain, Codomain] {
            -- инициализатор как в Lua и крест-конструкторах - левая часть всегда называет поле объекта, правая является выражением и вычисляется
            key -> key,
            image -> image,
            -- less_subnode и greater_subnode получают инициализацию по умолчанию
        }
    -- get_key[node] передаётся в составе OrderedMapNode[...]
    elseif key < get_key[get[node]] then
        state subnode = get_less_subnode[get[node]]
        subnode = set[subnode, set_node_image[less_node, key, domain]]
        return set_less_subnode[get[node], subnode]
    -- greater[k1, k2] берётся из внешнего контекста
    -- внутрь greater передаётся Ordered[Domain] из node
    elseif key > get_key[get[node]] then
        state subnode = get_greater_subnode[get[node]]
        subnode = set[less_node, set_node_image[less_node, key, domain]]
        return set_greater_subnode[get[node], subnode]
    else
        return set_image[get[node], image]
    end
end

-- альтернативный вариант записи типовых параметров
parametric [Domain: Type, Codomain: Type, Node: subset of OrderedMapNode[Domain, Codomain]] operator get_node_image[node: Optional[Node], key: Domain]: Optional[Codomain]
    if empty[node] then
        return Optional[Codomain] {}
    elseif key < get_key[get[node]] then
        return get_node_image[get_left_subnode[get[node]], key]
    elseif key > get_key[get[node]] then
        return get_node_image[get_right_subnode[get[node]], key]
    else
        return Optional[Codomain] {get_image[get[node]]}
    end
end

-- одно поведение может затрагивать сразу несколько типов на разных ролях
behavior MapContainer[Container: Type, Domain: Type, Codomain: Type]
    requires operator get[: Container, : Domain]: Optional[Codomain]
    requires operator set[: Container, : Domain, : Codomain]: Container
end

parametric [Domain: Type, Codomain: Type] class OrderedMap
    -- включает статическую проверку на реализацию
    -- кроме того, заполненный набор лямбд хранится в виде поля
    implements MapContainer[current class, Domain, Codomain]
    state root: Optional[OrderedMapNode[Domain, Codomain]]

    operator get[self: current class, key: Domain]: Optional[Codomain]
        return get_node_image[get_root[self], key]
    end

    operator set[self: current class, key: Domain, image: Codomain]: current class
        state newroot = set_node_image[get_root[self], key, image]
        return set_root[self, set[get_root[self], newroot]]
    end
end

operator test1[]
    state a = greater[10, 20] -- ок, вернёт false
    state b = greater["foo", "bar"] -- ок, вернёт true
    state c = greater[23, "45"] -- ошибка - в текущем контексте нет ни одной реализации Ordered[Value]
end

parametric [Container: Type, Domain: Type, Codomain: Type] operator test2[map: Container, key: Domain, image: Codomain]: Container
    return set[map, key, image] -- ошибка - в текущем контексте нет ни одного применимого set
end

parametric [Container: Type, Domain: Type, Codomain: Type] operator test3[map: Container, key: Domain, image: Codomain]: Container
        requires behavior MapContainer[Container, Domain, Codomain]
    return set[map, key, image] -- ок
end

operator test3[]
    state m = OrderedMap[String, Number]
    m = test3[m, "foo", 10] -- ок
    m = test3[m, "foo", "bar"] -- ошибка - нет ни одной реализации MapContainer[OrderedMap[String, Number], String, String]
end


Стоит заметить, что одиночные requires operator так же можно считать частным случаем поведения.
Язык автоматически переносит целые поведения, однако, он не станет автоматически собирать новый объект поведения из доступных операторов. Это правило не даёт языку, например, создать OrderedMap[Type, Value] - хоть Type и поддаётся сравнению в рамках PartiallyOrdered, этого недостаточно для корректного функционирования двоичного дерева. То есть - для поведений так же применяется LSP, как и для типов.


#31
16:51, 3 окт. 2018

А ещё недавно я увидел интересную идею - использовать систему типов в том числе и для проверки инвариантов.
Базовый принцип состоит в том, что вместе с нормальными аргументами передаётся специальный объект, который инкапсулирует требуемый инвариант.
Как-то вот так:

template<typename T, typename U>
struct OrderedPair
{
    T const a;
    U const b;

    OrderedPair(T a, U b)
        : a(a)
        , b(b)
    {
        if (a > b)
            throw InvariantFailedError;
    }
};

int get_random(OrderedPair<int, int> range)
{
    // range.a <= range.b гарантируется уже тем фактом, что range вообще существует
    return random(range.b - range.a) + range.a;
}

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

#32
20:25, 3 окт. 2018

А почему стате, а не вар?
А так очень круто выглядит. Знаком ли ты с Адой? Какие существенные отличия ты видишь?

#33
0:58, 4 окт. 2018

1 frag / 2 deaths
> А почему стате, а не вар?
Ну, логика примерно такая:
- "var" не совсем корректно применять к полям объекта, потому что они не являются независимыми переменными, а меняются вместе с полным объектом;
- "let" не работает по обратной причине - обычно "let" используют в математике для определений, которым в компьютерных языках соответствуют константы и чистые функции;
- "local" (из Луа) опять же не совсем подходит по смыслу - так говорят в первую очередь про локальные переменные.
Поэтому, чтобы не давать ложных ассоциаций, я решил использовать отдельный термин для понятия "вещь, которая обладает некоторым значением".
На текущей высоте полёта состояниями являются: локальные переменные, поля объектов, динамические переменные и переменные модуля.

1 frag / 2 deaths
> А так очень круто выглядит. Знаком ли ты с Адой?

It has built-in language support for design-by-contract, extremely strong typing, explicit concurrency, tasks, synchronous message passing, protected objects, and non-determinism. Ada improves code safety and maintainability by using the compiler to find errors in favor of runtime errors.

Ух ты. Нет, не знаком; но, видимо, стоит ознакомиться.
#34
1:25, 7 окт. 2018

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

1. Неявные связи - это зло. Если какая-то функция берёт список - это должно быть прописано в её прототипе. Если какое-то выражение меняет значение какой-то переменной - это должно быть явно выражено в месте вызова, либо позицией (слева от знака =), либо явным модификатором. В ситуации, когда функция Б использует результат работы функции А, естественным методом использования должна быть явная передача какого-либо значения между А и Б.
То есть,

objectA->putMyDataIntoSingletonQueue();
objectB->getDataFromSingletonQueue();
- это плохо и язык такие вещи должен как минимум не_поощрять; чтобы программисту в первую очередь приходили в голову решения вида
singletonQueue->putData(objectA);
objectB->getDataFrom(singletonQueue);
,
auto handle = objectA->putMyDataIntoSingletonQueue();
objectB->getDataFromSingletonQueue(handle);
или, на худой конец,
objectA->putMyDataIntoSingletonQueue();
synchronize(objectB, objectA);
objectB->getDataFromSingletonQueue();
_

2. Байтодрочерством пусть занимается оптимизатор. Семантика языка формулируется в первую очередь в математических терминах - неизменные агрегаты, атомные присваивания, клонирования, сборка мусора, полностью динамическая типизация и прочие удобства. С другой стороны, оптимизатору даётся широкая свобода действий - например, оптимизатору дозволяется свободно переставлять поля объекта. То, что в языке описано как

class Point
    state x: Integer
    state y: Integer
end

class Spline
    state points: List[Point]
end


в памяти может быть представлено и как
my_glorious_language::Value
, и как
struct {
    std::vector<my_glorious_language::Value> points;
}
, и как
struct {
    std::vector<my_glorious_language::LongInteger> xs;
    std::vector<my_glorious_language::LongInteger> ys;
}
, и как
struct {
    struct { int16_t x, y; } points[10]; // Диапазон и количество точек статично
}
, и, теоретически, даже как
struct {
    size_t count;
    void(*get)(int index, my_glorious_language::LongInteger *px, my_glorious_language::LongInteger *py);
}
В идеале, нужные преобразования оптимизатор должен подбирать самостоятельно, исходя из статической информации и показаний профайлера; однако, так же должна быть возможность вручную потребовать/запретить определённые преобразования.
_

3. Базовая семантика языка формализуется для самого общего случая. А именно: все объекты на самом деле живут на другом конце планеты, а любое обращение к ним выливается в серию UDP-запросов в обе стороны, а каждая функция и каждое выражение - это транзакция, которая имеет начало и конец и может быть откачена в любой момент между ними. Это легализует спекулятивное выполнение на уровне языка.
Многоуровневый кеш тогда оказывается частным случаем удалённого обращения.
В конструкции вида

if does_condition_hold[test_object] then
    do_some_stuff[modify work_object]
else
    do_other_stuff[modify work_object]
end
do_yet_another_stuff[modify work_object]

виртуальная машина имеет полное право скопировать work_object со всеми потрохами, начать выполнять обе ветки параллельно, вызвать do_yet_another_stuff сразу над обоими вариантами, полчаса спустя получить ответ из интернета о результате does_condition_hold и отбросить неправильную ветку прямо посреди исполнения.
Точно так же, при виде
-- Integer - это целое неограниченного размера
-- Для модульной арифметики нужен явно обозначенный тип, навроде Int32 и UInt64
state r: Integer = 0
for value -> x: Integer in list do
    r += x
end
return x

ничто не запрещает виртуальной машине превратить эту функцию в эффективный эквивалент
    int32_t r = 0;
    for (int x : list) {
        if (add_with_overflow_check(r, x))
            goto longint;
    }
    return my_glorious_language::wrap_layout<int32_t>(r);

longint:
    my_glorious_language::LongInteger lr = 0;
    for (int x : list) {
        lr += x;
    }
    return my_glorious_language::wrap_layout<my_glorious_language::LongInteger>(lr);
(причём разделение int32_t/LongInt может быть продолжено и дальше по месту вызова)
Наконец, вместе с возможностью свободно перестраивать форму данных, это позволяет виртуальной машине свободно выбирать между методами синхронизации, будь то обычная блокировка, CAS или что-то ещё.
А ещё мне нравится мечтать, что если подобный язык всё-таки выстрелит (не может же сразу всё получиться с первого раза), то это приведёт к появлению электроники, специально подогнанный под подобную семантику. То есть - будут всё те же процессоры с OOOE, спекуляцией и кэшами, но уже не в маскарадно-трёхколёсном режиме, а явно контролируемые софтом - который, будучи рантаймом языка, обладает бо́льшим объёмом информации о программе и поэтому может более эффективно распорядиться ресурсами.
При этом, нам в любом случае понадобиться выделить действия, которые невозможно отменить и, следовательно, должны быть выполнены однажды и наверняка. Например - если что-то выведено на экран, то мозг пользователя в обратную сторону откатить уже не получится.
Следовательно, классический режим - когда дела делаются по порядку и друг за другом - реализуется как частный случай транзакционной модели, где каждая транзакция - необратимая.
#35
5:58, 13 окт. 2018

Вот такой вопрос.
Были ли у кого-то попытки взять форму вида sea of nodes и продвинуть её ещё дальше в графы зависимостей; заменив управляющие конструкции на функциональные?
Теоретически, конструкцию

// На минуту представим себя оптимизатором и подставим "регистр" для памяти явным аргументом
Memory if_statement_123(Memory original_state) {
    if (cond(original_state)) { // для простоты считаем, что у cond нет побочных эффектов
        return true_branch(original_state);
    } else {
        return false_branch(original_state);
    }
}
можно представить таким образом:
Изображение

Тогда как цикл
Memory while_statement_123(Memory original_state) {
    Memory current_state = original_state;
    while (cond(original_state)) {
        current_state = loop_body(current_state);
    }
    return current_state;
}
можно реализовать через хвостовую рекурсию:
Memory while_statement_123(Memory original_state) {
    if (cond(original_state)) {
        Memory intermediate_state = loop_body(original_state);
        return while_statement_123(intermediate_state);
    } else {
        return original_state;
    }
}
Изображение

Собственно, пытались ли где-либо использовать IR подобного рода? Или она настолько нестандартная, что могла прийти в голову только извращенцу с гд? Или тут есть какой-то фатальный и очевидный недостаток, который я никак не могу увидеть?
Я думаю использовать такую модель в формализации работы вымышленного языка. В ней, структура графа играет роль байт-кода функции. Выполнение функции состоит в том, чтобы по заданным входным значениям пройтись по всем узлам и каким-нибудь образом найти результаты. Какие-то узлы - встроенные функции, включая арифметику, перелопачивание агрегатов, тринарный узел; они вычисляются рантаймом. Отдельная категория - это узлы-ссылки, когда в содержимое одного блока подставляется структура, описанная в другом месте; по сути - вызов подпрограммы. Графы и значения на рёбрах хранятся раздельно; более того - один граф, через ссылки, может быть инстанцирован множество раз, каждый раз со своим набором входом и промежуточных значений. Инстанс этого графа играет роль стекового фрейма, с тем отличием, что, в общем случае, фреймы образуют дерево. Разумеется, параллельные узлы могут вычисляться одновременно.
Главное отличие от обычных IR заключается в том, что:
- вместо использования фи-функций компилятор переносит условие вперёд телеги и расставляет тринарные выражения - это позволяет избавиться от region-блоков и выразить условные зависимости напрямую через узлы графа;
- рёбра графа являются не просто def-use связями, а несут конкретные значения - это позволяет напрямую использовать его вместо байт-кода;
- поскольку в графе нет циклов из фи-функций - по идее, это должно упростить принятие решений по порядку выполнения узлов; поскольку в таком формализме, программа - это не динамическая система, а статически заданная система уравнений, которую рантайм постепенно (и, хотелось бы надеяться, эффективно) решает с целью прийти к ответу - конечному состоянию.
_

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

#36
8:11, 13 окт. 2018

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

#37
8:22, 13 окт. 2018

*Lain*
> Напиши язык, чтобы за тебя всю программу писал. А ты только бы греб деньги
Лучше тогда запилить нейросеть, которая будет генерировать исекайные ранобешки. Переучивать её каждый год на свежие тренды и таким образом перебивать аудиторию у потных отакунов (кем является большинство современных рожателей ранобе).

Как-то тут тихо. Нужно больше кликбейта в названии.

#38
11:53, 4 ноя. 2018

Зарисовки по синтаксису.

Для поддержки версий и расширений, в самом начале файла ставится специальный маркер, наподобие шейдерных #version+#extension:

language foobar-4.2 {
    my-cool-extension,
    reflection,
    protobuf}

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

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

local spam: String = "123"

эквивалентна
(local spam: String) = "123"

Достоинством такого подхода является возможность использовать объявления в любых местах, где генерируются новые значения.
out-аргументы функций:
polar_to_cartesian[
    rho,
    phi,
    x -> produce local x: Float,
    y -> produce local y: Float]
return x*x + y*y

Параметр цикла:
for local v, index -> local k in name_list do
    print[format[k, width -> 3], ": ", quote[v]]
end

Опциональное значение:
with local description: String from get[description_map, name] do
    print[description]
else
    print["?unknown item?"]
end

Ещё одно проявление синтаксического единообразия - один и тот же if-else используется как для управления контролем, так и в качестве тринарного оператора:
operator tostring[b: Boolean]: String
    return if b then "true" else "false" end
end

В вымышленном языке не требуется оператор-запятая, как в си - обычная последовательность уже является тем же выражением.
Чтобы уменьшить вероятность ошибки, большинство мест - условия в if, аргументы функций, стороны присваивания и прочие - принимают только одно выражение; однако, явными операторными скобками полные конструкции можно вставить и туда:
procedure main[]
    -- тело функции - само по себе expression-sequence
    local n = 1 -- правая сторона присваивания - единичный expression
    -- следующее выражение уже не является частью присваивания
    local m = do -- вся конструкция do-end - это один expression
        -- содержимое do-end - это expression-sequence
        n += 1
        print["n = ", n, "\n"]
        n += 1
    before
        2 * n
    end
    print[
        "m = ",
        do
            m += 1
            -- без do-end, в этом месте была бы ошибка вида "unfinished argument list, ] expected"
        before
            m
        end,
        "\n"]
end

В общем, главная идея состоит в том, что в вымышленном языке, между "expression" и "statement" нет никакой синтаксической разницы. Тем не менее, за счёт разделения операций "чтения" и "исполнения", между ними проводится семантическая разница.

#39
11:53, 4 ноя. 2018

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

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

polar_to_cartesian[
    rho,
    phi,
    x -> produce local x: Float,
    y -> produce local y: Float]

устанавливается в соответствие такое дерево:
\\invoke {
    target -> polar_to_cartesian,
    \\argument {
        value -> rho},
    \\argument {
        value -> phi},
    \\argument {
        target -> x,
        mode -> @produce,
        value -> \\local {
            name -> x,
            type -> Float}},
    \\argument {
        target -> y,
        mode -> @produce,
        value -> \\local {
            name -> y,
            type -> Float}}}

"Синтаксический литерал" - это конструкция такого рода, записанная посреди исходного кода.
Главное назначение синтаксических литералов - вставить конструкции, которые невозможно записать обычной грамматикой, в первую очередь - обращение к интринсикам для реализации базового функционала:
operator read[modify p: Port]: Byte
    return \\volatile-read {
        address -> p.address,
    }
end

operator write[modify p: Port, v: Byte]
    \\volatile-write {
        address -> p.address,
        value -> v,
    }
end


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

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

Наконец, для аннотирования уже существующих конструкций дополнительной информацией, язык предоставляет механизм атрибутов, аналогичный C++11.

import serial

class Quad
    local center: Vector3
    local u: Vector3
    local v: Vector3
    local [[ serial.ignore ]] [[ serial.default[-1] ]] index: Integer
    implements Serializable[Quad] with serial.behave[current class]
end


Атрибуты используются в первую очередь для передачи дополнительной информации через рефлексию. Помимо этого, они так же используются для управления оптимизатором.
class Vertex
    local position: Vector3
    local normal: Vector3
    local tangent: Vector3
    local bitangent: Vector3
    local texture1: Vector2
    local texture2: Vector2
end

class Mesh
    local [[ layout.soa[level -> 1, array -> layout.chunk[size -> 1024, align -> 32]] ]] vertices: List[Vertex]
end

#40
17:03, 4 ноя. 2018

Очень мноха текста, ниасилил.
Можно вкратце изложить что это и зачем оно надо?

#41
17:15, 4 ноя. 2018

Слишком мнохо кликбейта в названии, поэтому решил не читать тред. Кратко содержание можешь не излагать. Все равно читать не буду

#42
8:02, 5 ноя. 2018

Great V.
> Очень мноха текста, ниасилил.
Сожалею.

> Можно вкратце изложить что это и зачем оно надо?
Это идеи на тему языка программирования, которые я пытаюсь свести в единую непротиворечивую систему, прежде, чем приступать к реализации.
Назначение состоит в том, чтобы прокачать скилл програмиста, и, возможно, принести что-то полезное в программирование.

*Lain*
> Слишком мнохо кликбейта в названии, поэтому решил не читать тред. Кратко
> содержание можешь не излагать. Все равно читать не буду
Окей.

#43
12:18, 5 ноя. 2018

Delfigamer
А можно эти идеи записать в таблицу с кратким описанием и ссылочками на оригиналы? А то даже количество фич непонятно.
Вообще говоря можно для этого отдельную общую тему завести, где каждый сможет запостить свои идеи.

#44
12:55, 5 ноя. 2018

Great V.
Окей, переформулирую назначение.
Я хочу сделать язык программирования, чтобы он был в определённой степени быстрым, выразительным и надёжным. У меня есть эскизы составных частей языка, которые должны помочь ему достичь этих целей. Под "эскизом" я понимаю стадию проεкта.
На данный момент, эти эскизы не складываются в рабочую систему.
Сюда я их выкладываю на случай, если у кого-то другого появится фидбек. Например, Тарас направил на Аду - это было полезно.
В общем, по задумке, этот тред - это не просто помойка для идей, а всё-таки попытка родить нечто прекрасное.

Страницы: 1 2 3 4 526 Следующая »
ФлеймФорумПрограммирование