
Как и многие молодые разработчики, когда появляется желание найти работу/стажировку — я смотрю в сторону крутых IT компаний.
Недавно я попробовал попасть в ряды JetBrains и под катом готов поделиться полученным опытом.
Почему «почти» удалось?
Наверняка сразу у вас встает такой вопрос.
На мой взгляд у меня имеется неплохое резюме с кучей ачивок и хороший скилл, который я день за днем совершенствую последние 8-9 лет.
Я выполнил тестовое задание (и как мне кажется хорошо), ранее посещал офис JB, который благо находится в моем городе, общался с HH и некоторыми разработчиками компании и в итоге получил отказ на стажировку без каких-либо комментариев.
Скорее всего причина таится в том, что на стажировку JetBrains отбирает исключительно студентов, а я на данный момент только выпустился из 11-го и сдаю один за другим экзамены.
Что ж, это повод в течении ещё целого года поднатаскать себя и подать заявку на следующий год.
Разбор тестового задания
Сроки подачи заявок на стажировку и проверки тестовых заданий закончились, а это значит, что все, кто их решил, включая меня — могут выложить разбор этих заданий, чтобы в следующем году любой желающий студент мог перед началом стажировок JB ознакомиться с примерным уровнем заданий, с которыми ему придется столкнуться и в случае чего подтянуть свои знания.
Я подавал заявку на стажировку в команду разработки отладчика корутин для Kotlin.
Задачей этой команды на стажировке у тех, кто на неё попал в этом году будет доработка этой части отладчика и её интеграция с IDE.
Задание было немного ожидаемым — написать отладчик для небольшого ЯП.
Я бы не сказал, что оно сложное, скорее наоборот. Оно не требует каких-либо глубоких знаний теории построения трансляторов и крутого скилла. Но тем не менее, те, кто подают заявку на стажировку по этому направлению, как минимум должны владеть этими основами и без проблем справиться с этим заданием. Я был удивлен, когда решил поискать на github'е по ключевым словам решения моих «конкурентов» и нашел 1-2 более-менее на вид рабочих решения против около 6-7 пустых репозиториев либо с парой кусков кода, после которых люди опустили руки. Возможно я плохо искал, но тем не менее результаты меня не порадовали. Если этот пост будут читать люди, которые забросили это задание — не надо в будущем так делать. В крайнем случае достаточно было посидеть над заданием пару дней и я уверен — вы бы с ним справились.
Сам текст задания
Задача: реализовать пошаговое исполнение кода для тривиального языка программирования Guu.
Внимание: в описании ниже заведомо опущены некоторые существенные моменты. Как правило, они остаются на ваше усмотрение. Если будет совсем непонятно, пишите на (тут почта, которую я решил убрать).
Программа на Guu состоит из набора процедур. Каждая процедура начинается со строки sub (subname) и завершается объявлением другой процедуры (или концом файла, если процедура в файле последняя). Исполнение начинается с sub main.
Тело процедуры – набор инструкций, каждая из которых находится на отдельной строке. В начале строки могут встречаться незначимые символы табуляции или пробелы. Пустые строки игнорируются. Комментариев в Guu нет.
В Guu есть лишь три оператора: — set (varname) (new value) – задание нового целочисленного значения переменной. — call (subname) – вызов процедуры. Вызовы могут быть рекурсивными. — print (varname) – печать значения переменной на экран.
Переменные в Guu имеют глобальную область видимости. Программа ниже выведет на экран строку a = 2.
sub main
set a 1
call foo
print a
sub foo
set a 2
А вот простейшая программа с бесконечной рекурсией:
sub main
call main
Необходимо написать пошаговый интерпретатор для Guu. При его запуске отладчик должен останавливаться на строчке с первой инструкцией в sub main и ждать команд от пользователя. Минимально необходимый набор команд отладчика:
i – step into, отладчик заходит внутрь call (subname).
o – step over, отладчик не заходит внутрь call.
trace – печать stack trace исполнения с номерами строк, начиная с main…
var – печать значений всех объявлённых переменных.
Формат общения пользователя с отладчиком остаётся на выше усмотрение. Вы можете выбрать как минималистичный GDB-like интерфейс, так и консольный или графический UI. Названия команд отладчика можно при желании изменить.
Для решения этой задачи вы можете использовать любой язык программирования из TIOBE TOP 50 и open-source компилятором/интерпретатором.
При оценке работы будет оцениваться:
Общая работоспособность программы;
Качество исходного кода и наличие тестов;
Простота расширения функциональности (например, поддержка новых операторов языка или инструкций отладчика).
Решение с инструкцией по его сборке нужно опубликовать в Git-репозиторий (например, на GitHub или BitBucket). В ответе нужно указать ссылку на репозиторий. Подойдёт и ссылка на приватный GitHub-репозиторий, только в него потребуется добавить меня.
Внимание: в описании ниже заведомо опущены некоторые существенные моменты. Как правило, они остаются на ваше усмотрение. Если будет совсем непонятно, пишите на (тут почта, которую я решил убрать).
Программа на Guu состоит из набора процедур. Каждая процедура начинается со строки sub (subname) и завершается объявлением другой процедуры (или концом файла, если процедура в файле последняя). Исполнение начинается с sub main.
Тело процедуры – набор инструкций, каждая из которых находится на отдельной строке. В начале строки могут встречаться незначимые символы табуляции или пробелы. Пустые строки игнорируются. Комментариев в Guu нет.
В Guu есть лишь три оператора: — set (varname) (new value) – задание нового целочисленного значения переменной. — call (subname) – вызов процедуры. Вызовы могут быть рекурсивными. — print (varname) – печать значения переменной на экран.
Переменные в Guu имеют глобальную область видимости. Программа ниже выведет на экран строку a = 2.
sub main
set a 1
call foo
print a
sub foo
set a 2
А вот простейшая программа с бесконечной рекурсией:
sub main
call main
Необходимо написать пошаговый интерпретатор для Guu. При его запуске отладчик должен останавливаться на строчке с первой инструкцией в sub main и ждать команд от пользователя. Минимально необходимый набор команд отладчика:
i – step into, отладчик заходит внутрь call (subname).
o – step over, отладчик не заходит внутрь call.
trace – печать stack trace исполнения с номерами строк, начиная с main…
var – печать значений всех объявлённых переменных.
Формат общения пользователя с отладчиком остаётся на выше усмотрение. Вы можете выбрать как минималистичный GDB-like интерфейс, так и консольный или графический UI. Названия команд отладчика можно при желании изменить.
Для решения этой задачи вы можете использовать любой язык программирования из TIOBE TOP 50 и open-source компилятором/интерпретатором.
При оценке работы будет оцениваться:
Общая работоспособность программы;
Качество исходного кода и наличие тестов;
Простота расширения функциональности (например, поддержка новых операторов языка или инструкций отладчика).
Решение с инструкцией по его сборке нужно опубликовать в Git-репозиторий (например, на GitHub или BitBucket). В ответе нужно указать ссылку на репозиторий. Подойдёт и ссылка на приватный GitHub-репозиторий, только в него потребуется добавить меня.
Я пишу на C++, Java и Object Pascal.
Сначала были мысли написать все на моем же ЯП (Mash), но я подумал, что это будет не очень удобно проверять сотруднику JB, да и заявку я подал за 2 дня до закрытия подачи (экзамены все-же...), да и за окном уже был вечер — решил я все быстренько написать на более известных языках.
Для решения задачи Pascal на мой взгляд подходит больше всего, как минимум из-за наиболее удобной реализации строк…
По крайней мере для меня. К тому же он находится в TIOBE TOP 50, так что я смело запустил IDE, а именно — Lazarus, т.к. он не коммерческий :) и приступил к решению задачи.
Несмотря на то, что JB дают аж целых 7 дней, по времени у меня в сумме ушло около часа, а проект получился примерно в 500 строк кода.
С чего начать?
Прежде всего нужно представить, как будет в итоге работать отладка кода.
Нам нужно реализовать пошаговое выполнение кода — значит каждая инструкция должна быть представлена в виде структуры/класса и в общем инструкции должны выглядеть как список этих классов или, как в моей реализации — ссылаться друг на друга образуя последовательность (позже распишу почему я так сделал).
Чтобы получить эту последовательность, нашему отладчику нужно обработать код на предложенном языке, значит нам также нужно реализовать небольшой парсер, а также синтаксический и семантический анализ кода.
Начнем с реализации парсера. Т.к. язык Guu состоит из набора токенов, разделяемых пробелом, то логично первым делом написать небольшой и простой токенайзер:
function GetToken(s: string; tokenNum: word): string; var p: word; begin s := Trim(s); s := StringReplace(s, ' ', ' ', [rfReplaceAll]); while tokenNum > 1 do begin p := Pos(' ', s); if p > 0 then Delete(s, 1, p) else begin s := ''; break; end; dec(tokenNum); end; p := Pos(' ', s); if p > 0 then Delete(s, p, Length(s)); Result := s; end;
Далее объявляем enum из токенов:
type TGuuToken = (opSub, opSet, opCall, opPrint, opUnknown); const GuuToken: array[opSub..opPrint] of string = ( 'sub', 'set', 'call', 'print' );
И сам класс инструкции, в которую будем разбирать строки кода:
type TGuuOp = class public OpType : TGuuToken; OpArgs : TStringList; OpLine : Cardinal; OpUnChangedLine: string; NextOp : TGuuOp; OpReg : Pointer; function Step(StepInto: boolean; CallBacks: TList; Trace: TStringList): TGuuOp; constructor Create(LineNum: Cardinal; Line:string); destructor Destroy; override; end;
В OpType будет храниться инструкция, в OpArgs — остальные части конструкции.
OpLine, OpUnChangedLine — информация для отладчика.
NextOp — указатель на следующую инструкцию. Если он равен nil (null в Pascal), то далее нет инструкций и нужно завершить выполнение кода, либо вернуться по callback стеку.
OpReg — небольшой указатель-регистр, который будет использоваться далее для небольшой оптимизации выполнения кода.
После того, как было написано объявление класса — я решил, что наиболее компактным и красивым решением было бы дописать парсер и небольшой синтаксический анализ в его конструкторе, что я дальше и сделал:
constructor TGuuOp.Create(LineNum: Cardinal; Line:string); (* * That method parse code line. *) var s: string; w: word; begin inherited Create; OpArgs := TStringList.Create; OpLine := LineNum; OpUnChangedLine := Line; NextOp := nil; OpReg := nil; s := GetToken(Line, 1); OpType := TGuuToken(AnsiIndexStr(s, GuuToken)); case OpType of opSub : begin // sub <name> s := GetToken(Line, 2); if Length(s) > 0 then OpArgs.Add(s) else begin writeln('[Syntax error]: Invalid construction "sub" at line ', OpLine, '.'); halt; end; if Length(GetToken(Line, 3)) > 0 then begin writeln('[Syntax error]: Invalid construction "', Line, '" at line ', OpLine, '.'); halt; end; end; opSet : begin // set <var> <value> OpArgs.Add(GetToken(Line, 2)); OpArgs.Add(GetToken(Line, 3)); w := 1; while w < Length(OpArgs[1]) + 1 do begin if not (OpArgs[1][w] in ['0'..'9']) then begin writeln('[Syntax error]: Invalid variable assigment "', Line, '" at line ', OpLine, '.'); halt; end; inc(w); end; if (Length(OpArgs[0]) = 0) or (Length(OpArgs[1]) = 0) or (Length(GetToken(Line, 4)) > 0) then begin writeln('[Syntax error]: Invalid construction "', Line, '" at line ', OpLine, '.'); halt; end end; opCall : begin // call <name> s := GetToken(Line, 2); if Length(s) > 0 then OpArgs.Add(s) else begin writeln('[Syntax error]: Invalid construction "call" at line ', OpLine, '.'); halt; end; if Length(GetToken(Line, 3)) > 0 then begin writeln('[Syntax error]: Invalid construction "', Line, '" at line ', OpLine, '.'); halt; end; end; opPrint: begin // print <var> s := GetToken(Line, 2); if Length(s) > 0 then OpArgs.Add(s) else begin writeln('[Syntax error]: Invalid construction "print" at line ', OpLine, '.'); halt; end; if Length(GetToken(Line, 3)) > 0 then begin writeln('[Syntax error]: Invalid construction "', Line, '" at line ', OpLine, '.'); halt; end; end; else begin writeln('[Syntax error]: Invalid token "', s, '" at line ', OpLine, '.'); halt; end; end; end; destructor TGuuOp.Destroy; begin FreeAndNil(OpArgs); inherited; end;
Тут мы по сути проверяем начало конструкции (т.е. первое слово), а затем смотрим на остальные токены и их количество. Если с кодом что-то явно не правильно — выводим ошибку.
В главном куске кода мы просто читаем из файла код в TStringList, построчно вызываем конструкторы TGuuOp и сохраняем указатели на экземпляры классов в GuuOps: TList.
Объявления:
var LabelNames: TStringList; GuuOps, GuuVars: TList; SubMain: TGuuOp = nil;
Совместно с парсингом кода хорошо бы выполнить ещё пару действий:
procedure ParseNext(LineNum: Cardinal; Line: string); (* * Parsing code lines and define variables and labels. *) var Op: TGuuOp; GV: TGuuVar; c: cardinal; begin if Trim(Line) <> '' then begin Op := TGuuOp.Create(LineNum, Line); GuuOps.Add(Op); case Op.OpType of opSet: begin // define variable and/or optimisation var calling GV := nil; c := 0; while c < GuuVars.Count do begin if TGuuVar(GuuVars[c]).gvName = Op.OpArgs[0] then begin GV := TGuuVar(GuuVars[c]); break; end; inc(c); end; if GV = nil then begin GV := TGuuVar.Create(Op.OpArgs[0]); GuuVars.Add(GV); end; Op.OpReg := GV; end; opSub: begin // Check for label dublicade declaration if Op.OpArgs[0] = 'main' then SubMain := Op; if LabelNames.IndexOf(Op.OpArgs[0]) <> -1 then begin writeln('[Error]: Dublicate sub "', Op.OpArgs[0], '" declaration at line ', Op.OpLine, '.'); halt; end else LabelNames.Add(Op.OpArgs[0]); end; end; end; end;
На данном этапе можно проверить точки входа на момент переопределения и вспомнить про OpReg — его я использовал для хранения указателя на Guu переменную.
К слову о переменных — вынес этот небольшой кусок кода в отдельный юнит:
unit uVars; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TGuuVar = class public gvName: string; gvVal: variant; constructor Create(VarName: string); end; implementation constructor TGuuVar.Create(VarName: string); begin inherited Create; gvName := VarName; gvVal := 0; end; end.
Теперь у нас есть распарсенный код, который по синтаксису вроде бы правильный. Осталось его проанализировать и можно приступать к выполнению и самому главному — отладке.
Далее следует реализовать небольшой семантический анализ и попутно подготовить все к выполнению и отладке кода:
procedure CheckSemantic; (* * Semantic analyse and calls optimisation. *) var c, x: cardinal; op: TGuuOp; begin if GuuOps.Count > 0 then begin if TGuuOp(GuuOps[0]).OpType <> opSub then begin writeln('[Error]: Operation outside sub at line ', TGuuOp(GuuOps[0]).OpLine, '.'); halt; end; c := 0; while c < GuuOps.Count do begin case TGuuOp(GuuOps[c]).OpType of opSub:; opCall: begin TGuuOp(GuuOps[c - 1]).NextOp := TGuuOp(GuuOps[c]); x := 0; op := nil; while x < GuuOps.Count do begin if TGuuOp(GuuOps[x]).OpType = opSub then if TGuuOp(GuuOps[x]).OpArgs[0] = TGuuOp(GuuOps[c]).OpArgs[0] then begin op := TGuuOp(GuuOps[x]); break; end; inc(x); end; if op <> nil then TGuuOp(GuuOps[c]).OpReg := op else begin writeln('[Error]: Calling to not exist sub "', TGuuOp(GuuOps[c]).OpArgs[0], '" at line ', TGuuOp(GuuOps[c]).OpLine, '.'); halt; end; end; opPrint: begin TGuuOp(GuuOps[c - 1]).NextOp := TGuuOp(GuuOps[c]); x := 0; while x < GuuVars.Count do begin if TGuuVar(GuuVars[x]).gvName = TGuuOp(GuuOps[c]).OpArgs[0] then begin TGuuOp(GuuOps[c]).OpReg := TGuuVar(GuuVars[x]); break; end; inc(x); end; if TGuuOp(GuuOps[c]).OpReg = nil then begin writeln('[Error]: Variable "', TGuuOp(GuuOps[c]).OpArgs[0], '" for print doesn''t exist at line ', TGuuOp(GuuOps[c]).OpLine, '.'); end; end; else TGuuOp(GuuOps[c - 1]).NextOp := TGuuOp(GuuOps[c]); end; inc(c); end; end; end;
В TGuuOp.NextOp каждого токена записываем указатель на следующий за ним токен.
Для опкода call делаем все хитро и просто — в NextOp записываем указатель на вызываемую точку входа.
Также проверяем выводимые переменные через инструкцию print…
Может быть их не объявили перед выводом?
Теперь нужно реализовать выполнение кода. Возвращаемся к классу TGuuOp и реализуем метод Step:
function TGuuOp.Step(StepInto: boolean; CallBacks: TList; Trace: TStringList): TGuuOp; (* * That method execute instruction. *) var Op: TGuuOp; CBSize: Cardinal; begin case OpType of opSub: begin Trace.Add('-> Sub "' + OpArgs[0] + '"'); Result := NextOp; end; opCall: begin if StepInto then begin if NextOp <> nil then CallBacks.Add(NextOp); Result := TGuuOp(OpReg); end else begin Op := TGuuOp(OpReg); CBSize := CallBacks.Count; while ((Op <> nil) or (CallBacks.Count > CBSize)) and (Trace.Count < STACK_SIZE) do begin if Op = nil then begin Op := TGuuOp(CallBacks[CallBacks.Count - 1]); CallBacks.Delete(CallBacks.Count - 1); Trace.Delete(Trace.Count - 1); end; Op := Op.Step(StepInto, CallBacks, Trace); end; Result := NextOp; end; end; opPrint: begin writeln(TGuuVar(OpReg).gvName, ' = ', TGuuVar(OpReg).gvVal); Result := NextOp; end; opSet: begin TGuuVar(OpReg).gvVal := OpArgs[1]; Result := NextOp; end; end; end;
Чтобы избежать access violation в случае зацикливания — лучше ограничить стек, что я и сделал.
Константа STACK_SIZE = 2048, объявленная выше как раз отвечает за это.
Теперь наконец настало время написать основной код нашего отладчика:
var code: TStringList; c: Cardinal; cmd: string; CallBacks: TList; Trace: TStringList; DebugMode: boolean = true; begin if ParamCount > 0 then begin // Initialisation if not FileExists(ParamStr(1)) then begin writeln('[Error]: Can''t open file "', ParamStr(1), '".'); halt; end; if ParamCount > 1 then if LowerCase(ParamStr(2)) = '/run' then DebugMode := false; code := TStringList.Create; code.LoadFromFile(ParamStr(1)); GuuOps := TList.Create; GuuVars := TList.Create; // Parsing and preparing LabelNames := TStringList.Create; c := 0; while c < code.Count do begin ParseNext(c + 1, Trim(code[c])); inc(c); end; FreeAndNil(LabelNames); CheckSemantic; if SubMain = nil then begin writeln('[Error]: Sub "main" doesn''t exist!'); halt; end; // Start code execution CurrentOp := SubMain; CallBacks := TList.Create; Trace := TStringList.Create; if DebugMode then begin //Out code and features ClrScr; writeln('Code for debugging:'); writeln('.....'); c := 0; while c < code.Count do begin writeln(FillSpaces(IntToStr(c + 1), 4), '| ', code[c]); inc(c); end; writeln('"""""'); FreeAndNil(code); writeln(sLineBreak, 'Features:', sLineBreak, '* i - step into.', sLineBreak, '* o - step over.', sLineBreak, '* trace - print stack trace.', sLineBreak, '* var - print variables list.', sLineBreak, '* x - exit.', sLineBreak); // Execution loop while ((CurrentOp <> nil) or (CallBacks.Count > 0)) and (Trace.Count < STACK_SIZE) do begin write('Line ', CurrentOp.OpLine, ' ~> '); readln(cmd); // Execute commands if cmd = 'i' then CurrentOp := CurrentOp.Step(true, CallBacks, Trace) else if cmd = 'o' then CurrentOp := CurrentOp.Step(false, CallBacks, Trace) else if cmd = 'trace' then begin writeln('| Trace:'); c := 0; while c < Trace.Count do begin writeln('| ', Trace[c]); inc(c); end; writeln('| -> Line ', CurrentOp.OpLine, ': "', CurrentOp.OpUnChangedLine, '".') end else if cmd = 'var' then begin writeln('| Variables list:'); c := 0; while c < GuuVars.Count do begin writeln('| ', TGuuVar(GuuVars[c]).gvName, ' = ', TGuuVar(GuuVars[c]).gvVal); inc(c); end; end else if cmd = 'x' then halt; // Check for method end & make callback if (CurrentOp = nil) and (CallBacks.Count > 0) then begin CurrentOp := TGuuOp(CallBacks[CallBacks.Count - 1]); CallBacks.Delete(CallBacks.Count - 1); Trace.Delete(Trace.Count - 1); end; end; end else begin // Only run mode (/run) FreeAndNil(code); while ((CurrentOp <> nil) or (CallBacks.Count > 0)) and (Trace.Count < STACK_SIZE) do begin CurrentOp := CurrentOp.Step(false, CallBacks, Trace); if (CurrentOp = nil) and (CallBacks.Count > 0) then begin CurrentOp := TGuuOp(CallBacks[CallBacks.Count - 1]); CallBacks.Delete(CallBacks.Count - 1); Trace.Delete(Trace.Count - 1); end; end; end; if Trace.Count >= STACK_SIZE then writeln('[Runtime error]: Stack overflow!'); FreeAndNil(CallBacks); FreeAndNil(Trace); end else writeln( 'Guu debugger v1.0.', sLineBreak, 'Author: Pavel Shiryaev (@RoPi0n).', sLineBreak, 'Run: svmc guu_debugger.vmc <guu source file> [arg]', sLineBreak, 'Args:', sLineBreak, ' /run - Run Guu code.' ); end.
По условию задания интерфейс можно реализовать как угодно.
Можно было бы реализовать полноценный UI, прикрутить SynEdit к проекту, но на мой взгляд — это пустой труд, который не отразит скилл, да и к тому же, за который не заплатят :)
Так что я ограничился небольшим консольным UI.
Код выше не является чем-то сложным, так что его можно оставить без комментариев. В нем мы берем готовые TGuuOp'сы и вызываем их Step.
Скрины решенной задачки:


Вывод информации об ошибках:


Ссылка на репозиторий моего решения: клац
Итоги
Итогов особо нет. Придется мне посвятить большую часть лета насыщенному отдыху и поиску вуза (ну, на случай если ЕГЭ я сдам хорошо, конечно), вместо двух месяцев работы и обучения в команде JetBrains.
Возможно в следующем году на Хабре появится новый пост, уже описывающий процесс стажировки в JB либо в другой интересной мне компании :)
