Это вторая статья из цикла “Разра­ба­тываем свой язык програм­ми­ро­вания на Java”, первую статью можно прочитать по ссылке.

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

Класси­фи­кация ошибок

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

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

Пример синтак­си­ческой ошибки (отсут­ствует закры­вающая кавычка):

println("Hello, World!)

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

var a = 1 + 2 * 3

Ожидая, что переменная a будет равна 9, но на самом деле она будет равна 7. Это проис­ходит из-за того, что оператор умножения имеет более высокий приоритет, чем оператор сложения. Семан­ти­ческая ошибка обычно может быть обнаружена во время отладки или обширного тести­ро­вания программы.

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

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

JimpleBaseVisitor

Для выявления ошибок и преду­пре­ждений нам понадо­бится, знакомый из первой статьи абстрактный класс JimpleBaseVisitor (сгене­ри­рован ANTLR), который по-умолчанию реализует интерфейс JimleVisitor. Он позволяет обходить AST-дерево (Abstract Syntax Tree) и на основе анализа его узлов мы будем решать ошибка, преду­пре­ждение или нормальная часть кода. По сути, диагно­стика ошибок почти не отличается от интер­пре­тации кода, кроме случаев когда нам нужно выполнять ввод/вывод или обращаться к внешним ресурсам. Например, если выпол­няется команда вывода в консоль, то наша задача проверить допустимый ли тип данных передается в качестве аргумента, без непосред­ственного вывода в консоль.

Создадим класс JimpleDiagnosticTool, который наследует JimleBaseVisitor и будет инкап­су­ли­ровать в себе всю логику поиска и хранения ошибок:

class JimpleDiagnosticTool extends JimpleBaseVisitor<ValidationInfo> {
    private Set<Issue> issues = new LinkedHashSet<>();
}

record Issue(IssueType type, String message, int lineNumber, int lineOffset, String details) {}

Данный класс содержит в себе список типа Issue, который представляет инфор­мацию о конкретной ошибке.

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

record ValidationInfo(ValidationType type, Object value) {}

enum ValidationType {
    /**
     * Expression returns nothing.
     */
    VOID,

    /**
     * Expression is String
     */
    STRING,

    /**
     * Expression is double
     */
    DOUBLE,

    /**
     * Expression is long
     */
    NUMBER,

    /**
     * Expression is boolean
     */
    BOOL,

    /**
     * Expression contains error and analysing in another context no makes sense.
     */
    SKIP,

    /**
     * When object can be any type. Used only in Check function definition mode.
     */
    ANY,

    /**
     * Tree part is function declaration
     */
    FUNCTION_DEFINITION
}

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

ValidationInfo checkBinaryOperatorCommon(ParseTree leftExp, ParseTree rightExp, Token operator) {
    ValidationInfo left = visit(leftExp);
    if (left.isSkip()) {
        return ValidationInfo.SKIP;
    }
    ValidationInfo right = visit(rightExp);
    if (right.isSkip()) {
        return ValidationInfo.SKIP;
    }
    // code omitted
}

Listeners vs Visitors

Перед тем как двигаться дальше, давайте посмотрим на еще один сгене­ри­ро­ванный ANTLR-ом интерфейс JimpleListener (шаблон Observer), который тоже может быть исполь­зован, если нам нужно обходить AST-дерево. В чем разница между ними? Самое большое различие между этими механизмами в том, что методы listener вызываются ANTLR-ом для каждого узла всегда, тогда как методы visitor должны обходить свои дочерние элементы явными вызовами. И если программист не вызывает visit() на дочерних узлах, то эти узлы не посещаются, т.е. у нас есть возмож­ность управлять обходом дерева. Например, в нашей реали­зации тело функции посещается сначала один раз полностью (режим checkFuncDefinition==true) для выявления ошибок во всей функции (все блоки if и else), и несколько раз с конкретными значе­ниями аргументов:

@Override
ValidationInfo visitIfStatement(IfStatementContext ctx) {
    // calc expression in "if" condition
    ValidationInfo condition = visit(ctx.expression());

    if (checkFuncDefinition) {
        visit(ctx.statement());
        // as it's just function definition check, check else statement as well
        JimpleParser.ElseStatementContext elseStatement = ctx.elseStatement();
        if (elseStatement != null) {
            visit(elseStatement);
        }
        return ValidationInfo.VOID;
    }

    // it's not check function definition, it's checking of certain function call
    if (condition.isBool() && condition.hasValue()) {
        if (condition.asBoolean()) {
            visit(ctx.statement());
        } else {
            JimpleParser.ElseStatementContext elseStatement = ctx.elseStatement();
            if (elseStatement != null) {
                visit(elseStatement);
            }
        }
    }

    return ValidationInfo.VOID;
}

Шаблон Visitor работает очень хорошо, если нам необходимо спроеци­ровать опреде­ленное значение для каждого узла дерева. Это именно то, что нам нужно.

Отлов синтак­си­ческих ошибок

Для того чтобы найти в коде некоторые синтак­си­ческие ошибки, нам необходимо реали­зовать интерфейс ANTLRErrorListener. Данный интерфейс содержит четыре метода, которые будут вызываться (парсером и/или лексером) в случае ошибки или неопре­де­ленного поведения:

interface ANTLRErrorListener {
    void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e);
    void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs);
    void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs);
    void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs);
} 

Название первого метода (syntaxError) говорит само за себя, он будет вызываться в случае синтак­си­ческой ошибки. Реали­зация довольно простая: нам нужно преоб­ра­зовать инфор­мацию об ошибке в объект типа Issue и добавить его в список ошибок:

@Override
void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
    int offset = charPositionInLine + 1;
    issues.add(new Issue(IssueType.ERROR, msg, line, offset, makeDetails(line, offset)));
}

Остальные три метода можно игнори­ровать. Также ANTLR сам реализует этот интерфейс (класс ConsoleErrorListener) и отправляет ошибки в стандартный поток ошибок (System.err). Чтобы отключить его и другие стандартные обработчики, нам необходимо вызвать метод removeErrorListeners у парсера и лексера:

    // убираем стандартные обработчики ошибок
    lexer.removeErrorListeners();
    parser.removeErrorListeners();

Другой тип синтак­си­ческих ошибок базируется на правилах конкретного языка. Например, в нашем языке функция иденти­фи­ци­руется по имени и количеству аргументов. Когда анали­затор встречает вызов функции, то он проверяет, существует ли функция с таким именем и количе­ством аргументов. Если нет, то он выдает ошибку. Для этого нам необходимо переопре­делить метод visitFunctionCall:

@Override
ValidationInfo visitFunctionCall(FunctionCallContext ctx) {
    String funName = ctx.IDENTIFIER().getText();
    int argumentsCount = ctx.expression().size();
    var funSignature = new FunctionSignature(funName, argumentsCount, ctx.getParent());
    // ищем функцию в контексте по сигнатуре (имя+количество аргументов)
    var handler = context.getFunction(funSignature);

    if (handler == null) {
        addIssue(IssueType.ERROR, ctx.start, "Function with such signature not found: " + funName);
        return ValidationInfo.SKIP;
    }

    // code omitted
}

Давайте проверим конструкцию if. Jimple требует, чтобы выражение в условии if было типа boolean:

@Override
ValidationInfo visitIfStatement(IfStatementContext ctx) {
    // visit expression
    ValidationInfo condition = visit(ctx.expression());
    // skip if expression contains error
    if (condition.isSkip()) {
        return ValidationInfo.SKIP;
    }

    if (!condition.isBool()) {
        addIssue(IssueType.WARNING, ctx.expression().start, "The \"if\" condition must be of boolean type only. But found: " + condition.type());
    }

    // code omitted
}

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

Поиск семан­ти­ческих ошибок

Как уже было сказано ранее, семан­ти­ческие ошибки сложны в поиске и часто могут быть найдены только во время отладки или тести­ро­вания программы. Однако, некоторые из них можно выявить на этапе компи­ляции. Например, если мы знаем, что функция X всегда возвращает значение 0, то мы можем выдать преду­пре­ждение, если в выражении деления в качестве делителя исполь­зуется данная функция. Деление на ноль обычно считается семан­ти­ческой ошибкой, поскольку деление на ноль не имеет смысла в математике.

Пример детек­ти­ро­вания ошибки “Деление на ноль”: сраба­тывает в случае когда в качестве делителя исполь­зуется выражение, которое всегда возвращает значение 0:

ValidationInfo checkBinaryOperatorForNumeric(ValidationInfo left, ValidationInfo right, Token operator) {
    if (operator.getType() == JimpleParser.SLASH && right.hasValue() && ((Number) right.value()).longValue() == 0) {
        // if we have value of right's part of division expression and it's zero
        addIssue(IssueType.WARNING, operator, "Division by zero");
    }

    // code omitted
}

Ошибки времени исполнения

Ошибки времени испол­нения также тяжело или даже невоз­можно обнаружить на этапе компиляции/интерпретации. Однако, некоторые подобные ошибки всё же можно выявить. Например, если функция вызывает сама себя (напрямую или через другую функцию), то это может привести к ошибке перепол­нения стека (StackOverflow). Первое что нам нужно сделать – это объявить список (Set), где мы будем сохранять функции, которые находятся в процессе вызова в данной момент. Саму проверку можно (и нужно) разме­стить в методе handleFuncInternal обработки вызова функции. В начале этого метода находится проверка наличия текущего FunctionDefinitionContext (контекст объяв­ления функции) в списке уже вызванных функций, и если да, то регистрируем преду­пре­ждение (Warning) и прерываем дальнейшую обработку функции. Если нет, то добавляем текущий контекст в наш список, и далее следует остальная логика. При выходе из handleFuncInternal нужно удалить из списка текущий контекст функции. Здест следует обратить внимание, что в данном случае мы не только выявили потен­ци­альный StackOverflow, но и избавились от этой же ошибки во время проверки кода, а именно при выполении зацик­ли­вания метода handleFuncInternal.

Set<FunctionDefinitionContext> calledFuncs = new HashSet<>();

ValidationInfo handleFuncInternal(List<String> parameters, List<Object> arguments, FunctionDefinitionContext ctx) {
    if (calledFuncs.contains(ctx)) {
        addIssue(IssueType.WARNING, ctx.name, String.format("Recursive call of function '%s' can lead to StackOverflow", ctx.name.getText()));
        return ValidationInfo.SKIP;
    }
    calledFuncs.add(ctx);
    
    // other checkings

    calledFuncs.remove(ctx);

    // return resulting ValidationInfo
}

Анализ потока управления/данных

Для более глубокого иссле­до­вания программного кода, оптими­зации и выявления сложных ошибок также используют Анализ потока управ­ления (Control-flow analysis) и Анализ потока данных (Data-flow analysis).

Анализ потока управ­ления фокуси­руется на понимании того, какие части программы выпол­няются в зависи­мости от различных условий и управ­ляющих структур, таких как условные операторы (if-else), циклы и переходы. Он позволяет выявить пути выпол­нения программы и иденти­фи­ци­ровать потен­ци­альные ошибки, связанные с непра­вильной логикой управ­ления потоком. Например, недости­жимый код или потен­ци­альные точки зависания программы.

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

Резюме

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

Весь исходный код интер­пре­татора можно посмотреть по ссылке.

Ссылки

В мире Java библиотека JAXB уже давно (как сообщает нам wikipedia с 2006 года) и по праву является распро­стра­ненным и очень удобным инстру­ментом для XML-cериа­ли­зации объектов и даже объектных моделей. В интернете есть масса примеров и целые сторонние библиотеки, постро­енные на JAXB — взять хотя бы docx4j (http://www.docx4java.org/trac/docx4j), которая работает с моделью документа в Open-XML формате. Так же много инфор­мации можно почерпнуть у самих авторов JAXBhttps://jaxb.java.net/. Без труда найдутся в интернете прекрасные готовые исходники для генерации классов Java по XML-модели. Примеров масса и можно увлека­тельно провести время, изучая возмож­ности JAXB.

Но практи­чески все примеры приво­дятся для одного простого класса, который сериа­лизуют в XML, что называется, с нуля. А что делать, если уже есть некая готовая объектная модель со сложной струк­турой насле­до­вания и зависи­мостей, которые запрещены к изменениям? Где следует применить JAXB-аннотации? Куда их добавить — непосред­ственно в каждый из доброй полторы сотни классов, либо в один базовый? Если в каждый класс иерархии, то целесо­об­разна ли такая переделка с точки зрения объема выпол­ненной работы? Если в один-два базовых, то будет ли вообще работать XML-сериа­ли­зация? Да, действи­тельно много вопросов, и посмотрим, как JAXB с этим справ­ляется для конкретной задачи проекта.

Например, есть модель документа: Root – некий корневой элемент, контейнер для других элементов. Он может содержать Paragraph, который также является контей­нером для элементов Text. В свою очередь все элементы являются контей­нерами атрибутов, например у Paragraph это Alignment и Indent, у Text – Bold, Italic, FontName, FontSize и Color. Конечно, это неполный перечень, но в качестве примера доста­точный. Кроме того, условием для контей­неров элементов – Root и Paragraph, является то, что они могут содержать неогра­ни­ченное количество подэле­ментов, т.е. Root содержит много Paragraph, а Paragraph — много Text. Но контейнеры атрибутов могут содержать каждый атрибут в единственном экзем­пляре. Например, зачем элементу Text несколько FontName или Bold?

Таким образом, есть уже готовая модель элементов и атрибутов, со своей специ­фи­ческой бизнес-логикой, полями, методами и т.д.

Начнем с базового абстрактного атрибута:

 

Обязан­ности:

  • Разра­ботка стратегии и плани­ро­вание продви­жения программных продуктов(deskctop и mobile)
  • Внедрение стратегии, плани­ро­вание, работа по плану
  • Такти­ческое управ­ление продуктом: тарифы, акции, стиму­ли­ро­вание роста продаж и доходов
  • Разра­ботка сопут­ствующих докумен­таций, презен­таций, флайеров(текст, без дизайна)
  • Прозрач­ность работы, отчетность

Требо­вания:

  • Опыт работы в марке­тинге или управ­лении проектами от 2 лет
  • Хороший разго­ворный и письменный английский язык, опцио­нально немецкий
  • Знание инстру­ментов интернет-маркетинга
  • Умение грамотно поставить задачу группе програм­мистов, SEO writers
  • Личные качества: анали­ти­ческие способ­ности, умение работать самосто­я­тельно и в команде, креатив­ность, нацелен­ность на высокий результат

Условия:

  • Преми­ро­вание при выпол­нении плана
  • % от продаж
 

Тип занятости:

  • Удалённая работа
  • Полная занятость, полный день
  • Коман­ди­ровки в Германию, Мюнхен.
  • Участие в между­на­родных выставках

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

  • Заказное ПО. Продукт разра­ботки предна­зна­чается опреде­ленному заказчику, который финан­сирует процесс разра­ботки и определяет требо­вания к программному продукту. По испол­нении заказа все права на продукт переходят к заказчику, который в будущем для совер­шен­ство­вания данного программного продукта может привлечь либо прежнего, либо любого другого исполнителя.
  • Инвести­ци­онное ПО. Отличается от заказного отсут­ствием конкретного заказчика (хотя разра­ботка может вестись и на средства, выделенные внешним инвестором). Обычно в этом случае права на программный код сохраняет испол­нитель. Благодаря этому создаются условия для работы по посто­янному совер­шен­ство­ванию данного ПО, выпуска его новых версий и обнов­лений для действующих. Вероятна и органи­зация непре­рывно работающей службы техни­ческой поддержки. Коммер­ческий эффект дости­гается обычно путем продажи программного продукта заинте­ре­со­ванным пользо­ва­телям. Почти все программные продукты, представ­ленные на свободном рынке относятся к данному типу ПО.
  • Игровое ПО. Это условное название, поскольку большинство компью­терных игр можно отнести скорее к инвести­ци­онному типу ПО. Но в целом данный тип ПО харак­те­ри­зуется тем, что в процессе его разра­ботки отсут­ствует (или до минимума ограничена по сроку) фаза сопро­вож­дения, что сильно влияет на экономику процесса разра­ботки и его организацию.
  • Встро­енное ПО. Это прикладное ПО, созда­ваемое под конкретные техни­ческие изделия и постав­ляемое вместе с ними. Сопро­вож­дению, как правило, не подлежит, но харак­те­ри­зуется повышенными требо­ва­ниями к качеству, надеж­ности и безот­каз­ности. Объяс­няется это тем, что отзыв партии изделий произ­во­ди­телем – ситуация очень непри­ятная, несущая серьезные прямые коммер­ческие и репута­ци­онные потери для произ­во­дителя изделий.

Этапы разра­ботки ПО

Любой процесс, в том числе и разра­ботка ПО состоит из какого-то количества опреде­ленных этапов. Процесс разра­ботки ПО отличается тем, что обычно к работам над следующим этапом приступают, когда уровень готов­ности преды­дущего достигает около 80–90%. В наибольшей степени данное обсто­я­тельство относится к разра­ботке требо­ваний, поскольку именно в них какой-то уровень неопре­де­лен­ности иногда сохра­няется практи­чески до завер­шения проекта. Этот момент является серьезным фактором риска, и должен непре­рывно контролироваться.

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

  1. Подго­товка. Опреде­ление общей концепции системы, востре­бо­ван­ности, реали­зу­е­мости и т.д.
  2. Разра­ботка конкретных требо­ваний к программному продукту.
  3. Разра­ботка архитектуры. Целью данного этапа является создание физической и логической архитектуры верхнего уровня, способной выполнить все требо­вания к разра­ба­ты­ва­емому программному продукту. На данном этапе возможна коррекция концепции, перво­на­чального техни­че­ского решения и требо­ваний. По его завер­шении, возможно начало парал­лельных работ над созданием следующей версии данного программного продукта.
  4. Этап поставки. Может включать несколько проме­жу­точных этапов, в зависи­мости от масштаб­ности проекта. На данной стадии проект приоб­ретает форму относи­тельно готового программного продукта.
  5. Опытная эксплу­а­тация. Продол­жи­тель­ность данного этапа также зависит от масштаб­ности проекта. На этом этапе выявляются и устра­няются недочеты, баги, уязви­мости и т.д. Прове­ряется качество работы ПО в различных условиях эксплу­а­тации, совме­сти­мость с другими программными продуктами и т.д. Данный этап можно также назвать этапом основной отладки и оптими­зации программного продукта.
  6. Этап коммер­ческой реали­зации, промыш­ленной эксплу­а­тации и т.д. Сопро­вож­дение и техни­ческая поддержка.
  7. Вывод из эксплу­а­тации. Прекра­щение техни­ческой поддержки и выпуска обновлений.
  8. Завер­шение работ.

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

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

 Под термином «корпо­рация» обычно понимают очень крупную компанию, количество сотруд­ников которой исчис­ляется в тысячах, разбро­санную геогра­фи­чески и занима­ю­щуюся бизнесом в нескольких отраслях. Понятно, что к инфор­ма­ци­онным системам (ИС), созда­ваемым для обслу­жи­вания нужд подобных бизнес-структур, предъ­яв­ляются особые требования.

Целью создание КИС является обеспе­чение инфор­ма­ци­онной поддержки всех видов деятель­ности, которыми занимается данная корпо­рация, на всех уровнях: учета, управ­ления, плани­ро­вания, прогно­зи­ро­вания и т.д.

Читать далее

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

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

Читать далее

С 15.05.2017 по 15.07.2017 наша компания совместно с фирмой ООО «Финансово-анали­ти­ческие системы» из Екате­рин­бурга проводит кампанию по тести­ро­ванию компо­ненты программного обеспе­чения Scell.

Принять участие в тести­ро­вании может любой желающий, доста­точно соблюсти некоторые правила участия.

Призы

3 самых активных участника получат вознаграждения.

  • 1е место — iPhone 7 Plus 32GB или Samsung Galaxy S8 64GB (по желанию победителя)
  • 2ое место — Huawei P10 64GB
  • 3е место — Honor 6X 64GB

Вручение призов будет прово­диться фирмой ООО «Финансово-анали­ти­ческие системы», для участ­ников не из Екате­рин­бурга призы высылаются почтой.

10 Ноября

В августе 2009 мы выиграли тендер на обслу­жи­вание проекта “Pflege und Weiterentwicklung Textsystem_RP” министерства юстиции Rheinland-Pfalz. И уже в ноябре 2009 получили право на работу с этим проектом.