
Wie viele junge Entwickler schaue ich, wenn der Wunsch besteht, einen Job / ein Praktikum zu finden, in Richtung cooler IT-Unternehmen.
Kürzlich habe ich versucht, in die Reihen der JetBrains einzusteigen, und unter dem Schnitt bin ich bereit, meine Erfahrungen zu teilen.
Warum war es „fast“ erfolgreich?
Sicher haben Sie sofort eine solche Frage.
Meiner Meinung nach habe ich einen guten Lebenslauf mit einer Reihe von Erfolgen und guten Fähigkeiten, die ich in den letzten 8-9 Jahren Tag für Tag verbessert habe.
Ich habe die Testaufgabe abgeschlossen (und es scheint mir gut zu sein), zuvor das Büro von JB in meiner Stadt besucht, mit HH und einigen Entwicklern des Unternehmens gesprochen und als Ergebnis ein Praktikum ohne Kommentare verweigert.
Der Grund liegt höchstwahrscheinlich in der Tatsache, dass JetBrains Studenten ausschließlich für das Praktikum auswählt, und im Moment habe ich gerade den 11. abgeschlossen und die Prüfungen nacheinander bestanden.
Nun, dies ist eine Gelegenheit für ein weiteres ganzes Jahr, sich hochzuziehen und sich für das nächste Jahr zu bewerben.
Analyse der Testaufgabe
Die Fristen für die Einreichung von Bewerbungen für Praktika und das Testen von Testaufgaben sind abgelaufen. Dies bedeutet, dass jeder, der sie gelöst hat, einschließlich mir, eine Analyse dieser Aufgaben veröffentlichen kann, damit sich jeder interessierte Student im nächsten Jahr mit dem ungefähren Auftragsniveau vertraut machen kann, bevor er mit JB-Praktika beginnt dem er sich stellen muss und in welchem Fall um sein Wissen zu sammeln.
Ich habe mich für ein Praktikum beim Corotin Debugger-Entwicklungsteam für Kotlin beworben.
Die Aufgabe dieses Teams während eines Praktikums für diejenigen, die es in diesem Jahr absolviert haben, wird es sein, diesen Teil des Debuggers und seine Integration in die IDE abzuschließen.
Die Aufgabe wurde ein wenig erwartet - einen Debugger für einen kleinen PL zu schreiben.
Ich würde nicht sagen, dass es komplex ist, eher das Gegenteil. Es erfordert keine gründlichen Kenntnisse der Theorie des Konstruierens von Übersetzern und eine coole Fähigkeit. Wer sich jedoch für ein Praktikum in diesem Bereich bewirbt, sollte zumindest über diese Grundlagen verfügen und diese Aufgabe problemlos bewältigen können. Ich war überrascht, als ich mich entschied, auf github nach Schlüsselwörtern für die Lösungen meiner "Konkurrenten" zu suchen und 1-2 mehr oder weniger funktionierende Lösungen gegen etwa 6-7 leere Repositories oder mit ein paar Codeteilen zu finden, nach denen die Leute aufgaben. Vielleicht sah ich schlecht aus, aber die Ergebnisse gefielen mir trotzdem nicht. Wenn dieser Beitrag von Personen gelesen wird, die diese Aufgabe aufgegeben haben, müssen Sie dies in Zukunft nicht mehr tun. Im Extremfall hat es gereicht, ein paar Tage auf der Aufgabe zu sitzen, und ich bin sicher, dass Sie sich damit befassen würden.
Der Text der Quest selbstZiel: Implementierung einer schrittweisen Codeausführung für die triviale Programmiersprache Guu.
Achtung: In der folgenden Beschreibung werden einige wichtige Punkte absichtlich weggelassen. Sie liegen in der Regel in Ihrem Ermessen. Wenn es völlig unverständlich ist, schreiben Sie an (hier ist die Mail, die ich entfernen wollte).
Ein Guu-Programm besteht aus einer Reihe von Verfahren. Jede Prozedur beginnt mit dem Zeilensub (Subname) und endet mit der Deklaration einer anderen Prozedur (oder dem Ende der Datei, wenn die Prozedur in der Datei die letzte ist). Die Ausführung beginnt mit sub main.
Der Hauptteil einer Prozedur besteht aus einer Reihe von Anweisungen, die sich jeweils in einer separaten Zeile befinden. Am Anfang einer Zeile können unbedeutende Tabulatoren oder Leerzeichen auftreten. Leerzeilen werden ignoriert. Es gibt keine Kommentare zu Guu.
Guu hat nur drei Operatoren: - set (varname) (neuer Wert) - Setzen eines neuen ganzzahligen Werts für die Variable. - call (subname) - rufe die Prozedur auf. Anrufe können rekursiv sein. - print (varname) - Gibt den Wert der Variablen auf dem Bildschirm aus.
Variablen in Guu haben einen globalen Geltungsbereich. Das folgende Programm zeigt die Zeile a = 2 an.
sub main
setze eine 1
ruf foo an
drucken a
sub foo
setze eine 2
Und hier ist das einfachste Programm mit unendlicher Rekursion:
sub main
Haupt anrufen
Sie müssen einen Schritt-für-Schritt-Dolmetscher für Guu schreiben. Beim Start sollte der Debugger in der Zeile mit der ersten Anweisung in Sub-Main anhalten und auf Befehle des Benutzers warten. Erforderlicher Mindestsatz an Debugger-Befehlen:
i - Schritt in, der Debugger geht in Aufruf (Subname).
o - Schritt über, der Debugger geht nicht in den Aufruf.
trace - Stack-Trace-Ausführung mit Zeilennummern ab Haupt ...
var - Druckwerte aller deklarierten Variablen.
Das Kommunikationsformat des Benutzers mit dem Debugger liegt im oben genannten Ermessen. Sie können entweder eine minimalistische GDB-ähnliche Oberfläche oder eine Konsole oder eine grafische Benutzeroberfläche auswählen. Die Namen der Debugger-Befehle können bei Bedarf geändert werden.
Um dieses Problem zu lösen, können Sie eine beliebige Programmiersprache von TIOBE TOP 50 und einen Open-Source-Compiler / Interpreter verwenden.
Bei der Bewertung wird die Arbeit bewertet:
Die Gesamtleistung des Programms;
Die Qualität des Quellcodes und die Verfügbarkeit von Tests;
Einfach zu erweiternde Funktionalität (z. B. Unterstützung für neue Sprachanweisungen oder Debugger-Anweisungen).
Eine Lösung mit Anweisungen zum Erstellen muss im Git-Repository veröffentlicht werden (z. B. auf GitHub oder BitBucket). In der Antwort müssen Sie einen Link zum Repository angeben. Ein Link zu einem privaten GitHub-Repository ist ebenfalls geeignet, nur Sie müssen mich hinzufügen.
Ich schreibe in C ++, Java und Object Pascal.
Anfangs gab es Gedanken, alles in mein MPS zu schreiben, aber ich dachte, es wäre nicht sehr bequem, nach einem JB-Mitarbeiter zu suchen, und ich reichte den Antrag 2 Tage vor Abschluss der Einreichung ein (Prüfungen trotzdem ...), und Es war schon Abend vor dem Fenster - ich beschloss, schnell alles in bekannteren Sprachen zu schreiben.
Meiner Meinung nach eignet sich Pascal am besten zur Lösung des Problems, zumindest aufgrund der bequemsten Implementierung von Zeichenfolgen ...
Zumindest für mich. Außerdem ist es in TIOBE TOP 50, also habe ich mutig die IDE gestartet, nämlich Lazarus, weil Er ist nicht kommerziell :) und machte sich daran, das Problem zu lösen.
Trotz der Tatsache, dass sie JB bis zu 7 Tage Zeit geben, habe ich ungefähr eine Stunde gebraucht, um das Projekt abzuschließen, und es stellte sich heraus, dass das Projekt ungefähr 500 Codezeilen umfasste.
Wo soll ich anfangen?
Zunächst müssen Sie sich vorstellen, wie das Debuggen von Code letztendlich funktioniert.
Wir müssen die schrittweise Codeausführung implementieren - das heißt, jede Anweisung sollte in Form einer Struktur / Klasse dargestellt werden, und im Allgemeinen sollten die Anweisungen wie eine Liste dieser Klassen aussehen oder sich wie in meiner Implementierung aufeinander beziehen und eine Sequenz bilden (ich werde aufschreiben, warum ich es später getan habe).
Um diese Sequenz zu erhalten, muss unser Debugger den Code in der vorgeschlagenen Sprache verarbeiten. Dies bedeutet, dass wir auch einen kleinen Parser sowie eine syntaktische und semantische Analyse des Codes implementieren müssen.
Beginnen wir mit der Implementierung des Parsers. Weil Da die Guu-Sprache aus einer Reihe von Token besteht, die durch ein Leerzeichen getrennt sind, ist es logisch, zuerst einen kleinen und einfachen Tokenizer zu schreiben:
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;
Als nächstes deklarieren Sie die Aufzählung der Token:
type TGuuToken = (opSub, opSet, opCall, opPrint, opUnknown); const GuuToken: array[opSub..opPrint] of string = ( 'sub', 'set', 'call', 'print' );
Und die Anweisungsklasse selbst, in die wir die Codezeilen analysieren werden:
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;
In OpType wird die Anweisung in OpArgs gespeichert - der Rest der Konstruktion.
OpLine, OpUnChangedLine - Informationen für den Debugger.
NextOp ist ein Zeiger auf die nächste Anweisung. Wenn es gleich nil ist (null in Pascal), gibt es keine weiteren Anweisungen und Sie müssen den Code vervollständigen oder über den Rückrufstapel zurückkehren.
OpReg ist ein kleines Zeigerregister, das später für eine kleine Optimierung der Codeausführung verwendet wird.
Nachdem die Klassendeklaration geschrieben wurde, entschied ich, dass die kompakteste und schönste Lösung darin bestehen würde, den Parser und ein wenig Analyse in seinem Konstruktor hinzuzufügen, was ich als nächstes tat:
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
Hier überprüfen wir im Wesentlichen den Beginn der Konstruktion (d. H. Das erste Wort) und betrachten dann die verbleibenden Token und ihre Anzahl. Wenn etwas mit dem Code eindeutig nicht stimmt, wird ein Fehler angezeigt.
Im Hauptcode lesen wir einfach den Code in der TStringList aus der Datei, rufen die TGuuOp-Konstruktoren zeilenweise auf und speichern die Zeiger auf die Klasseninstanzen in GuuOps: TList.
Ankündigungen:
var LabelNames: TStringList; GuuOps, GuuVars: TList; SubMain: TGuuOp = nil;
Zusammen mit der Code-Analyse wäre es schön, noch ein paar Aktionen auszuführen:
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
Zu diesem Zeitpunkt können Sie die Einstiegspunkte zum Zeitpunkt der Neudefinition überprüfen und an OpReg denken. Ich habe damit einen Zeiger auf eine Guu-Variable gespeichert.
Apropos Variablen, ich habe diesen kleinen Code in eine separate Einheit gebracht:
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.
Jetzt haben wir Code analysiert, der in der Syntax korrekt zu sein scheint. Es bleibt zu analysieren, und Sie können beginnen, das Wichtigste auszuführen - das Debuggen.
Als nächstes müssen Sie eine kleine semantische Analyse implementieren und gleichzeitig alles für die Ausführung und das Debuggen von Code vorbereiten:
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;
Schreiben Sie in TGuuOp.NextOp jedes Tokens einen Zeiger auf das nächste Token.
Für den Anruf-Opcode machen wir es schwierig und einfach - in NextOp schreiben wir einen Zeiger auf den aufgerufenen Einstiegspunkt.
Wir überprüfen die Ausgabevariablen auch über die print-Anweisung ...
Vielleicht wurden sie nicht vor dem Abschluss angekündigt?
Jetzt müssen Sie die Codeausführung implementieren. Kehren Sie zur TGuuOp-Klasse zurück und implementieren Sie die Step-Methode:
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;
Um eine Zugriffsverletzung im Falle einer Schleife zu vermeiden, ist es besser, den Stapel zu begrenzen, was ich getan habe.
Die oben deklarierte Konstante STACK_SIZE = 2048 ist nur dafür verantwortlich.
Jetzt ist es endlich Zeit, den Hauptcode unseres Debuggers zu schreiben:
var code: TStringList; c: Cardinal; cmd: string; CallBacks: TList; Trace: TStringList; DebugMode: boolean = true; begin if ParamCount > 0 then begin
Je nach Auftragsbedingung kann die Schnittstelle nach Ihren Wünschen implementiert werden.
Es wäre möglich, eine vollwertige Benutzeroberfläche zu implementieren und SynEdit mit dem Projekt zu verbinden, aber meiner Meinung nach ist es eine leere Arbeit, die die Fähigkeiten nicht widerspiegelt und außerdem nicht bezahlt wird :)
Also habe ich mich auf eine kleine Konsolen-Benutzeroberfläche beschränkt.
Der obige Code ist nicht kompliziert, daher können Sie ihn kommentarlos hinterlassen. Darin nehmen wir fertige TGuuOps und nennen sie Step.
Screenshots des gelösten Problems:


Ausgabe von Fehlerinformationen:


Link zum Repository meiner Lösung:
Klicken Sie aufZusammenfassung
Es gibt keine besonderen Ergebnisse. Ich muss den größten Teil des Sommers einem geschäftigen Urlaub widmen und nach einer Universität suchen (na ja, falls ich die Prüfung natürlich gut bestehe), anstatt zwei Monate im JetBrains-Team zu arbeiten und zu trainieren.
Vielleicht erscheint nächstes Jahr ein neuer Beitrag bei Habré, der bereits den Praktikumsprozess bei JB oder in einem anderen für mich interessanten Unternehmen beschreibt :)