
Comme beaucoup de jeunes développeurs, quand il y a un désir de trouver un emploi / stage, je regarde dans la direction des entreprises IT cool.
Récemment, j'ai essayé d'entrer dans les rangs de JetBrains et sous la coupe, je suis prêt à partager mon expérience.
Pourquoi «presque» a-t-il réussi?
Vous avez sûrement immédiatement une telle question.
À mon avis, j'ai un bon curriculum vitae avec un tas de réalisations et une bonne compétence, que j'améliore jour après jour les 8-9 dernières années.
J'ai terminé la tâche de test (et cela me semble bien), j'ai déjà visité le bureau de JB, qui est situé dans ma ville, parlé à HH et à certains développeurs de l'entreprise, et en conséquence, on m'a refusé un stage sans aucun commentaire.
Très probablement, la raison réside dans le fait que JetBrains sélectionne exclusivement les étudiants pour le stage, et pour le moment je viens de terminer le 11 et de passer les examens les uns après les autres.
Eh bien, c'est l'occasion pour une autre année entière de se relever et de postuler pour l'année suivante.
Analyse de la tâche de test
Les délais de soumission des demandes de stage et de test des devoirs de test sont dépassés, ce qui signifie que tous ceux qui les ont résolus, y compris moi, peuvent publier une analyse de ces missions afin que l'année prochaine tout étudiant intéressé puisse se familiariser avec le niveau approximatif des missions avant de commencer les stages JB, avec auquel il devra faire face et dans ce cas pour remonter ses connaissances.
J'ai postulé pour un stage avec l'équipe de développement du débogueur Corotin pour Kotlin.
La tâche de cette équipe lors d'un stage pour ceux qui l'ont touchée cette année sera de finaliser cette partie du débogueur et son intégration avec l'IDE.
La tâche était un peu attendue - écrire un débogueur pour un petit PL.
Je ne dirais pas que c'est complexe, bien au contraire. Il ne nécessite aucune connaissance approfondie de la théorie de la construction de traducteurs et une compétence cool. Mais néanmoins, ceux qui postulent pour un stage dans ce domaine devraient au moins avoir ces bases et faire face à cette tâche sans aucun problème. J'ai été surpris lorsque j'ai décidé de rechercher sur github des mots-clés les solutions de mes "concurrents" et trouvé 1-2 solutions plus ou moins fonctionnelles contre environ 6-7 référentiels vides ou avec quelques morceaux de code après quoi les gens ont abandonné. Peut-être que je regardais mal, mais néanmoins, les résultats ne m'ont pas plu. Si ce post sera lu par des personnes qui ont abandonné cette tâche - pas besoin de le faire à l'avenir. Dans un cas extrême, il suffisait de s'asseoir sur la tâche pendant quelques jours et je suis sûr que vous vous en occuperiez.
Le texte de la quête elle-mêmeObjectif: implémenter l'exécution de code étape par étape pour le langage de programmation trivial Guu.
Attention: dans la description ci-dessous, certains points importants sont volontairement omis. En règle générale, ils restent à votre discrétion. Si c'est complètement incompréhensible, écrivez à (voici le mail que j'ai décidé de supprimer).
Un programme Guu consiste en un ensemble de procédures. Chaque procédure commence par la ligne sub (subname) et se termine par la déclaration d'une autre procédure (ou la fin du fichier si la procédure dans le fichier est la dernière). L'exécution commence par le sous-principal.
Le corps d'une procédure est un ensemble d'instructions, chacune étant sur une ligne distincte. Des tabulations ou des espaces insignifiants peuvent apparaître au début d'une ligne. Les lignes vides sont ignorées. Il n'y a aucun commentaire sur Guu.
Guu n'a que trois opérateurs: - set (varname) (nouvelle valeur) - définissant une nouvelle valeur entière pour la variable. - call (subname) - appelle la procédure. Les appels peuvent être récursifs. - print (varname) - imprime la valeur de la variable à l'écran.
Les variables de Guu ont une portée globale. Le programme ci-dessous affichera la ligne a = 2.
sous-principal
définir un 1
appeler foo
imprimer un
sous foo
définir un 2
Et voici le programme le plus simple avec une récursion infinie:
sous-principal
appel principal
Vous devez écrire un interprète étape par étape pour Guu. Lorsqu'il démarre, le débogueur doit s'arrêter sur la ligne avec la première instruction en sous-main et attendre les commandes de l'utilisateur. Ensemble minimal requis de commandes de débogage:
i - entrez, le débogueur entre dans l'appel (sous-nom).
o - enjamber, le débogueur ne va pas à l'intérieur de l'appel.
trace - impression de l'exécution de la trace de la pile avec des numéros de ligne à partir de ...
var - affiche les valeurs de toutes les variables déclarées.
Le format de communication de l'utilisateur avec le débogueur est laissé à la discrétion ci-dessus. Vous pouvez choisir soit une interface minimaliste de type GDB, soit une console ou une interface graphique. Les noms des commandes du débogueur peuvent être modifiés si vous le souhaitez.
Pour résoudre ce problème, vous pouvez utiliser n'importe quel langage de programmation de TIOBE TOP 50 et un compilateur / interprète open-source.
Lors de l'évaluation du travail sera évalué:
La performance globale du programme;
La qualité du code source et la disponibilité des tests;
Fonctionnalité facile à étendre (par exemple, prise en charge de nouvelles instructions de langue ou instructions de débogage).
Une solution avec des instructions pour la construire doit être publiée dans le référentiel Git (par exemple, sur GitHub ou BitBucket). Dans la réponse, vous devez spécifier un lien vers le référentiel. Un lien vers un référentiel GitHub privé convient également, vous seul devrez y ajouter.
J'écris en C ++, Java et Object Pascal.
Au début, il y avait des pensées pour tout écrire dans mon même MPS, mais je pensais que ce ne serait pas très pratique de vérifier pour un employé JB, et j'ai soumis la candidature 2 jours avant la clôture de la soumission (examens tout de même ...), et c'était déjà le soir devant la fenêtre - j'ai décidé d'écrire rapidement tout dans des langues plus connues.
À mon avis, Pascal est le plus approprié pour résoudre le problème, au moins en raison de la mise en œuvre la plus pratique des chaînes ...
Du moins pour moi. De plus, il est dans TIOBE TOP 50, j'ai donc lancé hardiment l'IDE, à savoir Lazarus, car il n'est pas commercial :) et se mit à résoudre le problème.
Malgré le fait qu'ils donnent à JB jusqu'à 7 jours, cela m'a pris environ une heure pour terminer le projet, et le projet s'est avéré être environ 500 lignes de code.
Par où commencer?
Tout d'abord, vous devez imaginer comment le débogage de code fonctionnera à la fin.
Nous devons implémenter l'exécution de code étape par étape - cela signifie que chaque instruction doit être présentée sous la forme d'une structure / classe et, en général, les instructions doivent ressembler à une liste de ces classes ou, comme dans mon implémentation, se référer les unes aux autres formant une séquence (je noterai pourquoi je l'ai fait plus tard).
Pour obtenir cette séquence, notre débogueur doit traiter le code dans le langage proposé, ce qui signifie que nous devons également implémenter un petit analyseur, ainsi qu'une analyse syntaxique et sémantique du code.
Commençons par l'implémentation de l'analyseur. Parce que Étant donné que la langue Guu se compose d'un ensemble de jetons, séparés par un espace, il est logique d'écrire d'abord un petit et simple tokenizer:
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;
Ensuite, déclarez l'énumération des jetons:
type TGuuToken = (opSub, opSet, opCall, opPrint, opUnknown); const GuuToken: array[opSub..opPrint] of string = ( 'sub', 'set', 'call', 'print' );
Et la classe d'instruction elle-même, dans laquelle nous analyserons les lignes de code:
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;
Dans OpType, l'instruction sera stockée, dans OpArgs - le reste de la construction.
OpLine, OpUnChangedLine - informations pour le débogueur.
NextOp est un pointeur vers l'instruction suivante. S'il est égal à nil (null en Pascal), il n'y a pas d'autres instructions et vous devez compléter le code ou retourner via la pile de rappel.
OpReg est un petit registre de pointeurs, qui sera utilisé plus tard pour une petite optimisation de l'exécution du code.
Après la déclaration de classe a été écrite - j'ai décidé que la solution la plus compacte et la plus belle serait d'ajouter l'analyseur et un peu d'analyse dans son constructeur, ce que j'ai fait ensuite:
constructor TGuuOp.Create(LineNum: Cardinal; Line:string); 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
Ici, nous vérifions essentiellement le début de la construction (c'est-à-dire le premier mot), puis regardons les jetons restants et leur nombre. Si quelque chose ne va clairement pas avec le code, nous affichons une erreur.
Dans le morceau de code principal, nous lisons simplement le code dans la TStringList du fichier, appelons les constructeurs TGuuOp ligne par ligne et enregistrons les pointeurs vers les instances de classe dans GuuOps: TList.
Annonces:
var LabelNames: TStringList; GuuOps, GuuVars: TList; SubMain: TGuuOp = nil;
Avec l'analyse de code, il serait intéressant d'effectuer quelques actions supplémentaires:
procedure ParseNext(LineNum: Cardinal; Line: string); 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
À ce stade, vous pouvez vérifier les points d'entrée au moment de la redéfinition et penser à OpReg - je l'ai utilisé pour stocker un pointeur sur une variable Guu.
En parlant de variables, j'ai pris ce petit morceau de code dans une unité distincte:
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.
Nous avons maintenant un code analysé qui semble être correct dans la syntaxe. Il reste à l'analyser et vous pouvez commencer à effectuer la chose la plus importante - le débogage.
Ensuite, vous devez implémenter une petite analyse sémantique et préparer simultanément tout pour l'exécution de code et le débogage:
procedure CheckSemantic; 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;
Dans TGuuOp.NextOp de chaque jeton, écrivez un pointeur sur le jeton suivant.
Pour l'opcode call, nous le faisons délicat et simple - dans NextOp, nous écrivons un pointeur vers le point d'entrée appelé.
Nous vérifions également les variables de sortie via l'instruction print ...
Peut-être qu'ils n'ont pas été annoncés avant la conclusion?
Vous devez maintenant implémenter l'exécution de code. Revenez à la classe TGuuOp et implémentez la méthode Step:
function TGuuOp.Step(StepInto: boolean; CallBacks: TList; Trace: TStringList): TGuuOp; 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;
Pour éviter la violation d'accès en cas de boucle - il est préférable de limiter la pile, ce que j'ai fait.
La constante STACK_SIZE = 2048, déclarée ci-dessus est juste responsable de cela.
Il est maintenant temps d'écrire le code principal de notre débogueur:
var code: TStringList; c: Cardinal; cmd: string; CallBacks: TList; Trace: TStringList; DebugMode: boolean = true; begin if ParamCount > 0 then begin
Selon l'état du travail, l'interface peut être implémentée comme vous le souhaitez.
Il serait possible d'implémenter une interface utilisateur à part entière, visser SynEdit au projet, mais à mon avis, c'est un travail vide qui ne reflétera pas la compétence, et en plus il ne sera pas payé :)
Je me suis donc limité à une petite interface utilisateur de console.
Le code ci-dessus n'est pas compliqué, vous pouvez donc le laisser sans commentaire. Dans ce document, nous prenons des TGuuOp prêts à l'emploi et les appelons Step.
Captures d'écran du problème résolu:


Sortie d'informations d'erreur:


Lien vers le référentiel de ma solution:
cliquez surRésumé
Il n'y a pas de résultats particuliers. Je vais devoir consacrer la majeure partie de l’été à des vacances bien remplies et chercher une université (enfin, au cas où je réussirais bien l’examen, bien sûr), au lieu de deux mois de travail et de formation dans l’équipe JetBrains.
Peut-être que l'année prochaine un nouveau billet apparaîtra sur Habré, décrivant déjà le processus de stage chez JB ou dans une autre entreprise qui m'intéresse :)