Pasantía en JetBrains y cómo casi pude conseguirlo

imagen

Al igual que muchos desarrolladores jóvenes, cuando hay un deseo de encontrar un trabajo / pasantía, miro en la dirección de las mejores empresas de TI.

Recientemente intenté ingresar a las filas de JetBrains y, bajo el corte, estoy listo para compartir mi experiencia.

¿Por qué tuvo éxito "casi"?


Seguramente tienes una pregunta de inmediato.

En mi opinión, tengo un buen currículum con muchos logros y una buena habilidad, que he estado mejorando los últimos 8-9 años día tras día.

Completé la tarea de prueba (y me parece bien), visité previamente la oficina de JB, que se encuentra en mi ciudad, hablé con HH y algunos desarrolladores de la compañía, y como resultado se me negó una pasantía sin ningún comentario.

Lo más probable es que la razón radique en el hecho de que JetBrains selecciona a los estudiantes exclusivamente para la pasantía, y en este momento me acabo de graduar del 11 y aprobar los exámenes uno tras otro.

Bueno, esta es una ocasión para otro año entero para levantarte y solicitar el próximo año.

Análisis de la tarea de prueba.


Los plazos para enviar las solicitudes de pasantías y las tareas de prueba de prueba han terminado, lo que significa que todos los que los resolvieron, incluido yo, pueden publicar un análisis de estas tareas para que el próximo año cualquier estudiante interesado pueda familiarizarse con el nivel aproximado de las tareas antes de comenzar las pasantías JB, con que tendrá que enfrentar y en cuyo caso extraer sus conocimientos.

Solicité una pasantía con el equipo de desarrollo del depurador Corotin para Kotlin.

La tarea de este equipo durante una pasantía para quienes lo alcanzaron este año será finalizar esta parte del depurador y su integración con el IDE.

La tarea era un poco esperada: escribir un depurador para un PL pequeño.

No diría que es complejo, sino todo lo contrario. No requiere ningún conocimiento profundo de la teoría de la construcción de traductores y una habilidad genial. Sin embargo, aquellos que soliciten una pasantía en esta área al menos deberían tener estos conceptos básicos y hacer frente a esta tarea sin ningún problema. Me sorprendió cuando decidí buscar en github las palabras clave de las soluciones de mis "competidores" y encontré 1-2 soluciones más o menos funcionales en contra de unos 6-7 repositorios vacíos o con un par de códigos después de lo cual la gente se rindió. Tal vez estaba mirando mal, pero sin embargo, los resultados no me agradaron. Si esta publicación será leída por personas que abandonaron esta tarea, no hay necesidad de hacerlo en el futuro. En un caso extremo, fue suficiente para sentarse en la tarea durante un par de días y estoy seguro de que se ocuparía de ella.

El texto de la búsqueda.
Objetivo: implementar la ejecución de código paso a paso para el lenguaje de programación trivial Guu.

Atención: en la descripción a continuación, algunos puntos significativos se omiten deliberadamente. Como regla general, quedan a su discreción. Si es completamente incomprensible, escriba a (aquí está el correo que decidí eliminar).

Un programa Guu consiste en un conjunto de procedimientos. Cada procedimiento comienza con la línea sub (subnombre) y termina con la declaración de otro procedimiento (o el final del archivo si el procedimiento en el archivo es el último). La ejecución comienza con sub main.

El cuerpo de un procedimiento es un conjunto de instrucciones, cada una de las cuales está en una línea separada. Pueden aparecer tabulaciones o espacios insignificantes al comienzo de una línea. Las líneas en blanco se ignoran. No hay comentarios sobre Guu.

Guu solo tiene tres operadores: - set (varname) (nuevo valor) - establece un nuevo valor entero para la variable. - llamar (subnombre) - llamar al procedimiento. Las llamadas pueden ser recursivas. - print (varname): imprime el valor de la variable en la pantalla.

Las variables en Guu tienen un alcance global. El siguiente programa mostrará la línea a = 2.

sub principal
establecer un 1
llamar a foo
imprimir un

sub foo
establecer un 2

Y aquí está el programa más simple con recursión infinita:

sub principal
llamada principal

Necesita escribir un intérprete paso a paso para Guu. Cuando se inicia, el depurador debe detenerse en la línea con la primera instrucción en sub main y esperar los comandos del usuario. Conjunto mínimo requerido de comandos del depurador:

i - paso, el depurador entra dentro de la llamada (subnombre).
o - dé un paso al frente, el depurador no entra dentro de la llamada.
trace: imprime la ejecución de seguimiento de la pila con números de línea a partir de main ...
var: imprime valores de todas las variables declaradas.

El formato de comunicación del usuario con el depurador se deja a la discreción anterior. Puede elegir una interfaz minimalista tipo GDB o una consola o interfaz gráfica de usuario. Los nombres de los comandos del depurador se pueden cambiar si se desea.

Para resolver este problema, puede usar cualquier lenguaje de programación de TIOBE TOP 50 y un compilador / intérprete de código abierto.

Al evaluar el trabajo se evaluará:

El rendimiento general del programa;
La calidad del código fuente y la disponibilidad de pruebas;
Funcionalidad fácil de expandir (por ejemplo, soporte para nuevas instrucciones de lenguaje o instrucciones de depuración).
Una solución con instrucciones para compilarla debe publicarse en el repositorio de Git (por ejemplo, en GitHub o BitBucket). En la respuesta, debe especificar un enlace al repositorio. También es adecuado un enlace a un repositorio privado de GitHub, solo que necesitará agregarme a él.

Escribo en C ++, Java y Object Pascal.

Al principio pensé en escribir todo en mi mismo MPS, pero pensé que no sería muy conveniente buscar un empleado de JB, y presenté la solicitud 2 días antes del cierre de la presentación (los exámenes de todos modos ...), y ya era de noche fuera de la ventana: decidí escribir rápidamente todo en idiomas más conocidos.

En mi opinión, Pascal es el más adecuado para resolver el problema, al menos debido a la implementación más conveniente de las cadenas ...

Al menos para mi. Además, está en TIOBE TOP 50, así que audazmente lancé el IDE, a saber, Lazarus, porque él no es comercial :) y se propuso resolver el problema.

A pesar de que le dan a JB hasta 7 días, me llevó alrededor de una hora completar el proyecto, y el proyecto resultó ser unas 500 líneas de código.

Por donde empezar


En primer lugar, debe imaginar cómo funcionará la depuración de código al final.

Necesitamos implementar la ejecución de código paso a paso, eso significa que cada instrucción debe presentarse en forma de una estructura / clase y, en general, las instrucciones deben verse como una lista de estas clases o, como en mi implementación, referirse entre sí formando una secuencia (escribiré por qué lo hice más adelante).

Para obtener esta secuencia, nuestro depurador necesita procesar el código en el lenguaje propuesto, lo que significa que también debemos implementar un pequeño analizador, así como un análisis sintáctico y semántico del código.

Comencemos con la implementación del analizador. Porque Dado que el lenguaje Guu consiste en un conjunto de tokens, separados por un espacio, es lógico escribir primero un tokenizador pequeño y simple:

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; 

A continuación, declare la enumeración de los tokens:

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

Y la clase de instrucción en sí, en la que analizaremos las líneas 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; 

En OpType, la instrucción se almacenará, en OpArgs, el resto de la construcción.
OpLine, OpUnChangedLine: información para el depurador.

NextOp es un puntero a la siguiente declaración. Si es igual a nulo (nulo en Pascal), entonces no hay más instrucciones y debe completar el código o regresar a través de la pila de devolución de llamada.

OpReg es un pequeño registro de puntero, que se utilizará más adelante para una pequeña optimización de la ejecución del código.

Después de escribir la declaración de clase, decidí que la solución más compacta y hermosa sería agregar el analizador y un poco de análisis en su constructor, lo que hice a continuación:

 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; 

Aquí esencialmente verificamos el comienzo de la construcción (es decir, la primera palabra), y luego miramos las fichas restantes y su número. Si algo está claramente mal con el código, mostramos un error.

En la parte principal del código, simplemente leemos el código en la TStringList del archivo, llamamos a los constructores TGuuOp línea por línea y almacenamos los punteros a las instancias de clase en GuuOps: TList.

Anuncios:

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

Junto con el análisis de código, sería bueno realizar un par de acciones más:

 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; 

En esta etapa, puede verificar los puntos de entrada en el momento de la redefinición y pensar en OpReg: lo usé para almacenar un puntero a una variable Guu.

Hablando de variables, tomé este pequeño código en una unidad 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. 

Ahora hemos analizado el código que parece ser correcto en sintaxis. Queda por analizarlo y puede comenzar a realizar lo más importante: la depuración.

A continuación, debe implementar un pequeño análisis semántico y preparar simultáneamente todo para la ejecución y depuración 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; 

En TGuuOp.NextOp de cada token, escriba un puntero al siguiente token.
Para el código de operación de la llamada, lo estamos haciendo complicado y simple: en NextOp escribimos un puntero al punto de entrada llamado.

También verificamos las variables de salida a través de la declaración de impresión ...

¿Quizás no fueron anunciados antes de la conclusión?

Ahora necesita implementar la ejecución del código. Regrese a la clase TGuuOp e implemente el 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 la violación de acceso en caso de un bucle, es mejor limitar la pila, lo cual hice.
La constante STACK_SIZE = 2048, declarada anteriormente, es solo responsable de esto.

Ahora es hora de escribir el código principal de nuestro 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. 

Por la condición del trabajo, la interfaz se puede implementar a su gusto.

Sería posible implementar una interfaz de usuario completa, atornillar SynEdit al proyecto, pero en mi opinión es un trabajo vacío que no reflejará la habilidad, y además no se pagará :)

Así que me limité a una pequeña interfaz de usuario de consola.

El código anterior no es complicado, por lo que puede dejarlo sin comentarios. En él, tomamos TGuuOp ya preparados y los llamamos Paso.

Capturas de pantalla del problema resuelto:

imagen

imagen

Salida de información de error:

imagen

imagen

Enlace al repositorio de mi solución: haga clic en

Resumen


No hay resultados particulares. Tendré que dedicar la mayor parte del verano a unas vacaciones ocupadas y buscar una universidad (bueno, en caso de que apruebe bien el examen, por supuesto), en lugar de dos meses de trabajo y capacitación en el equipo de JetBrains.

Quizás el próximo año aparezca una nueva publicación en Habré, que ya describa el proceso de pasantía en JB o en otra compañía que me interese :)

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


All Articles