Estágio no JetBrains e como eu quase consegui entrar nele

imagem

Como muitos desenvolvedores jovens, quando há um desejo de encontrar um emprego / estágio, olho na direção de empresas legais de TI.

Recentemente, tentei entrar no ranking do JetBrains e, sob o corte, estou pronto para compartilhar minha experiência.

Por que "quase" teve sucesso?


Certamente você imediatamente tem essa pergunta.

Na minha opinião, tenho um bom currículo com muitas conquistas e uma boa habilidade, que venho aprimorando nos últimos 8 a 9 anos, dia após dia.

Concluí a tarefa de teste (e me parece bom), visitei anteriormente o escritório da JB, localizado em minha cidade, conversei com HH e alguns desenvolvedores da empresa e, como resultado, foi recusado um estágio sem nenhum comentário.

Provavelmente, o motivo está no fato de o JetBrains selecionar estudantes exclusivamente para o estágio, e no momento em que acabei de me formar no 11º e passar nos exames um após o outro.

Bem, esta é uma ocasião para outro ano inteiro se levantar e se candidatar ao próximo ano.

Análise da tarefa de teste


Os prazos para o envio de inscrições para estágios e testes de testes terminaram, o que significa que todos que os resolveram, inclusive eu, podem postar uma análise dessas tarefas para que no próximo ano qualquer aluno interessado possa se familiarizar com o nível aproximado de tarefas antes de iniciar os estágios JB, com que ele terá que enfrentar e, nesse caso, aumentar seu conhecimento.

Candidatei-me a um estágio na equipe de desenvolvimento do depurador Corotin da Kotlin.

A tarefa dessa equipe durante um estágio para aqueles que a receberem este ano será finalizar essa parte do depurador e sua integração com o IDE.

A tarefa era um pouco esperada - escrever um depurador para um pequeno PL.

Eu não diria que é complexo, pelo contrário. Não requer nenhum conhecimento aprofundado da teoria da construção de tradutores e uma habilidade interessante. Mas, no entanto, quem se candidatar a um estágio nesta área deve ter pelo menos essas noções básicas e lidar com essa tarefa sem problemas. Fiquei surpreso quando decidi pesquisar no github por palavras-chave as soluções dos meus "concorrentes" e encontrei 1-2 soluções mais ou menos funcionais em cerca de 6 a 7 repositórios vazios ou com alguns códigos depois dos quais as pessoas desistiram. Talvez eu estivesse mal, mas os resultados não me agradaram. Se este post for lido por pessoas que abandonaram essa tarefa - não é necessário fazer isso no futuro. Em um caso extremo, bastava permanecer na tarefa por alguns dias e tenho certeza de que você lidaria com ela.

O texto da missão
Objetivo: implementar a execução passo a passo do código para a linguagem de programação trivial Guu.

Atenção: na descrição abaixo, alguns pontos significativos são deliberadamente omitidos. Como regra, eles permanecem a seu critério. Se for completamente incompreensível, escreva para (aqui está o e-mail que eu decidi remover).

Um programa Guu consiste em um conjunto de procedimentos. Cada procedimento começa com a linha sub (subname) e termina com a declaração de outro procedimento (ou o final do arquivo, se o procedimento no arquivo for o último). A execução começa com sub Main.

O corpo de um procedimento é um conjunto de instruções, cada uma em uma linha separada. Guias ou espaços insignificantes podem ocorrer no início de uma linha. Linhas em branco são ignoradas. Não há comentários sobre Guu.

Guu possui apenas três operadores: - set (varname) (novo valor) - configurando um novo valor inteiro para a variável. - chamar (subnome) - chama o procedimento. As chamadas podem ser recursivas. - print (varname) - imprime o valor da variável na tela.

Variáveis ​​em Guu têm um escopo global. O programa abaixo exibirá a linha a = 2.

sub principal
definir um 1
chamar foo
imprima um

sub foo
defina um 2

E aqui está o programa mais simples com recursão infinita:

sub principal
chamada principal

Você precisa escrever um intérprete passo a passo para Guu. Quando iniciado, o depurador deve parar na linha com a primeira instrução em sub main e aguardar comandos do usuário. Conjunto mínimo necessário de comandos do depurador:

i - entrar, o depurador entra na chamada (subnome).
o - pule, o depurador não entra na chamada.
trace - execução de rastreamento de pilha de impressão com números de linhas a partir de ...
var - imprime valores de todas as variáveis ​​declaradas.

O formato de comunicação do usuário com o depurador é deixado a critério acima. Você pode escolher uma interface minimalista do tipo GDB ou um console ou interface gráfica. Os nomes dos comandos do depurador podem ser alterados, se desejado.

Para resolver esse problema, você pode usar qualquer linguagem de programação do TIOBE TOP 50 e um compilador / intérprete de código aberto.

Ao avaliar o trabalho será avaliado:

O desempenho geral do programa;
A qualidade do código fonte e a disponibilidade de testes;
Fácil de expandir a funcionalidade (por exemplo, suporte para novas instruções de idioma ou instruções do depurador).
Uma solução com instruções para compilá-la deve ser publicada no repositório Git (por exemplo, no GitHub ou BitBucket). Na resposta, você precisa especificar um link para o repositório. Um link para um repositório GitHub privado também é adequado, apenas você precisará me adicionar a ele.

Eu escrevo em C ++, Java e Object Pascal.

No início, pensei em escrever tudo no meu mesmo MPS, mas achei que não seria muito conveniente procurar um funcionário da JB e enviei a inscrição 2 dias antes do encerramento da submissão (exames da mesma forma ...), e já era noite do lado de fora da janela - decidi escrever rapidamente tudo em idiomas mais conhecidos.

Na minha opinião, Pascal é mais adequado para resolver o problema, pelo menos por causa da implementação mais conveniente de strings ...

Pelo menos para mim. Além disso, ele está no TIOBE TOP 50, então lancei corajosamente o IDE, o Lazarus, porque ele não é comercial :) e começou a resolver o problema.

Apesar de fornecerem ao JB até 7 dias, levei cerca de uma hora para concluir o projeto, e o projeto acabou por ser cerca de 500 linhas de código.

Por onde começar?


Primeiro de tudo, você precisa imaginar como a depuração de código funcionará no final.

Precisamos implementar a execução de código passo a passo - isso significa que cada instrução deve ser apresentada na forma de uma estrutura / classe e, em geral, as instruções devem parecer uma lista dessas classes ou, como na minha implementação, referir-se uma à outra formando uma sequência (vou escrever por que fiz isso mais tarde).

Para obter essa sequência, nosso depurador precisa processar o código na linguagem proposta, o que significa que também precisamos implementar um analisador pequeno, bem como análises sintáticas e semânticas do código.

Vamos começar com a implementação do analisador. Porque Como a linguagem Guu consiste em um conjunto de tokens, separados por um espaço, é lógico escrever primeiro um tokenizador pequeno e simples:

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; 

Em seguida, declare enum de tokens:

 type TGuuToken = (opSub, opSet, opCall, opPrint, opUnknown); const GuuToken: array[opSub..opPrint] of string = ( 'sub', 'set', 'call', 'print' ); 

E a própria classe de instrução, na qual analisaremos as linhas de código:

 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; 

No OpType, a instrução será armazenada, no OpArgs - o restante da construção.
OpLine, OpUnChangedLine - informações para o depurador.

NextOp é um ponteiro para a próxima instrução. Se for igual a zero (nulo em Pascal), não há mais instruções e você precisará concluir o código ou retornar através da pilha de retorno de chamada.

O OpReg é um pequeno registrador de ponteiro, que será usado posteriormente para uma pequena otimização da execução do código.

Depois que a declaração da classe foi escrita - eu decidi que a solução mais compacta e bonita seria adicionar o analisador e um pouco de análise em seu construtor, o que fiz a seguir:

 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; 

Aqui, verificamos essencialmente o início da construção (ou seja, a primeira palavra) e, em seguida, examinamos os tokens restantes e seu número. Se algo estiver claramente errado com o código, exibimos um erro.

Na parte principal do código, simplesmente lemos o código no TStringList do arquivo, chamamos os construtores TGuuOp linha por linha e salvamos os ponteiros nas instâncias de classe no GuuOps: TList.

Anúncios:

 var LabelNames: TStringList; GuuOps, GuuVars: TList; SubMain: TGuuOp = nil; 

Juntamente com a análise de código, seria bom executar mais algumas ações:

 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; 

Nesta fase, você pode verificar os pontos de entrada no momento da redefinição e pensar no OpReg - usei-o para armazenar um ponteiro para uma variável Guu.

Falando em variáveis, levei esse pequeno pedaço de código para uma unidade separada:

 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. 

Agora analisamos o código que parece estar correto na sintaxe. Resta analisá-lo e você pode começar a executar a coisa mais importante - depuração.

Em seguida, você precisa implementar um pouco de análise semântica e preparar simultaneamente tudo para execução e depuração de código:

 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; 

No TGuuOp.NextOp de cada token, escreva um ponteiro para o próximo token.
Para o código de chamada, estamos fazendo isso de maneira complicada e simples - no NextOp, escrevemos um ponteiro para o ponto de entrada chamado.

Também verificamos as variáveis ​​de saída através da instrução print ...

Talvez eles não tenham sido anunciados antes da conclusão?

Agora você precisa implementar a execução do código. Volte para a classe TGuuOp e implemente o método 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; 

Para evitar a violação de acesso no caso de um loop - é melhor limitar a pilha, o que eu fiz.
A constante STACK_SIZE = 2048, declarada acima, é apenas responsável por isso.

Agora chegou a hora de escrever o código principal do nosso depurador:

 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. 

Pela condição do trabalho, a interface pode ser implementada como você desejar.

Seria possível implementar uma interface de usuário completa, parafusar o SynEdit no projeto, mas, na minha opinião, é um trabalho vazio que não refletirá a habilidade e, além disso, não será pago :)

Então, me limitei a uma pequena interface do usuário do console.

O código acima não é complicado, portanto você pode deixá-lo sem comentar. Nele, pegamos TGuuOp's prontos e os chamamos de Step.

Capturas de tela do problema resolvido:

imagem

imagem

Saída de informações de erro:

imagem

imagem

Link para o repositório da minha solução: clique em

Sumário


Não há resultados particulares. Vou ter que dedicar a maior parte do verão a férias agitadas e procurar uma universidade (bem, caso eu passe bem no exame, é claro), em vez de dois meses de trabalho e treinamento na equipe JetBrains.

Talvez no próximo ano um novo post apareça na Habré, já descrevendo o processo de estágio na JB ou em outra empresa interessante para mim :)

Source: https://habr.com/ru/post/pt454354/


All Articles